use sql_dialect_fmt_parser::{parse, SyntaxKind};
use sql_dialect_fmt_test_support::parser::{assert_parse_clean, assert_parse_roundtrip};
const CLEAN: &[&str] = &[
"COPY INTO t FROM @s",
"COPY INTO t FROM @s/path/",
"COPY INTO t FROM @~/staged/",
"COPY INTO t FROM @%mytable/data/",
"COPY INTO db.sch.t FROM @db.sch.stage/orders/2026/06/",
"COPY INTO t (a, b, c) FROM @s",
"COPY INTO t FROM @s FILE_FORMAT = (TYPE = CSV)",
"COPY INTO t FROM @s FILE_FORMAT = (TYPE = CSV SKIP_HEADER = 1 FIELD_DELIMITER = ',')",
"COPY INTO t FROM @s FILE_FORMAT = (FORMAT_NAME = my_ff)",
"COPY INTO t FROM @s PATTERN = '.*[.]csv'",
"COPY INTO t FROM @s FILES = ('a.csv', 'b.csv')",
"COPY INTO t FROM @s ON_ERROR = CONTINUE",
"COPY INTO t FROM @s ON_ERROR = 'SKIP_FILE_5%' PURGE = FALSE FORCE = FALSE",
"COPY INTO t FROM @s MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE",
"COPY INTO t FROM @s VALIDATION_MODE = RETURN_ERRORS",
"COPY INTO t FROM @raw.stage/orders/ FILE_FORMAT = (TYPE = JSON) ON_ERROR = CONTINUE",
"COPY INTO t (a, b) FROM (SELECT $1, $2 FROM @s) FILE_FORMAT = (TYPE = CSV)",
"COPY INTO t FROM (SELECT $1:a::STRING FROM @s (FILE_FORMAT => my_ff))",
"COPY INTO t FROM (SELECT METADATA$FILENAME, $1 FROM @raw.stage/events/)",
"COPY INTO @s FROM t",
"COPY INTO @s/out/ FROM t",
"COPY INTO @~/exports/ FROM t",
"COPY INTO @mart.stage/daily/ FROM t",
"COPY INTO 'gcs://bucket/path/' FROM t FILE_FORMAT = (TYPE = PARQUET)",
"COPY INTO 's3://bucket/prefix/' FROM (SELECT * FROM t) OVERWRITE = TRUE",
"COPY INTO @s FROM (SELECT * FROM t)",
"COPY INTO @s FROM t PARTITION BY (dt) HEADER = TRUE",
"COPY INTO @s FROM (SELECT a, b FROM t) PARTITION BY (a) FILE_FORMAT = (TYPE = CSV) HEADER = TRUE",
"COPY INTO @s/out/ FROM t HEADER = TRUE SINGLE = TRUE MAX_FILE_SIZE = 16777216",
"COPY INTO @s FROM t OVERWRITE = TRUE SINGLE = FALSE INCLUDE_QUERY_ID = TRUE DETAILED_OUTPUT = TRUE",
];
const BROKEN: &[&str] = &[
"COPY",
"COPY INTO",
"COPY INTO t",
"COPY INTO t FROM",
"COPY INTO t FROM @",
"COPY INTO t FROM @s FILE_FORMAT =",
"COPY INTO t FROM @s FILE_FORMAT = (",
"COPY INTO FROM @s",
"COPY INTO @s FROM (SELECT",
];
#[test]
fn clean_copy_statements_parse_without_errors() {
for sql in CLEAN {
assert_parse_clean(sql);
}
}
#[test]
fn broken_copy_statements_round_trip_and_do_not_panic() {
for sql in BROKEN {
assert_parse_roundtrip(sql);
}
}
fn has(sql: &str, kind: SyntaxKind) -> bool {
parse(sql).syntax().descendants().any(|n| n.kind() == kind)
}
#[test]
fn produces_expected_nodes() {
let load = "COPY INTO t FROM @s/p FILE_FORMAT = (TYPE = CSV)";
assert!(has(load, SyntaxKind::COPY_STMT));
assert!(has(load, SyntaxKind::COPY_LOCATION));
assert!(has(load, SyntaxKind::COPY_OPTION));
let unload = "COPY INTO @s/out/ FROM (SELECT * FROM t) PARTITION BY (dt)";
assert!(has(unload, SyntaxKind::COPY_STMT));
assert!(has(unload, SyntaxKind::SUBQUERY));
assert!(has(unload, SyntaxKind::COPY_OPTION));
let transform = "COPY INTO t (a) FROM (SELECT $1 FROM @s)";
assert!(has(transform, SyntaxKind::SELECT_STMT));
assert!(has(transform, SyntaxKind::STAGE_REF));
}
#[test]
fn stage_locations_are_structured_inside_copy_locations() {
for sql in ["COPY INTO t FROM @s/p", "COPY INTO @s/out/ FROM t"] {
let parsed = assert_parse_clean(sql);
let has_stage_location = parsed
.syntax()
.descendants()
.filter(|node| node.kind() == SyntaxKind::COPY_LOCATION)
.any(|node| {
node.descendants()
.any(|child| child.kind() == SyntaxKind::STAGE_REF)
});
assert!(
has_stage_location,
"expected COPY_LOCATION to contain a STAGE_REF for {sql:?}"
);
}
}
#[test]
fn select_from_stage_parses_as_a_query() {
for sql in [
"SELECT * FROM @s",
"SELECT $1 FROM @s/p",
"SELECT $1 FROM @raw.stage/orders/ (FILE_FORMAT => ff)",
"SELECT METADATA$FILENAME, $1 FROM @~/staged/",
] {
let p = assert_parse_clean(sql);
assert!(
p.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::STAGE_REF),
"expected a STAGE_REF in {sql:?}"
);
}
}