grammar+db: 3b — SQL INSERT grammar + minimal execution (ADR-0033 §1)

SQL_INSERT_SHAPE (INTO <table> [(cols)] VALUES tuple(s)) with __rdbms_*
target rejection; Command::SqlInsert{sql,target_table}; Request::RunSqlInsert
+ do_sql_insert worker (tx-guarded: execute, then finalize_persistence for
CSV + history before commit, so failures roll back and don't re-persist).
Auto-show is best-effort via last_insert_rowid range.

Isolated behind a dev `sqlinsert` entry word (Advanced) so the SQL path is
testable without making `insert` a shared word yet (that's 3j, after 3d
auto-fill parity). Command::SqlInsert carries only sql+target_table; the
plan's listed_columns/returning land in 3d/3g where they're read.

6 grammar accept/reject tests + 8 integration tests (single/multi-row,
column-list, full-arity, history, rollback-on-failure, multi-row atomicity,
parse-path reconstruction, internal-table rejection). 1452 baseline green.
This commit is contained in:
claude@clouddev1
2026-05-21 18:51:21 +00:00
parent 4e16d97fe0
commit c87363168f
10 changed files with 605 additions and 3 deletions
+100
View File
@@ -580,6 +580,19 @@ enum Request {
source: Option<String>,
reply: oneshot::Sender<Result<DataResult, DbError>>,
},
/// Run a validated SQL `INSERT` typed in advanced mode
/// (ADR-0033 §1, sub-phase 3b). The grammar walker has
/// validated `sql` is in the supported subset; the worker
/// executes it as text, re-persists the target table's CSV
/// (ADR-0030 §11), and appends the literal line to
/// `history.log`. `target_table` comes from the parse so the
/// worker re-persists the right CSV without re-parsing.
RunSqlInsert {
sql: String,
source: Option<String>,
target_table: String,
reply: oneshot::Sender<Result<InsertResult, DbError>>,
},
/// Capture the query plan for an explainable command via
/// `EXPLAIN QUERY PLAN` (ADR-0028 §2). `query` is the inner
/// `ShowData` / `Update` / `Delete`; `EXPLAIN QUERY PLAN`
@@ -1037,6 +1050,28 @@ impl Database {
recv.await.map_err(|_| DbError::WorkerGone)?
}
/// Run a validated SQL `INSERT` and return the affected-row
/// count plus the inserted rows (ADR-0033 §1, sub-phase 3b).
/// `sql` is the grammar-validated statement text; `source` is
/// the literal submitted line for `history.log`; `target_table`
/// is the parsed target whose CSV is re-persisted.
pub async fn run_sql_insert(
&self,
sql: String,
source: Option<String>,
target_table: String,
) -> Result<InsertResult, DbError> {
let (reply, recv) = oneshot::channel();
self.send(Request::RunSqlInsert {
sql,
source,
target_table,
reply,
})
.await?;
recv.await.map_err(|_| DbError::WorkerGone)?
}
/// Capture the query plan for an explainable command
/// (ADR-0028 §2). The wrapped command is not executed —
/// `EXPLAIN QUERY PLAN` only inspects how the engine would
@@ -1482,6 +1517,20 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
&sql,
));
}
Request::RunSqlInsert {
sql,
source,
target_table,
reply,
} => {
let _ = reply.send(do_sql_insert(
conn,
persistence,
source.as_deref(),
&sql,
&target_table,
));
}
Request::RebuildFromText {
project_path,
source,
@@ -5698,6 +5747,57 @@ fn do_run_select_request(
Ok(data)
}
/// Worker handler for `Request::RunSqlInsert` (ADR-0033 §1,
/// sub-phase 3b). Mirrors `do_insert`'s persistence discipline:
/// run the validated SQL inside a transaction, re-persist the
/// target table's CSV + append `history.log` via
/// `finalize_persistence` *before* `tx.commit()` (so a
/// persistence failure rolls the insert back), then commit.
///
/// Grammar-as-text (ADR-0030 §4): the values are literals in
/// `sql`, so no parameters are bound. FK / UNIQUE / NOT NULL
/// engine errors surface enriched via `execute_with_fk_enrichment`
/// + the friendly-error layer.
///
/// Auto-show is best-effort: the inserted rows are the last
/// `rows_affected` rowids ending at `last_insert_rowid()`. For the
/// common case (sequential / engine-assigned rowids) this is
/// exactly the inserted rows; an INSERT that sets explicit
/// non-contiguous rowid/INTEGER-PK values may surface a partial
/// view. `RETURNING` (sub-phase 3g) is the precise tool.
fn do_sql_insert(
conn: &Connection,
persistence: Option<&Persistence>,
source: Option<&str>,
sql: &str,
target_table: &str,
) -> Result<InsertResult, DbError> {
debug!(sql = %sql, table = %target_table, "sql_insert");
let tx = conn
.unchecked_transaction()
.map_err(DbError::from_rusqlite)?;
let rows_affected = execute_with_fk_enrichment(conn, target_table, sql, &[])?;
let last = conn.last_insert_rowid();
let rowids: Vec<i64> = if rows_affected == 0 {
Vec::new()
} else {
let n = rows_affected as i64;
((last - n + 1)..=last).collect()
};
let data = query_rows_by_rowid(conn, target_table, &rowids)?;
let changes = Changes {
schema_dirty: false,
rewritten_tables: vec![target_table.to_string()],
..Changes::default()
};
finalize_persistence(conn, persistence, source, &changes)?;
tx.commit().map_err(DbError::from_rusqlite)?;
Ok(InsertResult {
rows_affected,
data,
})
}
/// Execute a grammar-validated SQL `SELECT` and collect its
/// rows into a [`DataResult`] (ADR-0030 §6, ADR-0032 §12 +
/// Amendment 1).