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:
@@ -1468,6 +1468,12 @@ impl App {
|
||||
// no single table name to fall back on. A query
|
||||
// failure routes through `Operation::Query`.
|
||||
C::Select { .. } => (Operation::Query, None, None),
|
||||
// A SQL `INSERT` (ADR-0033) — route engine errors
|
||||
// (FK / UNIQUE / NOT NULL) through the insert operation
|
||||
// with the parsed target table.
|
||||
C::SqlInsert { target_table, .. } => {
|
||||
(Operation::Insert, Some(target_table.as_str()), None)
|
||||
}
|
||||
C::Replay { .. } => (Operation::Replay, None, None),
|
||||
// An `explain` failure (e.g. unknown table) is best
|
||||
// described by the wrapped query it failed to plan.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -292,6 +292,18 @@ pub enum Command {
|
||||
Select {
|
||||
sql: String,
|
||||
},
|
||||
/// Run a validated SQL `INSERT` (ADR-0033 §1, sub-phase 3b).
|
||||
/// Advanced mode only. Grammar-as-text (ADR-0030 §4): `sql` is
|
||||
/// the validated statement the worker executes verbatim;
|
||||
/// `target_table` is extracted from the parse so the worker can
|
||||
/// re-persist that table's CSV after a successful insert
|
||||
/// (ADR-0030 §11) without re-parsing the SQL. `listed_columns`
|
||||
/// (3d, `shortid` auto-fill) and `returning` (3g) are added by
|
||||
/// the sub-phases that read them.
|
||||
SqlInsert {
|
||||
sql: String,
|
||||
target_table: String,
|
||||
},
|
||||
/// App-lifecycle command (per ADR-0003). These work in both
|
||||
/// simple and advanced modes; the dispatcher branches on the
|
||||
/// `Command::App(...)` variant before mode-specific routing.
|
||||
@@ -587,6 +599,7 @@ impl Command {
|
||||
Self::Replay { .. } => "replay",
|
||||
Self::Explain { .. } => "explain",
|
||||
Self::Select { .. } => "select",
|
||||
Self::SqlInsert { .. } => "insert into",
|
||||
Self::App(app) => match app {
|
||||
AppCommand::Quit => "quit",
|
||||
AppCommand::Help => "help",
|
||||
@@ -654,6 +667,9 @@ impl Command {
|
||||
// result renders as a data view, not a structure
|
||||
// view, so an empty target is correct here.
|
||||
Self::Select { .. } => "",
|
||||
// A SQL `INSERT` carries its parsed target table (for
|
||||
// CSV re-persistence and ok-summary subject).
|
||||
Self::SqlInsert { target_table, .. } => target_table,
|
||||
// App commands aren't tied to schema entities — the
|
||||
// verb is the most identifying thing. The
|
||||
// display_subject override below provides a richer
|
||||
|
||||
+50
-1
@@ -20,7 +20,7 @@ use crate::dsl::command::{Command, Expr, RowFilter};
|
||||
use crate::dsl::grammar::{
|
||||
CommandNode, IdentSource, Node, NumberValidator, ValidationError, Word, expr,
|
||||
shared::{column_value_list, current_column_value},
|
||||
sql_select,
|
||||
sql_insert, sql_select,
|
||||
};
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::value::Value;
|
||||
@@ -855,6 +855,37 @@ fn build_select(_path: &MatchedPath, source: &str) -> Result<Command, Validation
|
||||
})
|
||||
}
|
||||
|
||||
/// Build `Command::SqlInsert` from a validated SQL `INSERT`
|
||||
/// (ADR-0033 §1, sub-phase 3b). Extracts the target table from
|
||||
/// the matched path so the worker re-persists the right CSV.
|
||||
///
|
||||
/// Dev-scaffold detail: the entry word is `sqlinsert` (not valid
|
||||
/// SQL), so the statement is reconstructed as `insert` + the
|
||||
/// matched tail. Sub-phase 3j wires the real `insert` entry word,
|
||||
/// at which point this collapses to `source.trim()` like
|
||||
/// `build_select`.
|
||||
fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, ValidationError> {
|
||||
let target_table = path
|
||||
.items
|
||||
.iter()
|
||||
.find_map(|item| match item.kind {
|
||||
MatchedKind::Ident {
|
||||
role: "insert_target_table",
|
||||
..
|
||||
} => Some(item.text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Everything after the entry word is the `INTO …` tail; prefix
|
||||
// the real `insert` keyword for the engine.
|
||||
let tail = path
|
||||
.items
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("insert {}", tail.trim());
|
||||
Ok(Command::SqlInsert { sql, target_table })
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// CommandNodes
|
||||
// =================================================================
|
||||
@@ -930,6 +961,24 @@ pub static WITH: CommandNode = CommandNode {
|
||||
help_id: None,
|
||||
usage_ids: &["parse.usage.select"],};
|
||||
|
||||
/// SQL `INSERT` development scaffold (ADR-0033 sub-phase 3b–3i).
|
||||
///
|
||||
/// Registered under the temporary entry word `sqlinsert` so the
|
||||
/// SQL INSERT grammar and execution path can be exercised in
|
||||
/// isolation, WITHOUT yet making `insert` a shared DSL/SQL entry
|
||||
/// word. Sharing `insert` is sub-phase 3j, which depends on
|
||||
/// `shortid` auto-fill (3d) so advanced-mode DSL inserts keep
|
||||
/// parity rather than regressing through an incomplete SQL path.
|
||||
/// This scaffold (entry word + reconstruction in `build_sql_insert`)
|
||||
/// is removed when 3j wires the real `insert` entry word.
|
||||
pub static SQL_INSERT: CommandNode = CommandNode {
|
||||
entry: Word::keyword("sqlinsert"),
|
||||
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
|
||||
ast_builder: build_sql_insert,
|
||||
help_id: None,
|
||||
usage_ids: &[],
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// Tests — `explain` grammar (ADR-0028 §1)
|
||||
// =================================================================
|
||||
|
||||
@@ -28,6 +28,7 @@ pub mod ddl;
|
||||
pub mod expr;
|
||||
pub mod shared;
|
||||
pub mod sql_expr;
|
||||
pub mod sql_insert;
|
||||
pub mod sql_select;
|
||||
|
||||
use crate::dsl::command::Command;
|
||||
@@ -570,6 +571,10 @@ pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
|
||||
(&data::EXPLAIN, CommandCategory::Simple),
|
||||
(&data::SELECT, CommandCategory::Advanced),
|
||||
(&data::WITH, CommandCategory::Advanced),
|
||||
// SQL INSERT development scaffold (sub-phase 3b–3i); the
|
||||
// temporary `sqlinsert` entry word keeps it isolated from the
|
||||
// DSL `insert` word until 3j wires the shared entry.
|
||||
(&data::SQL_INSERT, CommandCategory::Advanced),
|
||||
];
|
||||
|
||||
/// Whether `entry` names an advanced-mode-only command (ADR-0030
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//! SQL `INSERT` grammar (ADR-0033 §1, sub-phase 3b).
|
||||
//!
|
||||
//! Grammar-as-text (ADR-0030 §4): the walker validates that the
|
||||
//! `INSERT` is in the supported subset; the worker executes the
|
||||
//! validated SQL text and re-persists the target table's CSV
|
||||
//! (ADR-0030 §11). The shape here is the post-`INSERT` portion —
|
||||
//! the entry-word dispatch consumes the leading `INSERT` keyword
|
||||
//! before this shape walks (mirroring `sql_select::SQL_SELECT_TAIL`).
|
||||
//!
|
||||
//! Scope (3b): single- and multi-row `VALUES`, an optional
|
||||
//! `(column_name_list)`, and the `__rdbms_*` target rejection.
|
||||
//! `INSERT … SELECT` (3c), `shortid` auto-fill (3d), `RETURNING`
|
||||
//! (3g), and `ON CONFLICT … ` UPSERT (3h) land in later
|
||||
//! sub-phases.
|
||||
|
||||
use crate::dsl::grammar::sql_expr;
|
||||
use crate::dsl::grammar::sql_select::reject_internal_table;
|
||||
use crate::dsl::grammar::{IdentSource, Node, Word};
|
||||
|
||||
static COMMA: Node = Node::Punct(',');
|
||||
|
||||
/// The `INSERT` target table. `__rdbms_*` rejected (ADR-0030 §6 /
|
||||
/// ADR-0033 §1). `writes_table` populates `current_table` /
|
||||
/// `current_table_columns` so the optional column list and the
|
||||
/// `VALUES` expressions get column completion against the target.
|
||||
const TARGET_TABLE: Node = Node::Ident {
|
||||
source: IdentSource::Tables,
|
||||
role: "insert_target_table",
|
||||
validator: Some(reject_internal_table),
|
||||
highlight_override: None,
|
||||
writes_table: true,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
/// One column name inside the optional `(col1, col2, …)` list.
|
||||
///
|
||||
/// `writes_user_listed_column` stays `false` in 3b — the worker
|
||||
/// requires explicit values for every column, so the listed-column
|
||||
/// set isn't needed yet. Sub-phase 3d (`shortid` auto-fill) turns
|
||||
/// it on and threads `listed_columns` into `Command::SqlInsert`.
|
||||
static COLUMN_NAME: Node = Node::Ident {
|
||||
source: IdentSource::Columns,
|
||||
role: "insert_column",
|
||||
validator: None,
|
||||
highlight_override: None,
|
||||
writes_table: false,
|
||||
writes_column: false,
|
||||
writes_user_listed_column: false,
|
||||
writes_table_alias: false,
|
||||
writes_cte_name: false,
|
||||
writes_projection_alias: false,
|
||||
};
|
||||
|
||||
static COLUMN_LIST_NODES: &[Node] = &[
|
||||
Node::Punct('('),
|
||||
Node::Repeated {
|
||||
inner: &COLUMN_NAME,
|
||||
separator: Some(&COMMA),
|
||||
min: 1,
|
||||
},
|
||||
Node::Punct(')'),
|
||||
];
|
||||
const OPTIONAL_COLUMN_LIST: Node = Node::Optional(&Node::Seq(COLUMN_LIST_NODES));
|
||||
|
||||
/// One value expression inside a `VALUES` tuple. Consumes the
|
||||
/// shared `sql_expr` grammar (ADR-0031), so literals, operators,
|
||||
/// `CASE`, function calls, etc. are all admitted; the engine
|
||||
/// evaluates them at execution time.
|
||||
static VALUE_EXPR: Node = Node::Subgrammar(&sql_expr::SQL_OR_EXPR);
|
||||
|
||||
static VALUE_TUPLE_NODES: &[Node] = &[
|
||||
Node::Punct('('),
|
||||
Node::Repeated {
|
||||
inner: &VALUE_EXPR,
|
||||
separator: Some(&COMMA),
|
||||
min: 1,
|
||||
},
|
||||
Node::Punct(')'),
|
||||
];
|
||||
/// `'(' sql_expr (',' sql_expr)* ')'` — one row of values.
|
||||
static VALUE_TUPLE: Node = Node::Seq(VALUE_TUPLE_NODES);
|
||||
|
||||
static VALUES_CLAUSE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("values")),
|
||||
Node::Repeated {
|
||||
inner: &VALUE_TUPLE,
|
||||
separator: Some(&COMMA),
|
||||
min: 1,
|
||||
},
|
||||
];
|
||||
/// `VALUES tuple (',' tuple)*` — single- or multi-row.
|
||||
const VALUES_CLAUSE: Node = Node::Seq(VALUES_CLAUSE_NODES);
|
||||
|
||||
static SQL_INSERT_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("into")),
|
||||
TARGET_TABLE,
|
||||
OPTIONAL_COLUMN_LIST,
|
||||
VALUES_CLAUSE,
|
||||
Node::Optional(&Node::Punct(';')),
|
||||
];
|
||||
|
||||
/// The post-`INSERT` portion of a SQL `INSERT` statement
|
||||
/// (ADR-0033 §1): `INTO <table> [ '(' col_list ')' ] VALUES
|
||||
/// <tuple> (',' <tuple>)* [ ';' ]`.
|
||||
///
|
||||
/// The entry-word dispatch consumes the leading `INSERT` keyword
|
||||
/// before this shape walks, so a `CommandNode` references it as
|
||||
/// its `shape` (sub-phase 3b registers a development entry word;
|
||||
/// sub-phase 3j wires the shared `insert` entry word).
|
||||
pub static SQL_INSERT_SHAPE: Node = Node::Seq(SQL_INSERT_TAIL_NODES);
|
||||
|
||||
// =================================================================
|
||||
// Tests — grammar accept/reject for the post-`INSERT` tail.
|
||||
// =================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SQL_INSERT_SHAPE;
|
||||
use crate::dsl::walker::context::WalkContext;
|
||||
use crate::dsl::walker::driver::{NodeWalkResult, walk_node};
|
||||
use crate::dsl::walker::outcome::MatchedPath;
|
||||
|
||||
/// Walk `input` against the INSERT tail. Returns `true` only
|
||||
/// when the walk matches *and* consumes all of `input`
|
||||
/// (trailing whitespace allowed). Schemaless context: the
|
||||
/// shape is structural, so table/column idents match by shape
|
||||
/// and `reject_internal_table` still fires on `__rdbms_*`.
|
||||
fn walks(input: &str) -> bool {
|
||||
let mut ctx = WalkContext::new();
|
||||
let mut path = MatchedPath::new();
|
||||
let mut per_byte = Vec::new();
|
||||
match walk_node(input, 0, &SQL_INSERT_SHAPE, &mut ctx, &mut path, &mut per_byte) {
|
||||
NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn good(input: &str) {
|
||||
assert!(walks(input), "{input:?} should be a valid INSERT tail");
|
||||
}
|
||||
|
||||
fn bad(input: &str) {
|
||||
assert!(!walks(input), "{input:?} should NOT walk as a complete INSERT tail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_row_values() {
|
||||
good("into orders values (1, 2.0)");
|
||||
good("into orders values (1, 'text', true, null)");
|
||||
good("into orders values (1);");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_row_values() {
|
||||
good("into orders values (1, 'a'), (2, 'b')");
|
||||
good("into orders values (1), (2), (3)");
|
||||
good("into orders values (1, 'a'), (2, 'b');");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_column_list() {
|
||||
good("into orders (id, total) values (1, 2.0)");
|
||||
good("into orders (id) values (1)");
|
||||
good("into orders (a, b, c) values (1, 2, 3), (4, 5, 6)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_expressions_admit_sql_expr() {
|
||||
good("into t values (1 + 2)");
|
||||
good("into t values (case when 1 > 0 then 'y' else 'n' end)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_target_table_rejected() {
|
||||
bad("into __rdbms_playground_columns values (1)");
|
||||
bad("into __rdbms_playground_relationships (a) values (1)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structurally_incomplete_or_wrong_rejected() {
|
||||
// Missing VALUES.
|
||||
bad("into orders");
|
||||
bad("into orders (id, total)");
|
||||
// Empty value tuple — at least one expression required.
|
||||
bad("into orders values ()");
|
||||
// Missing INTO.
|
||||
bad("orders values (1)");
|
||||
// Trailing comma with no following tuple.
|
||||
bad("into orders values (1),");
|
||||
// Unclosed tuple.
|
||||
bad("into orders values (1, 2");
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,9 @@ use crate::dsl::walker::lex_helpers::{consume_ident, skip_whitespace};
|
||||
/// Reject internal `__rdbms_*` metadata tables in any
|
||||
/// table-source slot (ADR-0030 §6 reused by ADR-0032 §4 — extends
|
||||
/// to every Phase-2 table-source slot: `FROM`, `JOIN` targets,
|
||||
/// CTE name, and the `FROM` inside any CTE body).
|
||||
fn reject_internal_table(name: &str) -> Result<(), ValidationError> {
|
||||
/// CTE name, and the `FROM` inside any CTE body; ADR-0033 §1
|
||||
/// reuses it on the SQL `INSERT` target slot).
|
||||
pub(crate) fn reject_internal_table(name: &str) -> Result<(), ValidationError> {
|
||||
if name.to_ascii_lowercase().starts_with("__rdbms_") {
|
||||
Err(ValidationError {
|
||||
message_key: "select.internal_table",
|
||||
|
||||
@@ -1881,6 +1881,14 @@ async fn execute_command_typed(
|
||||
.run_select(sql, src)
|
||||
.await
|
||||
.map(CommandOutcome::Query),
|
||||
// A SQL `INSERT` (advanced mode; ADR-0033 §1). Grammar-as-
|
||||
// text: the worker runs the validated `sql` and re-persists
|
||||
// the parsed `target_table`'s CSV. Reuses the DSL insert
|
||||
// outcome (affected-row count + auto-show).
|
||||
Command::SqlInsert { sql, target_table } => database
|
||||
.run_sql_insert(sql, src, target_table)
|
||||
.await
|
||||
.map(CommandOutcome::Insert),
|
||||
// `EXPLAIN QUERY PLAN` never executes the wrapped
|
||||
// statement (ADR-0028 §2), so explaining a destructive
|
||||
// command is safe. `src` is unused here — explain is a
|
||||
|
||||
Reference in New Issue
Block a user