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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user