style: format the whole tree with cargo fmt (stock defaults, #35)
One-time, mechanical reformat — no functional changes. The tree was not rustfmt-clean (~1800 hunks across ~100 files); this brings it to stock `cargo fmt` defaults so a `cargo fmt --check` CI gate can follow. Behaviour-preserving: 2509 pass / 0 fail / 1 ignored (unchanged baseline), clippy clean. A .git-blame-ignore-revs entry follows so `git blame` skips this commit.
This commit is contained in:
+422
-101
@@ -41,8 +41,7 @@ fn rt() -> tokio::runtime::Runtime {
|
||||
|
||||
fn open_project_db() -> (project::Project, Database, tempfile::TempDir) {
|
||||
let dir = tempfile::tempdir().expect("create tempdir");
|
||||
let project =
|
||||
project::open_or_create(None, Some(dir.path())).expect("open or create project");
|
||||
let project = project::open_or_create(None, Some(dir.path())).expect("open or create project");
|
||||
let persistence = Persistence::new(project.path().to_path_buf());
|
||||
let db = Database::open_with_persistence(project.db_path(), persistence)
|
||||
.expect("open db with persistence");
|
||||
@@ -86,7 +85,10 @@ fn single_row_insert_persists_and_counts() {
|
||||
.expect("insert runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row inserted");
|
||||
let csv = read_csv(&project, "T").expect("T.csv written after insert");
|
||||
assert!(csv.contains("Ada"), "CSV reflects the inserted row: {csv:?}");
|
||||
assert!(
|
||||
csv.contains("Ada"),
|
||||
"CSV reflects the inserted row: {csv:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -193,7 +195,10 @@ fn failed_multi_row_insert_is_atomic() {
|
||||
String::new(),
|
||||
false,
|
||||
));
|
||||
assert!(outcome.is_err(), "multi-row PK conflict must fail: {outcome:?}");
|
||||
assert!(
|
||||
outcome.is_err(),
|
||||
"multi-row PK conflict must fail: {outcome:?}"
|
||||
);
|
||||
let csv = read_csv(&project, "T").expect("T.csv still present");
|
||||
assert!(
|
||||
csv.contains("existing") && !csv.contains("fresh") && !csv.contains("collides"),
|
||||
@@ -208,7 +213,9 @@ fn parse_path_lowers_sqlinsert_scaffold_to_command() {
|
||||
let command = parse_command("insert into Orders (id, total) values (1, 99.5)")
|
||||
.expect("insert parses in advanced mode");
|
||||
match command {
|
||||
Command::SqlInsert { sql, target_table, .. } => {
|
||||
Command::SqlInsert {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(sql, "insert into Orders (id, total) values (1, 99.5)");
|
||||
assert_eq!(target_table, "Orders");
|
||||
}
|
||||
@@ -248,7 +255,9 @@ fn parse_path_lowers_insert_select_to_command() {
|
||||
let command = parse_command("insert into archive select * from source")
|
||||
.expect("INSERT … SELECT parses in advanced mode");
|
||||
match command {
|
||||
Command::SqlInsert { sql, target_table, .. } => {
|
||||
Command::SqlInsert {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(sql, "insert into archive select * from source");
|
||||
assert_eq!(target_table, "archive");
|
||||
}
|
||||
@@ -259,12 +268,13 @@ fn parse_path_lowers_insert_select_to_command() {
|
||||
#[test]
|
||||
fn parse_path_lowers_with_prefixed_insert_select() {
|
||||
// R4: a WITH-prefixed SELECT row source lowers verbatim.
|
||||
let command = parse_command(
|
||||
"insert into archive with t as (select * from orders) select * from t",
|
||||
)
|
||||
.expect("WITH-prefixed INSERT … SELECT parses");
|
||||
let command =
|
||||
parse_command("insert into archive with t as (select * from orders) select * from t")
|
||||
.expect("WITH-prefixed INSERT … SELECT parses");
|
||||
match command {
|
||||
Command::SqlInsert { sql, target_table, .. } => {
|
||||
Command::SqlInsert {
|
||||
sql, target_table, ..
|
||||
} => {
|
||||
assert_eq!(
|
||||
sql,
|
||||
"insert into archive with t as (select * from orders) select * from t",
|
||||
@@ -472,7 +482,13 @@ fn csv_rows(project: &project::Project, table: &str) -> Vec<Vec<String>> {
|
||||
fn values_autofills_omitted_shortid_pk() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x')")
|
||||
.expect("auto-fill insert runs");
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
@@ -486,25 +502,36 @@ fn values_autofills_omitted_shortid_pk() {
|
||||
fn values_multirow_autofills_distinct_shortids() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
let result = run_sqlinsert(
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (label) values ('a'), ('b'), ('c')",
|
||||
)
|
||||
.expect("multi-row auto-fill runs");
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('a'), ('b'), ('c')")
|
||||
.expect("multi-row auto-fill runs");
|
||||
assert_eq!(result.rows_affected, 3);
|
||||
let rows = csv_rows(&project, "t");
|
||||
let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect();
|
||||
assert_eq!(ids.len(), 3, "three DISTINCT non-empty shortids: {rows:?}");
|
||||
assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}");
|
||||
assert!(
|
||||
rows.iter().all(|r| !r[0].is_empty()),
|
||||
"no empty id: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_shortid_value_is_respected() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
// The user provided `id` explicitly — it must be honoured
|
||||
// verbatim (the override WARNING is sub-phase 3i).
|
||||
run_sqlinsert(
|
||||
@@ -521,10 +548,21 @@ fn explicit_shortid_value_is_respected() {
|
||||
fn insert_select_autofills_distinct_shortids() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "source", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(&db, &rt, "target", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
run_sqlinsert(&db, &rt, "insert into source (label) values ('a'), ('b')")
|
||||
.expect("seed source");
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"source",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"target",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into source (label) values ('a'), ('b')").expect("seed source");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
@@ -535,7 +573,10 @@ fn insert_select_autofills_distinct_shortids() {
|
||||
let rows = csv_rows(&project, "target");
|
||||
let ids: std::collections::HashSet<&String> = rows.iter().map(|r| &r[0]).collect();
|
||||
assert_eq!(ids.len(), 2, "two DISTINCT fresh shortids: {rows:?}");
|
||||
assert!(rows.iter().all(|r| !r[0].is_empty()), "no empty id: {rows:?}");
|
||||
assert!(
|
||||
rows.iter().all(|r| !r[0].is_empty()),
|
||||
"no empty id: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -547,11 +588,14 @@ fn combined_serial_and_shortid_autofill() {
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Serial), ("code", Type::ShortId), ("name", Type::Text)],
|
||||
&[
|
||||
("id", Type::Serial),
|
||||
("code", Type::ShortId),
|
||||
("name", Type::Text),
|
||||
],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (name) values ('x')")
|
||||
.expect("combined auto-fill runs");
|
||||
run_sqlinsert(&db, &rt, "insert into t (name) values ('x')").expect("combined auto-fill runs");
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert_eq!(rows.len(), 1, "{rows:?}");
|
||||
assert_eq!(rows[0][0], "1", "serial PK engine-filled: {rows:?}");
|
||||
@@ -566,7 +610,13 @@ fn shortid_autofill_respects_mixed_case_column_name() {
|
||||
// schema name `MyId`, not a lowercased form.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("MyId", Type::ShortId), ("label", Type::Text)], &["MyId"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("MyId", Type::ShortId), ("label", Type::Text)],
|
||||
&["MyId"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (label) values ('x')")
|
||||
.expect("mixed-case shortid auto-fill runs");
|
||||
let rows = csv_rows(&project, "t");
|
||||
@@ -584,7 +634,11 @@ fn two_shortids_pk_and_nonpk_both_autofill_distinctly() {
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)],
|
||||
&[
|
||||
("id", Type::ShortId),
|
||||
("code", Type::ShortId),
|
||||
("label", Type::Text),
|
||||
],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x'), ('y')")
|
||||
@@ -611,7 +665,11 @@ fn two_shortids_one_provided_one_autofilled() {
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("code", Type::ShortId), ("label", Type::Text)],
|
||||
&[
|
||||
("id", Type::ShortId),
|
||||
("code", Type::ShortId),
|
||||
("label", Type::Text),
|
||||
],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (id, label) values ('myid', 'x')")
|
||||
@@ -631,7 +689,11 @@ fn compound_pk_with_shortid_member_autofills() {
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("region", Type::Int), ("label", Type::Text)],
|
||||
&[
|
||||
("id", Type::ShortId),
|
||||
("region", Type::Int),
|
||||
("label", Type::Text),
|
||||
],
|
||||
&["id", "region"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (region, label) values (1, 'x')")
|
||||
@@ -653,14 +715,23 @@ fn autofill_does_not_mask_arity_mismatch() {
|
||||
// engine rather than mask the error.)
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let outcome = run_sqlinsert(&db, &rt, "insert into t (label) values ('a', 'b')");
|
||||
assert!(
|
||||
outcome.is_err(),
|
||||
"arity mismatch must be rejected, not masked: {outcome:?}",
|
||||
);
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert!(rows.is_empty(), "no row should land on a rejected insert: {rows:?}");
|
||||
assert!(
|
||||
rows.is_empty(),
|
||||
"no row should land on a rejected insert: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -672,14 +743,19 @@ fn sql_insert_autofills_omitted_nonpk_serial() {
|
||||
// shortid auto-fill, which already runs on this path.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("seq", Type::Serial)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("seq", Type::Serial)],
|
||||
&["id"],
|
||||
);
|
||||
|
||||
// Single row, omitting the non-PK serial `seq`.
|
||||
run_sqlinsert(&db, &rt, "insert into t (id) values (10)").expect("single-row insert runs");
|
||||
// Multi-row, omitting `seq` — each row gets a distinct, increasing
|
||||
// serial continuing from the current MAX.
|
||||
run_sqlinsert(&db, &rt, "insert into t (id) values (20), (30)")
|
||||
.expect("multi-row insert runs");
|
||||
run_sqlinsert(&db, &rt, "insert into t (id) values (20), (30)").expect("multi-row insert runs");
|
||||
|
||||
let rows = csv_rows(&project, "t");
|
||||
// No NULL serials, and the sequence is 1, 2, 3 across the three rows.
|
||||
@@ -700,11 +776,26 @@ fn autofill_insert_select_wider_projection_is_rejected() {
|
||||
// to the engine instead of dropping the extra projection.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "src", &[("a", Type::Text), ("b", Type::Text)], &["a"]);
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"src",
|
||||
&[("a", Type::Text), ("b", Type::Text)],
|
||||
&["a"],
|
||||
);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into src (a, b) values ('p', 'q')").expect("seed");
|
||||
let outcome = run_sqlinsert(&db, &rt, "insert into t (label) select a, b from src");
|
||||
assert!(outcome.is_err(), "wider projection must be rejected: {outcome:?}");
|
||||
assert!(
|
||||
outcome.is_err(),
|
||||
"wider projection must be rejected: {outcome:?}"
|
||||
);
|
||||
assert!(csv_rows(&project, "t").is_empty(), "nothing should land");
|
||||
}
|
||||
|
||||
@@ -724,7 +815,10 @@ fn autofill_insert_select_narrower_projection_is_rejected() {
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into src (a) values ('p')").expect("seed");
|
||||
let outcome = run_sqlinsert(&db, &rt, "insert into t (x, y) select a from src");
|
||||
assert!(outcome.is_err(), "narrower projection must be rejected: {outcome:?}");
|
||||
assert!(
|
||||
outcome.is_err(),
|
||||
"narrower projection must be rejected: {outcome:?}"
|
||||
);
|
||||
assert!(csv_rows(&project, "t").is_empty(), "nothing should land");
|
||||
}
|
||||
|
||||
@@ -736,11 +830,25 @@ fn autofill_insert_select_narrower_projection_is_rejected() {
|
||||
fn insert_returning_star_returns_inserted_row() {
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (id, b) values (1, 'Ada') returning *")
|
||||
.expect("INSERT … RETURNING * runs");
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("b", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (id, b) values (1, 'Ada') returning *",
|
||||
)
|
||||
.expect("INSERT … RETURNING * runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row inserted");
|
||||
assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the inserted row");
|
||||
assert_eq!(
|
||||
result.data.rows.len(),
|
||||
1,
|
||||
"RETURNING yielded the inserted row"
|
||||
);
|
||||
assert_eq!(result.data.columns, vec!["id".to_string(), "b".to_string()]);
|
||||
assert_eq!(result.data.rows[0][1], Some("Ada".to_string()));
|
||||
}
|
||||
@@ -749,7 +857,13 @@ fn insert_returning_star_returns_inserted_row() {
|
||||
fn insert_multirow_returning_id_yields_distinct_rows() {
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("b", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("b", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
@@ -760,7 +874,12 @@ fn insert_multirow_returning_id_yields_distinct_rows() {
|
||||
assert_eq!(result.data.columns, vec!["id".to_string()]);
|
||||
let ids: std::collections::BTreeSet<_> =
|
||||
result.data.rows.iter().map(|r| r[0].clone()).collect();
|
||||
assert_eq!(ids.len(), 3, "three distinct ids returned: {:?}", result.data.rows);
|
||||
assert_eq!(
|
||||
ids.len(),
|
||||
3,
|
||||
"three distinct ids returned: {:?}",
|
||||
result.data.rows
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -771,16 +890,33 @@ fn insert_returning_autofills_shortid_and_returns_it() {
|
||||
// surfaces in the returned row.
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (label) values ('x') returning *")
|
||||
.expect("auto-fill INSERT … RETURNING * runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row inserted (RETURNING-counted)");
|
||||
assert_eq!(
|
||||
result.rows_affected, 1,
|
||||
"one row inserted (RETURNING-counted)"
|
||||
);
|
||||
assert_eq!(result.data.rows.len(), 1, "RETURNING yielded the row");
|
||||
// `id` is the auto-filled shortid column; it must be non-empty in
|
||||
// the returned row (proving the rewrite kept RETURNING).
|
||||
let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column");
|
||||
let id_idx = result
|
||||
.data
|
||||
.columns
|
||||
.iter()
|
||||
.position(|c| c == "id")
|
||||
.expect("id column");
|
||||
let id_val = result.data.rows[0][id_idx].clone();
|
||||
assert!(id_val.is_some_and(|s| !s.is_empty()), "generated shortid surfaced via RETURNING");
|
||||
assert!(
|
||||
id_val.is_some_and(|s| !s.is_empty()),
|
||||
"generated shortid surfaced via RETURNING"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -790,11 +926,29 @@ fn insert_returning_recovers_bare_column_type() {
|
||||
// renders as the word, not 0/1).
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("active", Type::Bool)], &["id"]);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (id, active) values (1, true) returning active")
|
||||
.expect("INSERT … RETURNING active runs");
|
||||
assert_eq!(result.data.column_types, vec![Some(Type::Bool)], "bool type recovered");
|
||||
assert_eq!(result.data.rows[0][0], Some("true".to_string()), "rendered as the bool word");
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("active", Type::Bool)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (id, active) values (1, true) returning active",
|
||||
)
|
||||
.expect("INSERT … RETURNING active runs");
|
||||
assert_eq!(
|
||||
result.data.column_types,
|
||||
vec![Some(Type::Bool)],
|
||||
"bool type recovered"
|
||||
);
|
||||
assert_eq!(
|
||||
result.data.rows[0][0],
|
||||
Some("true".to_string()),
|
||||
"rendered as the bool word"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -803,11 +957,29 @@ fn insert_returning_computed_expression_is_typeless() {
|
||||
// so its recovered type is None (renders with neutral alignment).
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("n", Type::Int)], &["id"]);
|
||||
let result = run_sqlinsert(&db, &rt, "insert into t (id, n) values (1, 5) returning n + 1")
|
||||
.expect("INSERT … RETURNING <expr> runs");
|
||||
assert_eq!(result.data.column_types, vec![None], "computed projection is typeless");
|
||||
assert_eq!(result.data.rows[0][0], Some("6".to_string()), "engine evaluated n + 1");
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("n", Type::Int)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (id, n) values (1, 5) returning n + 1",
|
||||
)
|
||||
.expect("INSERT … RETURNING <expr> runs");
|
||||
assert_eq!(
|
||||
result.data.column_types,
|
||||
vec![None],
|
||||
"computed projection is typeless"
|
||||
);
|
||||
assert_eq!(
|
||||
result.data.rows[0][0],
|
||||
Some("6".to_string()),
|
||||
"engine evaluated n + 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -858,7 +1030,13 @@ fn multirow_autofill_returning_yields_distinct_generated_ids() {
|
||||
// rows each carrying its own generated id.
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
@@ -867,11 +1045,25 @@ fn multirow_autofill_returning_yields_distinct_generated_ids() {
|
||||
.expect("multi-row auto-fill INSERT … RETURNING * runs");
|
||||
assert_eq!(result.rows_affected, 3, "three rows inserted");
|
||||
assert_eq!(result.data.rows.len(), 3, "three rows returned");
|
||||
let id_idx = result.data.columns.iter().position(|c| c == "id").expect("id column");
|
||||
let id_idx = result
|
||||
.data
|
||||
.columns
|
||||
.iter()
|
||||
.position(|c| c == "id")
|
||||
.expect("id column");
|
||||
let ids: std::collections::BTreeSet<_> =
|
||||
result.data.rows.iter().map(|r| r[id_idx].clone()).collect();
|
||||
assert_eq!(ids.len(), 3, "three DISTINCT generated ids via RETURNING: {:?}", result.data.rows);
|
||||
assert!(ids.iter().all(|v| v.as_ref().is_some_and(|s| !s.is_empty())), "all ids non-empty");
|
||||
assert_eq!(
|
||||
ids.len(),
|
||||
3,
|
||||
"three DISTINCT generated ids via RETURNING: {:?}",
|
||||
result.data.rows
|
||||
);
|
||||
assert!(
|
||||
ids.iter()
|
||||
.all(|v| v.as_ref().is_some_and(|s| !s.is_empty())),
|
||||
"all ids non-empty"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -881,15 +1073,39 @@ fn insert_select_returning_executes_and_returns_rows() {
|
||||
// source feeds the insert, and RETURNING yields the inserted rows).
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "src", &[("id", Type::Int), ("b", Type::Text)], &["id"]);
|
||||
create_cols(&db, &rt, "dst", &[("id", Type::Int), ("b", Type::Text)], &["id"]);
|
||||
run_sqlinsert(&db, &rt, "insert into src (id, b) values (1, 'x'), (2, 'y')").expect("seed src");
|
||||
let result = run_sqlinsert(&db, &rt, "insert into dst select * from src returning id, b")
|
||||
.expect("INSERT … SELECT … RETURNING runs");
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"src",
|
||||
&[("id", Type::Int), ("b", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"dst",
|
||||
&[("id", Type::Int), ("b", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into src (id, b) values (1, 'x'), (2, 'y')",
|
||||
)
|
||||
.expect("seed src");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into dst select * from src returning id, b",
|
||||
)
|
||||
.expect("INSERT … SELECT … RETURNING runs");
|
||||
assert_eq!(result.rows_affected, 2, "two rows copied");
|
||||
assert_eq!(result.data.rows.len(), 2, "RETURNING yielded both inserted rows");
|
||||
let bs: std::collections::BTreeSet<_> =
|
||||
result.data.rows.iter().map(|r| r[1].clone()).collect();
|
||||
assert_eq!(
|
||||
result.data.rows.len(),
|
||||
2,
|
||||
"RETURNING yielded both inserted rows"
|
||||
);
|
||||
let bs: std::collections::BTreeSet<_> = result.data.rows.iter().map(|r| r[1].clone()).collect();
|
||||
assert!(bs.contains(&Some("x".to_string())) && bs.contains(&Some("y".to_string())));
|
||||
}
|
||||
|
||||
@@ -934,32 +1150,59 @@ fn autofill_upsert_real_conflict_preserves_clause_and_excluded() {
|
||||
"t".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id", Type::ShortId),
|
||||
ColumnSpec { unique: true, ..ColumnSpec::new("code", Type::Text) },
|
||||
ColumnSpec {
|
||||
unique: true,
|
||||
..ColumnSpec::new("code", Type::Text)
|
||||
},
|
||||
ColumnSpec::new("label", Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
))
|
||||
.expect("create table with shortid pk + unique code");
|
||||
run_sqlinsert(&db, &rt, "insert into t (code, label) values ('A', 'first')").expect("seed");
|
||||
run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (code, label) values ('A', 'first')",
|
||||
)
|
||||
.expect("seed");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
"insert into t (code, label) values ('A', 'second') on conflict (code) do update set label = excluded.label",
|
||||
)
|
||||
.expect("auto-filled UPSERT with a real conflict (clause preserved)");
|
||||
assert_eq!(result.rows_affected, 1, "the conflicting row was updated, not inserted");
|
||||
assert_eq!(
|
||||
result.rows_affected, 1,
|
||||
"the conflicting row was updated, not inserted"
|
||||
);
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert_eq!(rows.len(), 1, "still one row (DO UPDATE, not a second insert)");
|
||||
assert!(rows[0].iter().any(|c| c == "second"), "label updated via excluded: {rows:?}");
|
||||
assert!(!rows[0].iter().any(|c| c == "first"), "old label replaced: {rows:?}");
|
||||
assert_eq!(
|
||||
rows.len(),
|
||||
1,
|
||||
"still one row (DO UPDATE, not a second insert)"
|
||||
);
|
||||
assert!(
|
||||
rows[0].iter().any(|c| c == "second"),
|
||||
"label updated via excluded: {rows:?}"
|
||||
);
|
||||
assert!(
|
||||
!rows[0].iter().any(|c| c == "first"),
|
||||
"old label replaced: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_conflict_do_nothing_keeps_existing_row() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("name", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
@@ -970,14 +1213,23 @@ fn on_conflict_do_nothing_keeps_existing_row() {
|
||||
assert_eq!(result.rows_affected, 0, "conflicting row left untouched");
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert_eq!(rows.len(), 1, "still one row");
|
||||
assert!(rows[0].iter().any(|c| c == "orig"), "original value kept: {rows:?}");
|
||||
assert!(
|
||||
rows[0].iter().any(|c| c == "orig"),
|
||||
"original value kept: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_conflict_do_update_applies_excluded() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("name", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
@@ -988,14 +1240,23 @@ fn on_conflict_do_update_applies_excluded() {
|
||||
assert_eq!(result.rows_affected, 1, "the conflicting row was updated");
|
||||
let rows = csv_rows(&project, "t");
|
||||
assert_eq!(rows.len(), 1, "still one row (updated, not inserted)");
|
||||
assert!(rows[0].iter().any(|c| c == "new"), "row updated to excluded.name: {rows:?}");
|
||||
assert!(
|
||||
rows[0].iter().any(|c| c == "new"),
|
||||
"row updated to excluded.name: {rows:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_conflict_do_nothing_without_target() {
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("name", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::Int), ("name", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
run_sqlinsert(&db, &rt, "insert into t (id, name) values (1, 'orig')").expect("seed");
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
@@ -1003,7 +1264,10 @@ fn on_conflict_do_nothing_without_target() {
|
||||
"insert into t (id, name) values (1, 'x') on conflict do nothing",
|
||||
)
|
||||
.expect("ON CONFLICT (no target) DO NOTHING runs");
|
||||
assert_eq!(result.rows_affected, 0, "any-conflict do-nothing absorbed the duplicate");
|
||||
assert_eq!(
|
||||
result.rows_affected, 0,
|
||||
"any-conflict do-nothing absorbed the duplicate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1017,7 +1281,13 @@ fn autofill_preserves_on_conflict_clause() {
|
||||
// the rewrite doesn't prepare-fail and the clause survives.
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::ShortId), ("label", Type::Text)], &["id"]);
|
||||
create_cols(
|
||||
&db,
|
||||
&rt,
|
||||
"t",
|
||||
&[("id", Type::ShortId), ("label", Type::Text)],
|
||||
&["id"],
|
||||
);
|
||||
let result = run_sqlinsert(
|
||||
&db,
|
||||
&rt,
|
||||
@@ -1054,7 +1324,10 @@ fn sql_dml_validates_literal_values_like_the_dsl() {
|
||||
let dsl = r.block_on(db.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["id".to_string(), "d".to_string()]),
|
||||
vec![Value::Number("1".to_string()), Value::Text("2025/01/15".to_string())],
|
||||
vec![
|
||||
Value::Number("1".to_string()),
|
||||
Value::Text("2025/01/15".to_string()),
|
||||
],
|
||||
Some("insert".to_string()),
|
||||
));
|
||||
assert!(
|
||||
@@ -1187,20 +1460,34 @@ fn advanced_upsert_do_update_set_offers_typed_slot_hint() {
|
||||
// column-typed slot hint (boundary-aware lookahead → typed slot).
|
||||
let mut cache = SchemaCache::default();
|
||||
let cols = vec![
|
||||
TableColumn { name: "id".to_string(), user_type: Type::Int, not_null: false, has_default: false },
|
||||
TableColumn { name: "Name".to_string(), user_type: Type::Text, not_null: false, has_default: false },
|
||||
TableColumn {
|
||||
name: "id".to_string(),
|
||||
user_type: Type::Int,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
},
|
||||
TableColumn {
|
||||
name: "Name".to_string(),
|
||||
user_type: Type::Text,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
},
|
||||
];
|
||||
cache.tables.push("Customers".to_string());
|
||||
cache.columns.push("id".to_string());
|
||||
cache.columns.push("Name".to_string());
|
||||
cache.table_columns.insert("Customers".to_string(), cols);
|
||||
|
||||
let input = "insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name=";
|
||||
let input =
|
||||
"insert into Customers (id, Name) values (1, 'x') on conflict (id) do update set Name=";
|
||||
let hint = ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced);
|
||||
let Some(AmbientHint::Prose(prose)) = hint else {
|
||||
panic!("expected a Prose hint at the UPSERT SET value slot, got {hint:?}");
|
||||
};
|
||||
assert!(prose.contains("Name"), "hint names the column `Name`: {prose:?}");
|
||||
assert!(
|
||||
prose.contains("Name"),
|
||||
"hint names the column `Name`: {prose:?}"
|
||||
);
|
||||
assert!(
|
||||
prose.contains("quoted string"),
|
||||
"text-column hint says `quoted string`: {prose:?}"
|
||||
@@ -1251,8 +1538,14 @@ fn advanced_insert_form_a_value_offers_typed_slot_hint() {
|
||||
// user-listed column, so the hint is that column's typed prose.
|
||||
let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]);
|
||||
let prose = prose_at("insert into Things (note) values (", &schema);
|
||||
assert!(prose.contains("note"), "names listed column `note`: {prose:?}");
|
||||
assert!(prose.contains("quoted string"), "text-column prose: {prose:?}");
|
||||
assert!(
|
||||
prose.contains("note"),
|
||||
"names listed column `note`: {prose:?}"
|
||||
);
|
||||
assert!(
|
||||
prose.contains("quoted string"),
|
||||
"text-column prose: {prose:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1271,8 +1564,14 @@ fn advanced_insert_second_position_hints_second_column() {
|
||||
// hint is the SECOND column's typed prose.
|
||||
let schema = vschema(&[("Things", &[("k", Type::Int), ("note", Type::Text)])]);
|
||||
let prose = prose_at("insert into Things (k, note) values (5, ", &schema);
|
||||
assert!(prose.contains("note"), "second position names `note`: {prose:?}");
|
||||
assert!(prose.contains("quoted string"), "text-column prose: {prose:?}");
|
||||
assert!(
|
||||
prose.contains("note"),
|
||||
"second position names `note`: {prose:?}"
|
||||
);
|
||||
assert!(
|
||||
prose.contains("quoted string"),
|
||||
"text-column prose: {prose:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1283,13 +1582,19 @@ fn advanced_insert_value_int_mismatch_is_caught_live() {
|
||||
&schema,
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(!matches!(bad, InputState::Valid), "decimal into int rejected live: {bad:?}");
|
||||
assert!(
|
||||
!matches!(bad, InputState::Valid),
|
||||
"decimal into int rejected live: {bad:?}"
|
||||
);
|
||||
let ok = classify_input_with_schema_in_mode(
|
||||
"insert into Things (k) values (5)",
|
||||
&schema,
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(matches!(ok, InputState::Valid), "valid int literal parses: {ok:?}");
|
||||
assert!(
|
||||
matches!(ok, InputState::Valid),
|
||||
"valid int literal parses: {ok:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1303,7 +1608,10 @@ fn advanced_insert_string_into_int_is_caught_live() {
|
||||
&schema,
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(!matches!(bad, InputState::Valid), "string into int rejected live: {bad:?}");
|
||||
assert!(
|
||||
!matches!(bad, InputState::Valid),
|
||||
"string into int rejected live: {bad:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1314,7 +1622,10 @@ fn advanced_insert_multi_row_typed_and_mismatch_caught() {
|
||||
&schema,
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(matches!(ok, InputState::Valid), "well-formed multi-row parses: {ok:?}");
|
||||
assert!(
|
||||
matches!(ok, InputState::Valid),
|
||||
"well-formed multi-row parses: {ok:?}"
|
||||
);
|
||||
let bad = classify_input_with_schema_in_mode(
|
||||
"insert into Things (k, note) values (1, 'a'), (3.14, 'b')",
|
||||
&schema,
|
||||
@@ -1333,14 +1644,21 @@ fn advanced_insert_form_b_maps_all_columns_including_serial() {
|
||||
// takes an int literal (unlike the DSL, which omits auto-gen cols).
|
||||
let schema = vschema(&[(
|
||||
"Customers",
|
||||
&[("id", Type::Serial), ("Name", Type::Text), ("Email", Type::Text)],
|
||||
&[
|
||||
("id", Type::Serial),
|
||||
("Name", Type::Text),
|
||||
("Email", Type::Text),
|
||||
],
|
||||
)]);
|
||||
let state = classify_input_with_schema_in_mode(
|
||||
"insert into Customers values (1, 'Bob', 'b@c')",
|
||||
&schema,
|
||||
Mode::Advanced,
|
||||
);
|
||||
assert!(matches!(state, InputState::Valid), "Form B maps all 3 columns: {state:?}");
|
||||
assert!(
|
||||
matches!(state, InputState::Valid),
|
||||
"Form B maps all 3 columns: {state:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1356,6 +1674,9 @@ fn advanced_insert_value_expressions_still_parse_via_sql_expr() {
|
||||
"insert into Things (k) values ((select 1))",
|
||||
] {
|
||||
let state = classify_input_with_schema_in_mode(input, &schema, Mode::Advanced);
|
||||
assert!(matches!(state, InputState::Valid), "{input:?} must parse: {state:?}");
|
||||
assert!(
|
||||
matches!(state, InputState::Valid),
|
||||
"{input:?} must parse: {state:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user