Skip to main content

reddb_rql/
sql.rs

1use crate::ast::{
2    AlterMetricQuery, AlterQueueQuery, AlterTableQuery, AlterUserStmt, ApplyMigrationQuery,
3    AskQuery, BinOp, CompareOp, ConfigCommand, CopyFormat, CopyFromQuery, CreateCollectionQuery,
4    CreateForeignTableQuery, CreateIndexQuery, CreateMetricQuery, CreateMigrationQuery,
5    CreatePolicyQuery, CreateQueueQuery, CreateSchemaQuery, CreateSequenceQuery, CreateServerQuery,
6    CreateSloQuery, CreateTableQuery, CreateTimeSeriesQuery, CreateTreeQuery, CreateUserStmt,
7    CreateVcsRefQuery, CreateVectorQuery, CreateViewQuery, DeleteQuery, DropCollectionQuery,
8    DropDocumentQuery, DropForeignTableQuery, DropGraphQuery, DropIndexQuery, DropKvQuery,
9    DropPolicyQuery, DropQueueQuery, DropSchemaQuery, DropSequenceQuery, DropServerQuery,
10    DropTableQuery, DropTimeSeriesQuery, DropTreeQuery, DropVcsRefQuery, DropVectorQuery,
11    DropViewQuery, EventsBackfillQuery, ExplainAlterQuery, ExplainMigrationQuery, Expr, FieldRef,
12    Filter, ForeignColumnDef, GrantStmt, GraphCommand, GraphQuery, HybridQuery, InsertQuery,
13    JoinQuery, KvCommand, MaintenanceCommand, PathQuery, PolicyAction, ProbabilisticCommand,
14    QueryExpr, QueueCommand, QueueSelectQuery, RankOfQuery, RankRangeQuery,
15    RefreshMaterializedViewQuery, RevokeStmt, RollbackMigrationQuery, SearchCommand, Span,
16    TableQuery, TreeCommand, TruncateQuery, TxnControl, UpdateQuery, VcsCommand,
17    VcsConflictResolution, VcsRefKind, VcsResetMode, VectorQuery,
18};
19use crate::lexer::Token;
20use crate::parser::{ParseError, Parser, SafeTokenDisplay};
21use crate::sql_lowering::filter_to_expr;
22use reddb_types::catalog::CollectionModel;
23use reddb_types::types::Value;
24
25/// Canonical SQL frontend command surface.
26///
27/// This is the single entrypoint for SQL/RQL-style commands before they are
28/// lowered into the broader multi-backend `QueryExpr` space.
29#[derive(Debug, Clone)]
30pub enum SqlStatement {
31    Query(SqlQuery),
32    Mutation(SqlMutation),
33    Schema(SqlSchemaCommand),
34    Admin(SqlAdminCommand),
35}
36
37#[derive(Debug, Clone)]
38#[allow(clippy::large_enum_variant)]
39pub enum FrontendStatement {
40    Sql(SqlStatement),
41    Graph(GraphQuery),
42    GraphCommand(GraphCommand),
43    Path(PathQuery),
44    Vector(VectorQuery),
45    Hybrid(HybridQuery),
46    Search(SearchCommand),
47    Ask(AskQuery),
48    QueueSelect(QueueSelectQuery),
49    QueueCommand(QueueCommand),
50    EventsBackfill(EventsBackfillQuery),
51    EventsBackfillStatus { collection: String },
52    TreeCommand(TreeCommand),
53    ProbabilisticCommand(ProbabilisticCommand),
54    KvCommand(KvCommand),
55    ConfigCommand(ConfigCommand),
56    Ranking(QueryExpr),
57}
58
59#[derive(Debug, Clone)]
60pub enum SqlCommand {
61    Select(TableQuery),
62    Join(JoinQuery),
63    Insert(InsertQuery),
64    Update(UpdateQuery),
65    Delete(DeleteQuery),
66    ExplainAlter(ExplainAlterQuery),
67    CreateTable(CreateTableQuery),
68    CreateCollection(CreateCollectionQuery),
69    CreateVector(CreateVectorQuery),
70    DropTable(DropTableQuery),
71    DropGraph(DropGraphQuery),
72    DropVector(DropVectorQuery),
73    DropDocument(DropDocumentQuery),
74    DropKv(DropKvQuery),
75    DropCollection(DropCollectionQuery),
76    Truncate(TruncateQuery),
77    AlterTable(AlterTableQuery),
78    CreateVcsRef(CreateVcsRefQuery),
79    DropVcsRef(DropVcsRefQuery),
80    CreateIndex(CreateIndexQuery),
81    DropIndex(DropIndexQuery),
82    CreateTimeSeries(CreateTimeSeriesQuery),
83    CreateMetric(CreateMetricQuery),
84    AlterMetric(AlterMetricQuery),
85    CreateSlo(CreateSloQuery),
86    DropTimeSeries(DropTimeSeriesQuery),
87    CreateQueue(CreateQueueQuery),
88    AlterQueue(AlterQueueQuery),
89    DropQueue(DropQueueQuery),
90    CreateTree(CreateTreeQuery),
91    DropTree(DropTreeQuery),
92    Probabilistic(ProbabilisticCommand),
93    SetConfig {
94        key: String,
95        value: Value,
96    },
97    ShowConfig {
98        prefix: Option<String>,
99        as_json: bool,
100    },
101    SetSecret {
102        key: String,
103        value: Value,
104    },
105    DeleteSecret {
106        key: String,
107    },
108    ShowSecrets {
109        prefix: Option<String>,
110    },
111    SetKv {
112        key: String,
113        value: Value,
114    },
115    DeleteKv {
116        key: String,
117    },
118    SetTenant(Option<String>),
119    ShowTenant,
120    TransactionControl(TxnControl),
121    Maintenance(MaintenanceCommand),
122    Vcs(VcsCommand),
123    CreateSchema(CreateSchemaQuery),
124    DropSchema(DropSchemaQuery),
125    CreateSequence(CreateSequenceQuery),
126    DropSequence(DropSequenceQuery),
127    CopyFrom(CopyFromQuery),
128    CreateView(CreateViewQuery),
129    DropView(DropViewQuery),
130    RefreshMaterializedView(RefreshMaterializedViewQuery),
131    CreatePolicy(CreatePolicyQuery),
132    DropPolicy(DropPolicyQuery),
133    CreateServer(CreateServerQuery),
134    DropServer(DropServerQuery),
135    CreateForeignTable(CreateForeignTableQuery),
136    DropForeignTable(DropForeignTableQuery),
137    /// `GRANT … ON … TO …`
138    Grant(GrantStmt),
139    /// `REVOKE … ON … FROM …`
140    Revoke(RevokeStmt),
141    /// `ALTER USER name <attrs>`
142    AlterUser(AlterUserStmt),
143    /// `CREATE USER name PASSWORD '...' [ROLE read|write|admin]`
144    CreateUser(CreateUserStmt),
145    /// IAM policy DDL (CREATE POLICY '...' AS '...', DROP POLICY '...',
146    /// ATTACH/DETACH POLICY, SHOW POLICIES, SIMULATE, SHOW EFFECTIVE
147    /// PERMISSIONS). Stored as a pre-built QueryExpr so the dispatcher
148    /// can route the multitude of shapes through a single arm.
149    IamPolicy(QueryExpr),
150    CreateMigration(CreateMigrationQuery),
151    ApplyMigration(ApplyMigrationQuery),
152    RollbackMigration(RollbackMigrationQuery),
153    ExplainMigration(ExplainMigrationQuery),
154}
155
156/// Issue #789 — Analytics v0 non-goal map for `CREATE …` forms.
157///
158/// PRD #782 ringfences Analytics v0 around a metric-centric catalog and
159/// explicitly excludes generic analytics objects, a separate event
160/// storage model, cohorts, funnels, SLA contracts, and adapter surfaces.
161/// When the parser sees one of these idents in the `CREATE` head, return
162/// a stable v0-scoped rejection message; otherwise return `None` and let
163/// the regular CREATE fallback handle the token.
164fn analytics_v0_non_goal_create(token: &Token) -> Option<String> {
165    let ident = match token {
166        Token::Ident(s) => s,
167        _ => return None,
168    };
169    let upper = ident.to_ascii_uppercase();
170    let message = match upper.as_str() {
171        "ANALYTICS" => {
172            "CREATE ANALYTICS is not supported in Analytics v0 — \
173             use CREATE METRIC <dotted.path> for the metric-centric \
174             catalog (PRD #782 non-goal)"
175        }
176        "EVENT" => {
177            "CREATE EVENT is not supported in Analytics v0 — \
178             event-shaped data lives in ordinary TABLE/DOCUMENT \
179             collections, not a new storage model (PRD #782 non-goal)"
180        }
181        "COHORT" => {
182            "CREATE COHORT is not supported in Analytics v0 — \
183             cohort surfaces are deferred (PRD #782 non-goal)"
184        }
185        "FUNNEL" => {
186            "CREATE FUNNEL is not supported in Analytics v0 — \
187             funnel surfaces are deferred (PRD #782 non-goal)"
188        }
189        "SLA" => {
190            "CREATE SLA is not supported in Analytics v0 — \
191             SLA/legal/commercial contract modeling is post-MVP \
192             (PRD #782 non-goal)"
193        }
194        "ADAPTER" => {
195            "CREATE ADAPTER is not supported in Analytics v0 — \
196             Prometheus/Grafana/Snowplow/Google Analytics adapters \
197             are deferred (PRD #782 non-goal)"
198        }
199        _ => return None,
200    };
201    Some(message.to_string())
202}
203
204fn collection_model_filter(model: &str) -> Filter {
205    Filter::Compare {
206        field: FieldRef::column("", "model"),
207        op: CompareOp::Eq,
208        value: Value::Text(model.to_string().into()),
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use reddb_types::catalog::CollectionModel;
216
217    fn frontend(input: &str) -> FrontendStatement {
218        parse_frontend(input)
219            .unwrap_or_else(|err| panic!("failed to parse frontend {input:?}: {err:?}"))
220    }
221
222    fn expr(input: &str) -> QueryExpr {
223        frontend(input).into_query_expr()
224    }
225
226    fn sql_command(input: &str) -> SqlCommand {
227        sql_command_result(input)
228            .unwrap_or_else(|err| panic!("failed to parse SQL command {input:?}: {err:?}"))
229    }
230
231    fn sql_command_result(input: &str) -> Result<SqlCommand, ParseError> {
232        let mut parser = Parser::new(input)?;
233        parser.parse_sql_command()
234    }
235
236    fn assert_text(value: &Value, expected: &str) {
237        match value {
238            Value::Text(text) => assert_eq!(text.as_ref(), expected),
239            other => panic!("expected text {expected:?}, got {other:?}"),
240        }
241    }
242
243    #[test]
244    fn parse_frontend_routes_core_sql_statements() {
245        let FrontendStatement::Sql(SqlStatement::Query(SqlQuery::Select(query))) =
246            frontend("SELECT * FROM users")
247        else {
248            panic!("SELECT should route to SqlStatement::Query::Select");
249        };
250        assert_eq!(query.table, "users");
251
252        let QueryExpr::Insert(query) = expr("INSERT INTO users (id, name) VALUES (1, 'ada')")
253        else {
254            panic!("INSERT should lower through the SQL frontend");
255        };
256        assert_eq!(query.table, "users");
257        assert_eq!(query.columns, vec!["id", "name"]);
258        assert_eq!(query.values.len(), 1);
259
260        let QueryExpr::Update(query) = expr("UPDATE users SET name = 'ada' WHERE id = 1") else {
261            panic!("UPDATE should lower through the SQL frontend");
262        };
263        assert_eq!(query.table, "users");
264        assert_eq!(query.assignments[0].0, "name");
265
266        let QueryExpr::Delete(query) = expr("DELETE FROM users WHERE id = 1") else {
267            panic!("DELETE should lower through the SQL frontend");
268        };
269        assert_eq!(query.table, "users");
270        assert!(query.filter.is_some());
271
272        let QueryExpr::CreateTable(query) = expr("CREATE TABLE users (id INT, name TEXT)") else {
273            panic!("CREATE TABLE should lower through the SQL frontend");
274        };
275        assert_eq!(query.collection_model, CollectionModel::Table);
276        assert_eq!(query.name, "users");
277        assert_eq!(query.columns[0].name, "id");
278
279        let QueryExpr::DropTable(query) = expr("DROP TABLE IF EXISTS users") else {
280            panic!("DROP TABLE should lower through the SQL frontend");
281        };
282        assert_eq!(query.name, "users");
283        assert!(query.if_exists);
284    }
285
286    #[test]
287    fn parse_frontend_routes_admin_and_catalog_sql() {
288        let QueryExpr::Table(query) = expr("SHOW COLLECTIONS") else {
289            panic!("SHOW COLLECTIONS should become a red.collections table query");
290        };
291        assert_eq!(query.table, "red.collections");
292        assert!(query.filter.is_some());
293
294        let QueryExpr::Table(query) = expr("SHOW TABLES LIMIT 5") else {
295            panic!("SHOW TABLES should become a filtered red.collections table query");
296        };
297        assert_eq!(query.table, "red.collections");
298        assert_eq!(query.limit, Some(5));
299        assert!(query.filter.is_some());
300
301        assert!(matches!(
302            expr("SHOW CONFIG durability.mode"),
303            QueryExpr::ShowConfig { prefix: Some(prefix), as_json: false } if prefix == "durability.mode"
304        ));
305        assert!(matches!(
306            expr("SHOW CONFIG"),
307            QueryExpr::ShowConfig {
308                prefix: None,
309                as_json: false
310            }
311        ));
312        assert!(matches!(
313            expr("SHOW CONFIG runtime.result_cache AS JSON"),
314            QueryExpr::ShowConfig { prefix: Some(prefix), as_json: true } if prefix == "runtime.result_cache"
315        ));
316        assert!(matches!(
317            expr("SHOW CONFIG FORMAT JSON"),
318            QueryExpr::ShowConfig {
319                prefix: None,
320                as_json: true
321            }
322        ));
323
324        let QueryExpr::SetConfig { key, value } = expr("SET CONFIG durability.mode = 'sync'")
325        else {
326            panic!("SET CONFIG should stay on the SQL admin surface");
327        };
328        assert_eq!(key, "durability.mode");
329        assert_text(&value, "sync");
330
331        let QueryExpr::SetSecret { key, value } = expr("SET SECRET provider.api_key = 'sk_test'")
332        else {
333            panic!("SET SECRET should stay on the SQL admin surface");
334        };
335        assert_eq!(key, "provider.api_key");
336        assert_text(&value, "sk_test");
337        assert!(matches!(
338            expr("SET SECRET red.secrets.provider.api_key = 'sk_test'"),
339            QueryExpr::SetSecret { key, .. } if key == "red.secret.provider.api_key"
340        ));
341
342        assert!(matches!(
343            expr("DELETE SECRET provider.api_key"),
344            QueryExpr::DeleteSecret { key } if key == "provider.api_key"
345        ));
346        assert!(matches!(
347            expr("DELETE SECRET red.secrets.provider.api_key"),
348            QueryExpr::DeleteSecret { key } if key == "red.secret.provider.api_key"
349        ));
350        assert!(matches!(
351            expr("SHOW SECRETS provider"),
352            QueryExpr::ShowSecrets { prefix: Some(prefix) } if prefix == "provider"
353        ));
354        assert!(matches!(
355            expr("SHOW SECRETS red.secrets.provider"),
356            QueryExpr::ShowSecrets { prefix: Some(prefix) } if prefix == "red.secret.provider"
357        ));
358        assert!(matches!(
359            expr("SET TENANT 'acme'"),
360            QueryExpr::SetTenant(Some(tenant)) if tenant == "acme"
361        ));
362        assert!(matches!(expr("RESET TENANT"), QueryExpr::SetTenant(None)));
363        assert!(matches!(expr("SHOW TENANT"), QueryExpr::ShowTenant));
364        assert!(matches!(
365            expr("BEGIN ISOLATION LEVEL SNAPSHOT"),
366            QueryExpr::TransactionControl(TxnControl::Begin)
367        ));
368        assert!(matches!(
369            expr("ROLLBACK TO SAVEPOINT sp1"),
370            QueryExpr::TransactionControl(TxnControl::RollbackToSavepoint(name)) if name == "sp1"
371        ));
372        assert!(matches!(
373            expr("VACUUM FULL users"),
374            QueryExpr::MaintenanceCommand(MaintenanceCommand::Vacuum {
375                target: Some(target),
376                full: true,
377            }) if target == "users"
378        ));
379    }
380
381    #[test]
382    fn parse_frontend_routes_extended_schema_sql() {
383        assert!(matches!(
384            expr("CREATE SCHEMA IF NOT EXISTS app"),
385            QueryExpr::CreateSchema(CreateSchemaQuery {
386                name,
387                if_not_exists: true,
388            }) if name == "app"
389        ));
390        assert!(matches!(
391            expr("DROP SCHEMA IF EXISTS app CASCADE"),
392            QueryExpr::DropSchema(DropSchemaQuery {
393                name,
394                if_exists: true,
395                cascade: true,
396            }) if name == "app"
397        ));
398        assert!(matches!(
399            expr("CREATE SEQUENCE IF NOT EXISTS seq START WITH 10 INCREMENT BY 2"),
400            QueryExpr::CreateSequence(CreateSequenceQuery {
401                name,
402                if_not_exists: true,
403                start: 10,
404                increment: 2,
405            }) if name == "seq"
406        ));
407        assert!(matches!(
408            expr("DROP SEQUENCE IF EXISTS seq"),
409            QueryExpr::DropSequence(DropSequenceQuery {
410                name,
411                if_exists: true,
412            }) if name == "seq"
413        ));
414
415        let QueryExpr::CopyFrom(copy) = expr(
416            "COPY users FROM '/tmp/u.csv' WITH (FORMAT = csv, HEADER = true, DELIMITER = ';')",
417        ) else {
418            panic!("COPY should lower through SQL frontend");
419        };
420        assert_eq!(copy.table, "users");
421        assert_eq!(copy.path, "/tmp/u.csv");
422        assert_eq!(copy.format, CopyFormat::Csv);
423        assert_eq!(copy.delimiter, Some(';'));
424        assert!(copy.has_header);
425
426        let QueryExpr::CreateView(view) = expr(
427            "CREATE MATERIALIZED VIEW IF NOT EXISTS mv WITH RETENTION 1 h \
428             AS SELECT id FROM users REFRESH EVERY 5 s",
429        ) else {
430            panic!("CREATE MATERIALIZED VIEW should lower through SQL frontend");
431        };
432        assert_eq!(view.name, "mv");
433        assert!(view.materialized);
434        assert!(view.if_not_exists);
435        assert_eq!(view.retention_duration_ms, Some(3_600_000));
436        assert_eq!(view.refresh_every_ms, Some(5_000));
437        assert!(matches!(*view.query, QueryExpr::Table(_)));
438
439        assert!(matches!(
440            expr("DROP MATERIALIZED VIEW IF EXISTS mv"),
441            QueryExpr::DropView(DropViewQuery {
442                name,
443                materialized: true,
444                if_exists: true,
445            }) if name == "mv"
446        ));
447        assert!(matches!(
448            expr("REFRESH MATERIALIZED VIEW mv"),
449            QueryExpr::RefreshMaterializedView(RefreshMaterializedViewQuery { name }) if name == "mv"
450        ));
451    }
452
453    #[test]
454    fn parse_frontend_routes_fdw_policy_auth_and_migrations() {
455        let QueryExpr::CreateServer(server) = expr(
456            "CREATE SERVER IF NOT EXISTS csvsrv FOREIGN DATA WRAPPER csv OPTIONS (path '/data')",
457        ) else {
458            panic!("CREATE SERVER should lower through SQL frontend");
459        };
460        assert_eq!(server.name, "csvsrv");
461        assert_eq!(server.wrapper, "csv");
462        assert!(server.if_not_exists);
463        assert_eq!(
464            server.options,
465            vec![("path".to_string(), "/data".to_string())]
466        );
467
468        let QueryExpr::CreateForeignTable(table) = expr(
469            "CREATE FOREIGN TABLE IF NOT EXISTS ext_users \
470             (id INT, name TEXT) SERVER csvsrv OPTIONS (file 'users.csv')",
471        ) else {
472            panic!("CREATE FOREIGN TABLE should lower through SQL frontend");
473        };
474        assert_eq!(table.name, "ext_users");
475        assert_eq!(table.server, "csvsrv");
476        assert!(table.if_not_exists);
477        assert_eq!(table.columns.len(), 2);
478        assert!(!table.columns[0].not_null);
479
480        assert!(matches!(
481            expr("DROP SERVER IF EXISTS csvsrv CASCADE"),
482            QueryExpr::DropServer(DropServerQuery {
483                name,
484                if_exists: true,
485                cascade: true,
486            }) if name == "csvsrv"
487        ));
488        assert!(matches!(
489            expr("DROP FOREIGN TABLE IF EXISTS ext_users"),
490            QueryExpr::DropForeignTable(DropForeignTableQuery {
491                name,
492                if_exists: true,
493            }) if name == "ext_users"
494        ));
495
496        let QueryExpr::CreatePolicy(policy) = expr(
497            "CREATE POLICY readonly ON NODES OF mygraph FOR SELECT TO analytics USING (public = 1)",
498        ) else {
499            panic!("CREATE POLICY should lower through SQL frontend");
500        };
501        assert_eq!(policy.name, "readonly");
502        assert_eq!(policy.table, "mygraph");
503        assert_eq!(policy.action, Some(PolicyAction::Select));
504        assert_eq!(policy.role.as_deref(), Some("analytics"));
505        assert_eq!(policy.target_kind.as_ident(), "nodes");
506
507        assert!(matches!(
508            expr("DROP POLICY IF EXISTS readonly ON mygraph"),
509            QueryExpr::DropPolicy(DropPolicyQuery {
510                name,
511                table,
512                if_exists: true,
513            }) if name == "readonly" && table == "mygraph"
514        ));
515
516        assert!(matches!(
517            expr("GRANT SELECT ON TABLE public.users TO tenant1.alice"),
518            QueryExpr::Grant(grant)
519                if grant.actions == vec!["SELECT"]
520                    && grant.objects[0].schema.as_deref() == Some("public")
521        ));
522        assert!(matches!(
523            expr("REVOKE GRANT OPTION FOR USAGE ON SCHEMA analytics FROM GROUP analysts"),
524            QueryExpr::Revoke(revoke) if revoke.grant_option_for && revoke.all == false
525        ));
526        assert!(matches!(
527            expr("ALTER USER bob ENABLE SET search_path TO 'public'"),
528            QueryExpr::AlterUser(user)
529                if user.username == "bob" && user.attributes.len() == 2
530        ));
531        assert!(matches!(
532            expr("CREATE USER tenant1.alice WITH PASSWORD 'pw' ROLE write"),
533            QueryExpr::CreateUser(user)
534                if user.tenant.as_deref() == Some("tenant1")
535                    && user.username == "alice"
536                    && user.password == "pw"
537                    && user.role == "write"
538        ));
539
540        assert!(matches!(
541            expr("CREATE POLICY 'readonly' AS '{\"Statement\":[]}'"),
542            QueryExpr::CreateIamPolicy { id, json }
543                if id == "readonly" && json == "{\"Statement\":[]}"
544        ));
545        assert!(matches!(
546            expr("DROP POLICY 'readonly'"),
547            QueryExpr::DropIamPolicy { id } if id == "readonly"
548        ));
549
550        assert!(matches!(
551            expr("CREATE MIGRATION m2 DEPENDS ON m0 BATCH 10 ROWS AS CREATE TABLE accounts (id INT)"),
552            QueryExpr::CreateMigration(migration)
553                if migration.name == "m2"
554                    && migration.depends_on == vec!["m0".to_string()]
555                    && migration.batch_size == Some(10)
556        ));
557        assert!(matches!(
558            expr("APPLY MIGRATION * FOR TENANT tenant1"),
559            QueryExpr::ApplyMigration(apply)
560                if apply.for_tenant.as_deref() == Some("tenant1")
561        ));
562        assert!(matches!(
563            expr("ROLLBACK MIGRATION m2"),
564            QueryExpr::RollbackMigration(RollbackMigrationQuery { name }) if name == "m2"
565        ));
566        assert!(matches!(
567            expr("EXPLAIN MIGRATION m2"),
568            QueryExpr::ExplainMigration(ExplainMigrationQuery { name }) if name == "m2"
569        ));
570    }
571
572    #[test]
573    fn parse_sql_statement_covers_statement_category_wrapping() {
574        enum Expected {
575            Select,
576            Insert,
577            CreateSchema,
578            SetTenant,
579        }
580
581        let cases = [
582            ("SELECT * FROM users", Expected::Select),
583            ("INSERT INTO users (id) VALUES (1)", Expected::Insert),
584            ("CREATE SCHEMA app", Expected::CreateSchema),
585            ("SET TENANT 'acme'", Expected::SetTenant),
586        ];
587
588        for (input, expected) in cases {
589            let mut parser = Parser::new(input).expect("lexer");
590            let statement = parser
591                .parse_sql_statement()
592                .unwrap_or_else(|err| panic!("failed to parse {input:?}: {err:?}"));
593            let matched = match expected {
594                Expected::Select => matches!(statement, SqlStatement::Query(SqlQuery::Select(_))),
595                Expected::Insert => {
596                    matches!(statement, SqlStatement::Mutation(SqlMutation::Insert(_)))
597                }
598                Expected::CreateSchema => matches!(
599                    statement,
600                    SqlStatement::Schema(SqlSchemaCommand::CreateSchema(_))
601                ),
602                Expected::SetTenant => {
603                    matches!(
604                        statement,
605                        SqlStatement::Admin(SqlAdminCommand::SetTenant(_))
606                    )
607                }
608            };
609            assert!(matched, "{input}");
610        }
611    }
612
613    #[test]
614    fn parse_frontend_routes_non_sql_frontends() {
615        let QueryExpr::KvCommand(KvCommand::Get {
616            model,
617            collection,
618            key,
619        }) = expr("KV GET settings.feature")
620        else {
621            panic!("KV GET should route to FrontendStatement::KvCommand");
622        };
623        assert_eq!(model, CollectionModel::Kv);
624        assert_eq!(collection, "settings");
625        assert_eq!(key, "feature");
626
627        let QueryExpr::ConfigCommand(ConfigCommand::Watch {
628            collection,
629            key,
630            prefix,
631            from_lsn,
632        }) = expr("WATCH CONFIG app PREFIX feature FROM LSN 7")
633        else {
634            panic!("WATCH CONFIG should route to FrontendStatement::ConfigCommand");
635        };
636        assert_eq!(collection, "app");
637        assert_eq!(key, "feature");
638        assert!(prefix);
639        assert_eq!(from_lsn, Some(7));
640
641        let QueryExpr::ConfigCommand(ConfigCommand::List {
642            collection,
643            prefix,
644            limit,
645            offset,
646        }) = expr("LIST CONFIG app PREFIX feature LIMIT 3 OFFSET 1")
647        else {
648            panic!("LIST CONFIG should route to FrontendStatement::ConfigCommand");
649        };
650        assert_eq!(collection, "app");
651        assert_eq!(prefix.as_deref(), Some("feature"));
652        assert_eq!(limit, Some(3));
653        assert_eq!(offset, 1);
654
655        let QueryExpr::KvCommand(KvCommand::List {
656            model,
657            collection,
658            prefix,
659            limit,
660            offset,
661            as_json,
662        }) = expr("KV LIST settings PREFIX 'feature.' LIMIT 10 OFFSET 2")
663        else {
664            panic!("KV LIST should route to FrontendStatement::KvCommand");
665        };
666        assert_eq!(model, CollectionModel::Kv);
667        assert_eq!(collection, "settings");
668        assert_eq!(prefix.as_deref(), Some("feature."));
669        assert_eq!(limit, Some(10));
670        assert_eq!(offset, 2);
671        assert!(!as_json);
672
673        let QueryExpr::KvCommand(KvCommand::List {
674            model,
675            collection,
676            prefix,
677            limit,
678            offset,
679            as_json,
680        }) = expr("LIST KV settings PREFIX feature LIMIT 10 OFFSET 2")
681        else {
682            panic!("LIST KV should route to FrontendStatement::KvCommand");
683        };
684        assert_eq!(model, CollectionModel::Kv);
685        assert_eq!(collection, "settings");
686        assert_eq!(prefix.as_deref(), Some("feature"));
687        assert_eq!(limit, Some(10));
688        assert_eq!(offset, 2);
689        assert!(!as_json);
690
691        let QueryExpr::KvCommand(KvCommand::List {
692            model,
693            collection,
694            prefix,
695            as_json,
696            ..
697        }) = expr("KV LIST settings PREFIX feature AS JSON")
698        else {
699            panic!("KV LIST AS JSON should route to FrontendStatement::KvCommand");
700        };
701        assert_eq!(model, CollectionModel::Kv);
702        assert_eq!(collection, "settings");
703        assert_eq!(prefix.as_deref(), Some("feature"));
704        assert!(as_json);
705
706        let QueryExpr::KvCommand(KvCommand::Watch {
707            model,
708            collection,
709            key,
710            prefix,
711            from_lsn,
712        }) = expr("WATCH sessions.user.* FROM LSN 3")
713        else {
714            panic!("bare WATCH should route to FrontendStatement::KvCommand");
715        };
716        assert_eq!(model, CollectionModel::Kv);
717        assert_eq!(collection, "sessions");
718        assert_eq!(key, "user");
719        assert!(prefix);
720        assert_eq!(from_lsn, Some(3));
721
722        let QueryExpr::KvCommand(KvCommand::Watch {
723            model,
724            collection,
725            key,
726            prefix,
727            from_lsn,
728        }) = expr("WATCH VAULT secrets PREFIX api FROM LSN 7")
729        else {
730            panic!("WATCH VAULT should route to FrontendStatement::KvCommand");
731        };
732        assert_eq!(model, CollectionModel::Vault);
733        assert_eq!(collection, "secrets");
734        assert_eq!(key, "api");
735        assert!(prefix);
736        assert_eq!(from_lsn, Some(7));
737
738        let QueryExpr::KvCommand(KvCommand::List {
739            model,
740            collection,
741            prefix,
742            limit,
743            offset,
744            as_json,
745        }) = expr("LIST VAULT secrets PREFIX api LIMIT 10 OFFSET 2")
746        else {
747            panic!("LIST VAULT should route to FrontendStatement::KvCommand");
748        };
749        assert_eq!(model, CollectionModel::Vault);
750        assert_eq!(collection, "secrets");
751        assert_eq!(prefix.as_deref(), Some("api"));
752        assert_eq!(limit, Some(10));
753        assert_eq!(offset, 2);
754        assert!(!as_json);
755
756        assert!(matches!(
757            expr("INVALIDATE CONFIG app feature_flag"),
758            QueryExpr::ConfigCommand(ConfigCommand::InvalidVolatileOperation {
759                operation,
760                collection,
761                key: Some(key),
762            }) if operation == "INVALIDATE" && collection == "app" && key == "feature_flag"
763        ));
764        assert!(matches!(
765            expr("INVALIDATE TAGS [user:42, org:7] FROM sessions"),
766            QueryExpr::KvCommand(KvCommand::InvalidateTags { collection, tags })
767                if collection == "sessions" && tags == vec!["user:42".to_string(), "org:7".to_string()]
768        ));
769
770        let QueryExpr::EventsBackfill(query) =
771            expr("EVENTS BACKFILL users WHERE status = 'active' TO audit LIMIT 10")
772        else {
773            panic!("EVENTS BACKFILL should route to FrontendStatement::EventsBackfill");
774        };
775        assert_eq!(query.collection, "users");
776        assert_eq!(query.where_filter.as_deref(), Some("status = 'active'"));
777        assert_eq!(query.target_queue, "audit");
778        assert_eq!(query.limit, Some(10));
779
780        let QueryExpr::Table(query) = expr("EVENTS STATUS users LIMIT 2") else {
781            panic!("EVENTS STATUS should route through the SQL select surface");
782        };
783        assert_eq!(query.table, "red.subscriptions");
784        assert_eq!(query.limit, Some(2));
785        assert!(query.filter.is_some());
786
787        assert!(matches!(
788            expr("EVENTS BACKFILL STATUS users"),
789            QueryExpr::EventsBackfillStatus { collection } if collection == "users"
790        ));
791        assert!(parse_frontend("LIST UNKNOWN").is_err());
792        assert!(parse_frontend("EVENTS UNKNOWN").is_err());
793    }
794
795    #[test]
796    fn parse_frontend_routes_ranking_reads() {
797        assert!(matches!(
798            expr("RANK OF 42 IN page_rank"),
799            QueryExpr::RankOf(RankOfQuery { ranking, entity_id })
800                if ranking == "page_rank" && entity_id == 42
801        ));
802        assert!(matches!(
803            expr("APPROX RANK OF 7 IN page_rank"),
804            QueryExpr::ApproxRankOf(RankOfQuery { ranking, entity_id })
805                if ranking == "page_rank" && entity_id == 7
806        ));
807        assert!(matches!(
808            expr("RANK RANGE 1 TO 3 IN page_rank"),
809            QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi })
810                if ranking == "page_rank" && lo == 1 && hi == 3
811        ));
812        assert!(matches!(
813            expr("ZRANK page_rank 0"),
814            QueryExpr::RankOf(RankOfQuery { ranking, entity_id })
815                if ranking == "page_rank" && entity_id == 0
816        ));
817        assert!(matches!(
818            expr("ZRANGE page_rank 0 3 WITHSCORES"),
819            QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi })
820                if ranking == "page_rank" && lo == 1 && hi == 4
821        ));
822        assert!(
823            parse_frontend("RANK RANGE 3 TO 1 IN page_rank").is_err(),
824            "rank range must reject reversed bounds"
825        );
826    }
827
828    #[test]
829    fn parse_frontend_covers_multimodel_command_routing() {
830        assert!(matches!(
831            expr("GRAPH CENTRALITY ALGORITHM pagerank LIMIT 5"),
832            QueryExpr::GraphCommand(GraphCommand::Centrality {
833                algorithm,
834                limit: Some(5),
835                ..
836            }) if algorithm == "pagerank"
837        ));
838        assert!(matches!(
839            expr("SEARCH TEXT 'login failure' COLLECTION incidents LIMIT 20 FUZZY"),
840            QueryExpr::SearchCommand(SearchCommand::Text {
841                query,
842                collection: Some(collection),
843                limit: 20,
844                fuzzy: true,
845                ..
846            }) if query == "login failure" && collection == "incidents"
847        ));
848        assert!(matches!(
849            expr("ASK 'why did login fail?' USING openai LIMIT 3"),
850            QueryExpr::Ask(query)
851                if query.question == "why did login fail?"
852                    && query.provider.as_deref() == Some("openai")
853                    && query.limit == Some(3)
854        ));
855        assert!(matches!(
856            expr("QUEUE LEN tasks"),
857            QueryExpr::QueueCommand(QueueCommand::Len { queue }) if queue == "tasks"
858        ));
859        assert!(matches!(
860            expr("TREE REBALANCE forest.org DRY RUN"),
861            QueryExpr::TreeCommand(TreeCommand::Rebalance {
862                collection,
863                tree_name,
864                dry_run: true,
865            }) if collection == "forest" && tree_name == "org"
866        ));
867        assert!(matches!(
868            expr("HLL COUNT visitors"),
869            QueryExpr::ProbabilisticCommand(ProbabilisticCommand::HllCount { names })
870                if names == vec!["visitors".to_string()]
871        ));
872    }
873
874    #[test]
875    fn sql_command_round_trips_multimodel_schema_variants() {
876        macro_rules! assert_command_round_trip {
877            ($input:expr, $pattern:pat) => {{
878                let command = sql_command($input);
879                assert!(matches!(command, $pattern), "unexpected command for {}", $input);
880
881                let statement = sql_command($input).into_statement();
882                let command = statement.into_command();
883                assert!(
884                    matches!(command, $pattern),
885                    "statement round trip changed command for {}",
886                    $input
887                );
888
889                let expr = sql_command($input).into_query_expr();
890                assert!(
891                    !matches!(expr, QueryExpr::Table(TableQuery { table, .. }) if table.is_empty()),
892                    "lowering produced an empty table placeholder for {}",
893                    $input
894                );
895            }};
896        }
897
898        assert_command_round_trip!(
899            "EXPLAIN ALTER FOR CREATE TABLE users (id INT) FORMAT JSON",
900            SqlCommand::ExplainAlter(_)
901        );
902        assert_command_round_trip!("CREATE TABLE users (id INT)", SqlCommand::CreateTable(_));
903        assert_command_round_trip!("DROP TABLE IF EXISTS users", SqlCommand::DropTable(_));
904        assert_command_round_trip!(
905            "ALTER TABLE users ADD COLUMN status TEXT",
906            SqlCommand::AlterTable(_)
907        );
908        assert_command_round_trip!(
909            "CREATE INDEX idx_email ON users (email) USING HASH",
910            SqlCommand::CreateIndex(_)
911        );
912        assert_command_round_trip!(
913            "DROP INDEX IF EXISTS idx_email ON users",
914            SqlCommand::DropIndex(_)
915        );
916        assert_command_round_trip!("CREATE GRAPH identity", SqlCommand::CreateTable(_));
917        assert_command_round_trip!("CREATE DOCUMENT docs", SqlCommand::CreateTable(_));
918        assert_command_round_trip!(
919            "CREATE VECTOR embeddings DIM 4",
920            SqlCommand::CreateVector(_)
921        );
922        assert_command_round_trip!(
923            "CREATE COLLECTION turbo KIND vector.turbo DIM 3",
924            SqlCommand::CreateCollection(_)
925        );
926        assert_command_round_trip!("CREATE KV settings", SqlCommand::CreateTable(_));
927        assert_command_round_trip!("CREATE CONFIG app", SqlCommand::CreateTable(_));
928        assert_command_round_trip!(
929            "CREATE VAULT secrets WITH OWN MASTER KEY",
930            SqlCommand::CreateTable(_)
931        );
932        assert_command_round_trip!(
933            "CREATE TIMESERIES metrics RETENTION 90 d",
934            SqlCommand::CreateTimeSeries(_)
935        );
936        assert_command_round_trip!(
937            "CREATE METRIC svc.latency TYPE gauge ROLE sli",
938            SqlCommand::CreateMetric(_)
939        );
940        assert_command_round_trip!(
941            "ALTER METRIC svc.latency SET ROLE internal",
942            SqlCommand::AlterMetric(_)
943        );
944        assert_command_round_trip!(
945            "CREATE SLO svc.availability ON svc.latency TARGET 99.9 WINDOW 5 m",
946            SqlCommand::CreateSlo(_)
947        );
948        assert_command_round_trip!(
949            "CREATE QUEUE tasks MAX_SIZE 100",
950            SqlCommand::CreateQueue(_)
951        );
952        assert_command_round_trip!(
953            "ALTER QUEUE tasks SET MODE FANOUT",
954            SqlCommand::AlterQueue(_)
955        );
956        assert_command_round_trip!(
957            "CREATE TREE org IN forest ROOT LABEL root MAX_CHILDREN 4",
958            SqlCommand::CreateTree(_)
959        );
960        assert_command_round_trip!(
961            "CREATE HLL visitors PRECISION 14",
962            SqlCommand::Probabilistic(_)
963        );
964        assert_command_round_trip!(
965            "CREATE SKETCH freqs WIDTH 512 DEPTH 3",
966            SqlCommand::Probabilistic(_)
967        );
968        assert_command_round_trip!(
969            "CREATE FILTER seen CAPACITY 1024",
970            SqlCommand::Probabilistic(_)
971        );
972        assert_command_round_trip!("COPY users FROM '/tmp/u.csv'", SqlCommand::CopyFrom(_));
973        assert_command_round_trip!(
974            "CREATE VIEW active_users AS SELECT * FROM users",
975            SqlCommand::CreateView(_)
976        );
977        assert_command_round_trip!("DROP VIEW active_users", SqlCommand::DropView(_));
978        assert_command_round_trip!(
979            "REFRESH MATERIALIZED VIEW active_users",
980            SqlCommand::RefreshMaterializedView(_)
981        );
982        assert_command_round_trip!(
983            "CREATE SERVER mycsv FOREIGN DATA WRAPPER csv OPTIONS (base_path '/data')",
984            SqlCommand::CreateServer(_)
985        );
986        assert_command_round_trip!(
987            "DROP SERVER IF EXISTS mycsv CASCADE",
988            SqlCommand::DropServer(_)
989        );
990        assert_command_round_trip!(
991            "CREATE FOREIGN TABLE ext_users (id INT, name TEXT) SERVER mycsv OPTIONS (path 'users.csv')",
992            SqlCommand::CreateForeignTable(_)
993        );
994        assert_command_round_trip!(
995            "DROP FOREIGN TABLE IF EXISTS ext_users",
996            SqlCommand::DropForeignTable(_)
997        );
998    }
999
1000    #[test]
1001    fn sql_command_round_trips_drop_truncate_and_maintenance_variants() {
1002        macro_rules! assert_command_round_trip {
1003            ($input:expr, $pattern:pat) => {{
1004                let command = sql_command($input);
1005                assert!(
1006                    matches!(command, $pattern),
1007                    "unexpected command for {}",
1008                    $input
1009                );
1010                let statement = sql_command($input).into_statement();
1011                assert!(
1012                    matches!(statement.into_command(), $pattern),
1013                    "statement round trip changed command for {}",
1014                    $input
1015                );
1016            }};
1017        }
1018
1019        assert_command_round_trip!("DROP GRAPH IF EXISTS identity", SqlCommand::DropGraph(_));
1020        assert_command_round_trip!(
1021            "DROP VECTOR IF EXISTS embeddings",
1022            SqlCommand::DropVector(_)
1023        );
1024        assert_command_round_trip!("DROP DOCUMENT IF EXISTS docs", SqlCommand::DropDocument(_));
1025        assert_command_round_trip!("DROP KV IF EXISTS settings", SqlCommand::DropKv(_));
1026        assert_command_round_trip!("DROP CONFIG IF EXISTS app", SqlCommand::DropKv(_));
1027        assert_command_round_trip!("DROP VAULT IF EXISTS secrets", SqlCommand::DropKv(_));
1028        assert_command_round_trip!(
1029            "DROP COLLECTION IF EXISTS docs",
1030            SqlCommand::DropCollection(_)
1031        );
1032        assert_command_round_trip!(
1033            "DROP TIMESERIES IF EXISTS metrics",
1034            SqlCommand::DropTimeSeries(_)
1035        );
1036        assert_command_round_trip!(
1037            "DROP HYPERTABLE IF EXISTS metrics",
1038            SqlCommand::DropTimeSeries(_)
1039        );
1040        assert_command_round_trip!("DROP QUEUE IF EXISTS tasks", SqlCommand::DropQueue(_));
1041        assert_command_round_trip!("DROP TREE IF EXISTS org IN forest", SqlCommand::DropTree(_));
1042        assert_command_round_trip!("DROP HLL IF EXISTS visitors", SqlCommand::Probabilistic(_));
1043        assert_command_round_trip!("DROP SKETCH IF EXISTS freqs", SqlCommand::Probabilistic(_));
1044        assert_command_round_trip!("DROP FILTER IF EXISTS seen", SqlCommand::Probabilistic(_));
1045        assert_command_round_trip!(
1046            "TRUNCATE VECTOR IF EXISTS embeddings",
1047            SqlCommand::Truncate(_)
1048        );
1049        assert_command_round_trip!(
1050            "COMMIT WORK",
1051            SqlCommand::TransactionControl(TxnControl::Commit)
1052        );
1053        assert_command_round_trip!(
1054            "ROLLBACK",
1055            SqlCommand::TransactionControl(TxnControl::Rollback)
1056        );
1057        assert_command_round_trip!(
1058            "SAVEPOINT before_batch",
1059            SqlCommand::TransactionControl(TxnControl::Savepoint(_))
1060        );
1061        assert_command_round_trip!(
1062            "RELEASE SAVEPOINT before_batch",
1063            SqlCommand::TransactionControl(TxnControl::ReleaseSavepoint(_))
1064        );
1065        assert_command_round_trip!(
1066            "ANALYZE users",
1067            SqlCommand::Maintenance(MaintenanceCommand::Analyze { .. })
1068        );
1069        assert_command_round_trip!(
1070            "VACUUM",
1071            SqlCommand::Maintenance(MaintenanceCommand::Vacuum { .. })
1072        );
1073    }
1074
1075    #[test]
1076    fn parse_sql_command_covers_show_and_error_branches() {
1077        assert!(matches!(
1078            sql_command("SHOW CREATE TABLE public.users"),
1079            SqlCommand::Select(TableQuery { table, .. }) if table == "red.show_create"
1080        ));
1081        assert!(matches!(
1082            sql_command("SHOW COLLECTIONS INCLUDING INTERNAL LIMIT 2"),
1083            SqlCommand::Select(TableQuery { table, limit: Some(2), .. }) if table == "red.collections"
1084        ));
1085        assert!(matches!(
1086            sql_command("SHOW QUEUES INCLUDING INTERNAL"),
1087            SqlCommand::Select(TableQuery { table, filter: None, .. }) if table == "red.queues"
1088        ));
1089        assert!(matches!(
1090            sql_command("SHOW INDICES ON users"),
1091            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.show_indexes"
1092        ));
1093        assert!(matches!(
1094            sql_command("SHOW POLICIES ON users WHERE action = 'SELECT'"),
1095            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.policies"
1096        ));
1097        assert!(matches!(
1098            sql_command("SHOW STATS 'users' WHERE rows > 0"),
1099            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.stats"
1100        ));
1101        assert!(matches!(
1102            sql_command("SHOW SAMPLE users"),
1103            SqlCommand::Select(TableQuery { table, limit: Some(10), .. }) if table == "users"
1104        ));
1105        assert!(matches!(
1106            sql_command("DESC public.users"),
1107            SqlCommand::Select(TableQuery { table, filter: Some(_), .. }) if table == "red.describe"
1108        ));
1109        assert!(
1110            sql_command_result("CREATE VIEW v WITH RETENTION 1 h AS SELECT * FROM users").is_err()
1111        );
1112        assert!(sql_command_result("CREATE TABLE bad WITH ANALYTICS (centrality)").is_err());
1113        assert!(sql_command_result("BEGIN ISOLATION LEVEL SERIALIZABLE").is_err());
1114        assert!(sql_command_result("EVENTS BACKFILL STATUS users").is_err());
1115    }
1116
1117    #[test]
1118    fn parse_sql_command_covers_remaining_catalog_and_copy_shapes() {
1119        for input in [
1120            "SHOW VECTORS",
1121            "SHOW DOCUMENTS",
1122            "SHOW TIMESERIES",
1123            "SHOW GRAPHS",
1124            "SHOW CONFIGS",
1125            "SHOW VAULTS",
1126            "SHOW KV",
1127            "SHOW SCHEMA public.users",
1128        ] {
1129            assert!(
1130                matches!(sql_command(input), SqlCommand::Select(_)),
1131                "{input}"
1132            );
1133        }
1134
1135        for input in [
1136            "TRUNCATE TABLE users",
1137            "TRUNCATE GRAPH identity",
1138            "TRUNCATE DOCUMENT docs",
1139            "TRUNCATE TIMESERIES metrics",
1140            "TRUNCATE METRICS metrics",
1141            "TRUNCATE KV settings",
1142            "TRUNCATE QUEUE tasks",
1143            "TRUNCATE COLLECTION docs",
1144        ] {
1145            assert!(
1146                matches!(sql_command(input), SqlCommand::Truncate(_)),
1147                "{input}"
1148            );
1149        }
1150        assert!(sql_command_result("TRUNCATE UNKNOWN users").is_err());
1151
1152        let SqlCommand::CopyFrom(copy) = sql_command("COPY users FROM '/tmp/u.csv' WITH (HEADER)")
1153        else {
1154            panic!("expected COPY");
1155        };
1156        assert!(copy.has_header);
1157        assert_eq!(copy.delimiter, None);
1158
1159        let SqlCommand::CopyFrom(copy) =
1160            sql_command("COPY users FROM '/tmp/u.csv' WITH (HEADER = false)")
1161        else {
1162            panic!("expected COPY");
1163        };
1164        assert!(!copy.has_header);
1165
1166        let SqlCommand::CopyFrom(copy) =
1167            sql_command("COPY users FROM '/tmp/u.csv' DELIMITER '|' HEADER")
1168        else {
1169            panic!("expected COPY");
1170        };
1171        assert_eq!(copy.delimiter, Some('|'));
1172        assert!(copy.has_header);
1173    }
1174
1175    #[test]
1176    fn parse_sql_command_covers_remaining_ranking_and_event_errors() {
1177        assert!(matches!(
1178            expr("APPROXIMATE RANK OF 9 IN page_rank"),
1179            QueryExpr::ApproxRankOf(RankOfQuery { ranking, entity_id })
1180                if ranking == "page_rank" && entity_id == 9
1181        ));
1182        assert!(parse_frontend("APPROX OF 7 IN page_rank").is_err());
1183        assert!(parse_frontend("RANK 1 IN page_rank").is_err());
1184        assert!(parse_frontend("RANK RANGE 0 TO 3 IN page_rank").is_err());
1185        assert!(parse_frontend("ZRANK page_rank -1").is_err());
1186        assert!(parse_frontend("ZRANGE page_rank 3 1").is_err());
1187
1188        let QueryExpr::Table(query) = expr("EVENTS STATUS 'users' WHERE active = true") else {
1189            panic!("EVENTS STATUS should accept a quoted collection");
1190        };
1191        assert_eq!(query.table, "red.subscriptions");
1192        assert!(query.filter.is_some());
1193        assert!(query.where_expr.is_some());
1194
1195        let QueryExpr::EventsBackfill(query) = expr("EVENTS BACKFILL users TO audit") else {
1196            panic!("EVENTS BACKFILL should allow omitted filter and limit");
1197        };
1198        assert_eq!(query.collection, "users");
1199        assert_eq!(query.where_filter, None);
1200        assert_eq!(query.limit, None);
1201
1202        assert!(parse_frontend("EVENTS BACKFILL users WHERE TO audit").is_err());
1203    }
1204
1205    #[test]
1206    fn parse_sql_command_covers_analytics_non_goal_rejections() {
1207        for head in ["ANALYTICS", "EVENT", "COHORT", "FUNNEL", "SLA", "ADAPTER"] {
1208            let err = sql_command_result(&format!("CREATE {head} demo"))
1209                .expect_err("analytics v0 non-goal should be rejected");
1210            assert!(err.to_string().contains(&format!("CREATE {head}")), "{err}");
1211        }
1212    }
1213
1214    #[test]
1215    fn parse_sql_command_covers_transaction_isolation_edges() {
1216        for input in [
1217            "BEGIN ISOLATION LEVEL READ UNCOMMITTED",
1218            "BEGIN ISOLATION LEVEL READ COMMITTED",
1219            "BEGIN ISOLATION LEVEL REPEATABLE READ",
1220            "START TRANSACTION ISOLATION LEVEL SNAPSHOT",
1221        ] {
1222            assert!(
1223                matches!(
1224                    sql_command(input),
1225                    SqlCommand::TransactionControl(TxnControl::Begin)
1226                ),
1227                "{input}"
1228            );
1229        }
1230
1231        assert!(sql_command_result("BEGIN ISOLATION LEVEL READ").is_err());
1232        assert!(sql_command_result("BEGIN ISOLATION LEVEL REPEATABLE").is_err());
1233        assert!(sql_command_result("BEGIN ISOLATION LEVEL CHAOS").is_err());
1234    }
1235
1236    #[test]
1237    fn parse_sql_command_covers_vcs_working_set_verbs() {
1238        assert!(matches!(
1239            sql_command("CHECKPOINT 'initial import' AUTHOR 'Ada <ada@reddb.io>'"),
1240            SqlCommand::Vcs(VcsCommand::Checkpoint { .. })
1241        ));
1242        assert!(matches!(
1243            sql_command("CHECKOUT main"),
1244            SqlCommand::Vcs(VcsCommand::Checkout { .. })
1245        ));
1246        assert!(matches!(
1247            sql_command("RESET HARD TO abc123"),
1248            SqlCommand::Vcs(VcsCommand::Reset {
1249                mode: VcsResetMode::Hard,
1250                ..
1251            })
1252        ));
1253        assert!(matches!(
1254            sql_command("MERGE 'feature'"),
1255            SqlCommand::Vcs(VcsCommand::Merge { .. })
1256        ));
1257        assert!(matches!(
1258            sql_command("CHERRY PICK 'abc123'"),
1259            SqlCommand::Vcs(VcsCommand::CherryPick { .. })
1260        ));
1261        assert!(matches!(
1262            sql_command("REVERT 'abc123'"),
1263            SqlCommand::Vcs(VcsCommand::Revert { .. })
1264        ));
1265        assert!(matches!(
1266            sql_command("RESOLVE CONFLICT 'orders/1' USING THEIRS"),
1267            SqlCommand::Vcs(VcsCommand::ResolveConflict {
1268                resolution: VcsConflictResolution::Theirs,
1269                ..
1270            })
1271        ));
1272        assert!(matches!(
1273            sql_command("RESET TENANT"),
1274            SqlCommand::SetTenant(None)
1275        ));
1276    }
1277
1278    #[test]
1279    fn parse_sql_command_covers_iam_and_hypertable_dispatch_edges() {
1280        assert!(matches!(
1281            expr("CREATE HYPERTABLE metrics TIME_COLUMN ts CHUNK_INTERVAL '1d'"),
1282            QueryExpr::CreateTimeSeries(query)
1283                if query.name == "metrics" && query.hypertable.is_some()
1284        ));
1285        assert!(sql_command_result("CREATE OR TABLE bad (id INT)").is_err());
1286        assert!(sql_command_result("DROP MATERIALIZED TABLE bad").is_err());
1287
1288        assert!(matches!(
1289            expr("ATTACH POLICY 'readonly' TO USER tenant1.alice"),
1290            QueryExpr::AttachPolicy { policy_id, .. } if policy_id == "readonly"
1291        ));
1292        assert!(matches!(
1293            expr("DETACH POLICY 'readonly' FROM GROUP analysts"),
1294            QueryExpr::DetachPolicy { policy_id, .. } if policy_id == "readonly"
1295        ));
1296        assert!(matches!(
1297            expr("SHOW POLICIES FOR USER alice"),
1298            QueryExpr::ShowPolicies { filter: Some(_) }
1299        ));
1300        assert!(matches!(
1301            expr("SHOW EFFECTIVE PERMISSIONS FOR alice"),
1302            QueryExpr::ShowEffectivePermissions { resource: None, .. }
1303        ));
1304        assert!(matches!(
1305            expr("SIMULATE alice ACTION 'iam:PassRole' ON TABLE:public.orders"),
1306            QueryExpr::SimulatePolicy { action, .. } if action == "iam:PassRole"
1307        ));
1308        assert!(matches!(
1309            expr("LINT POLICY JSON '{\"Statement\":[]}'"),
1310            QueryExpr::LintPolicy { .. }
1311        ));
1312        assert!(matches!(
1313            expr("MIGRATE POLICY MODE TO 'policy_only' DRY RUN"),
1314            QueryExpr::MigratePolicyMode {
1315                target,
1316                dry_run: true,
1317            } if target == "policy_only"
1318        ));
1319        assert!(parse_frontend("MIGRATE OTHER").is_err());
1320    }
1321
1322    #[test]
1323    fn parse_frontend_rejects_trailing_tokens() {
1324        let err = parse_frontend("SET TENANT 'acme' junk")
1325            .expect_err("parse_frontend should reject trailing tokens");
1326        assert!(
1327            err.to_string().contains("Unexpected token after query"),
1328            "{err}"
1329        );
1330    }
1331}
1332
1333fn add_table_filter(query: &mut TableQuery, filter: Filter) {
1334    let combined = match query.filter.take() {
1335        Some(existing) => existing.and(filter),
1336        None => filter,
1337    };
1338    query.where_expr = Some(filter_to_expr(&combined));
1339    query.filter = Some(combined);
1340}
1341
1342fn parse_show_collections_by_model(
1343    parser: &mut Parser<'_>,
1344    model: &str,
1345) -> Result<TableQuery, ParseError> {
1346    let mut query = TableQuery::new("red.collections");
1347    parser.parse_table_clauses(&mut query)?;
1348    add_table_filter(&mut query, collection_model_filter(model));
1349    Ok(query)
1350}
1351
1352#[derive(Debug, Clone)]
1353#[allow(clippy::large_enum_variant)]
1354pub enum SqlQuery {
1355    Select(TableQuery),
1356    Join(JoinQuery),
1357}
1358
1359#[derive(Debug, Clone)]
1360pub enum SqlMutation {
1361    Insert(InsertQuery),
1362    Update(UpdateQuery),
1363    Delete(DeleteQuery),
1364}
1365
1366#[derive(Debug, Clone)]
1367pub enum SqlSchemaCommand {
1368    ExplainAlter(ExplainAlterQuery),
1369    CreateTable(CreateTableQuery),
1370    CreateCollection(CreateCollectionQuery),
1371    CreateVector(CreateVectorQuery),
1372    DropTable(DropTableQuery),
1373    DropGraph(DropGraphQuery),
1374    DropVector(DropVectorQuery),
1375    DropDocument(DropDocumentQuery),
1376    DropKv(DropKvQuery),
1377    DropCollection(DropCollectionQuery),
1378    Truncate(TruncateQuery),
1379    AlterTable(AlterTableQuery),
1380    CreateVcsRef(CreateVcsRefQuery),
1381    DropVcsRef(DropVcsRefQuery),
1382    CreateIndex(CreateIndexQuery),
1383    DropIndex(DropIndexQuery),
1384    CreateTimeSeries(CreateTimeSeriesQuery),
1385    CreateMetric(CreateMetricQuery),
1386    AlterMetric(AlterMetricQuery),
1387    CreateSlo(CreateSloQuery),
1388    DropTimeSeries(DropTimeSeriesQuery),
1389    CreateQueue(CreateQueueQuery),
1390    AlterQueue(AlterQueueQuery),
1391    DropQueue(DropQueueQuery),
1392    CreateTree(CreateTreeQuery),
1393    DropTree(DropTreeQuery),
1394    Probabilistic(ProbabilisticCommand),
1395    CreateSchema(CreateSchemaQuery),
1396    DropSchema(DropSchemaQuery),
1397    CreateSequence(CreateSequenceQuery),
1398    DropSequence(DropSequenceQuery),
1399    CopyFrom(CopyFromQuery),
1400    CreateView(CreateViewQuery),
1401    DropView(DropViewQuery),
1402    RefreshMaterializedView(RefreshMaterializedViewQuery),
1403    CreatePolicy(CreatePolicyQuery),
1404    DropPolicy(DropPolicyQuery),
1405    CreateServer(CreateServerQuery),
1406    DropServer(DropServerQuery),
1407    CreateForeignTable(CreateForeignTableQuery),
1408    DropForeignTable(DropForeignTableQuery),
1409    CreateMigration(CreateMigrationQuery),
1410    ApplyMigration(ApplyMigrationQuery),
1411    RollbackMigration(RollbackMigrationQuery),
1412    ExplainMigration(ExplainMigrationQuery),
1413}
1414
1415#[derive(Debug, Clone)]
1416#[allow(clippy::large_enum_variant)]
1417pub enum SqlAdminCommand {
1418    SetConfig {
1419        key: String,
1420        value: Value,
1421    },
1422    ShowConfig {
1423        prefix: Option<String>,
1424        as_json: bool,
1425    },
1426    SetSecret {
1427        key: String,
1428        value: Value,
1429    },
1430    DeleteSecret {
1431        key: String,
1432    },
1433    ShowSecrets {
1434        prefix: Option<String>,
1435    },
1436    SetKv {
1437        key: String,
1438        value: Value,
1439    },
1440    DeleteKv {
1441        key: String,
1442    },
1443    SetTenant(Option<String>),
1444    ShowTenant,
1445    TransactionControl(TxnControl),
1446    Maintenance(MaintenanceCommand),
1447    Vcs(VcsCommand),
1448    Grant(GrantStmt),
1449    Revoke(RevokeStmt),
1450    AlterUser(AlterUserStmt),
1451    CreateUser(CreateUserStmt),
1452    IamPolicy(QueryExpr),
1453}
1454
1455impl SqlStatement {
1456    pub fn into_command(self) -> SqlCommand {
1457        match self {
1458            SqlStatement::Query(SqlQuery::Select(query)) => SqlCommand::Select(query),
1459            SqlStatement::Query(SqlQuery::Join(query)) => SqlCommand::Join(query),
1460            SqlStatement::Mutation(SqlMutation::Insert(query)) => SqlCommand::Insert(query),
1461            SqlStatement::Mutation(SqlMutation::Update(query)) => SqlCommand::Update(query),
1462            SqlStatement::Mutation(SqlMutation::Delete(query)) => SqlCommand::Delete(query),
1463            SqlStatement::Schema(SqlSchemaCommand::ExplainAlter(query)) => {
1464                SqlCommand::ExplainAlter(query)
1465            }
1466            SqlStatement::Schema(SqlSchemaCommand::CreateTable(query)) => {
1467                SqlCommand::CreateTable(query)
1468            }
1469            SqlStatement::Schema(SqlSchemaCommand::CreateCollection(query)) => {
1470                SqlCommand::CreateCollection(query)
1471            }
1472            SqlStatement::Schema(SqlSchemaCommand::CreateVector(query)) => {
1473                SqlCommand::CreateVector(query)
1474            }
1475            SqlStatement::Schema(SqlSchemaCommand::DropTable(query)) => {
1476                SqlCommand::DropTable(query)
1477            }
1478            SqlStatement::Schema(SqlSchemaCommand::DropGraph(query)) => {
1479                SqlCommand::DropGraph(query)
1480            }
1481            SqlStatement::Schema(SqlSchemaCommand::DropVector(query)) => {
1482                SqlCommand::DropVector(query)
1483            }
1484            SqlStatement::Schema(SqlSchemaCommand::DropDocument(query)) => {
1485                SqlCommand::DropDocument(query)
1486            }
1487            SqlStatement::Schema(SqlSchemaCommand::DropKv(query)) => SqlCommand::DropKv(query),
1488            SqlStatement::Schema(SqlSchemaCommand::DropCollection(query)) => {
1489                SqlCommand::DropCollection(query)
1490            }
1491            SqlStatement::Schema(SqlSchemaCommand::Truncate(query)) => SqlCommand::Truncate(query),
1492            SqlStatement::Schema(SqlSchemaCommand::AlterTable(query)) => {
1493                SqlCommand::AlterTable(query)
1494            }
1495            SqlStatement::Schema(SqlSchemaCommand::CreateVcsRef(query)) => {
1496                SqlCommand::CreateVcsRef(query)
1497            }
1498            SqlStatement::Schema(SqlSchemaCommand::DropVcsRef(query)) => {
1499                SqlCommand::DropVcsRef(query)
1500            }
1501            SqlStatement::Schema(SqlSchemaCommand::CreateIndex(query)) => {
1502                SqlCommand::CreateIndex(query)
1503            }
1504            SqlStatement::Schema(SqlSchemaCommand::DropIndex(query)) => {
1505                SqlCommand::DropIndex(query)
1506            }
1507            SqlStatement::Schema(SqlSchemaCommand::CreateTimeSeries(query)) => {
1508                SqlCommand::CreateTimeSeries(query)
1509            }
1510            SqlStatement::Schema(SqlSchemaCommand::CreateMetric(query)) => {
1511                SqlCommand::CreateMetric(query)
1512            }
1513            SqlStatement::Schema(SqlSchemaCommand::AlterMetric(query)) => {
1514                SqlCommand::AlterMetric(query)
1515            }
1516            SqlStatement::Schema(SqlSchemaCommand::CreateSlo(query)) => {
1517                SqlCommand::CreateSlo(query)
1518            }
1519            SqlStatement::Schema(SqlSchemaCommand::DropTimeSeries(query)) => {
1520                SqlCommand::DropTimeSeries(query)
1521            }
1522            SqlStatement::Schema(SqlSchemaCommand::CreateQueue(query)) => {
1523                SqlCommand::CreateQueue(query)
1524            }
1525            SqlStatement::Schema(SqlSchemaCommand::AlterQueue(query)) => {
1526                SqlCommand::AlterQueue(query)
1527            }
1528            SqlStatement::Schema(SqlSchemaCommand::DropQueue(query)) => {
1529                SqlCommand::DropQueue(query)
1530            }
1531            SqlStatement::Schema(SqlSchemaCommand::CreateTree(query)) => {
1532                SqlCommand::CreateTree(query)
1533            }
1534            SqlStatement::Schema(SqlSchemaCommand::DropTree(query)) => SqlCommand::DropTree(query),
1535            SqlStatement::Schema(SqlSchemaCommand::Probabilistic(command)) => {
1536                SqlCommand::Probabilistic(command)
1537            }
1538            SqlStatement::Admin(SqlAdminCommand::SetConfig { key, value }) => {
1539                SqlCommand::SetConfig { key, value }
1540            }
1541            SqlStatement::Admin(SqlAdminCommand::ShowConfig { prefix, as_json }) => {
1542                SqlCommand::ShowConfig { prefix, as_json }
1543            }
1544            SqlStatement::Admin(SqlAdminCommand::SetSecret { key, value }) => {
1545                SqlCommand::SetSecret { key, value }
1546            }
1547            SqlStatement::Admin(SqlAdminCommand::DeleteSecret { key }) => {
1548                SqlCommand::DeleteSecret { key }
1549            }
1550            SqlStatement::Admin(SqlAdminCommand::ShowSecrets { prefix }) => {
1551                SqlCommand::ShowSecrets { prefix }
1552            }
1553            SqlStatement::Admin(SqlAdminCommand::SetKv { key, value }) => {
1554                SqlCommand::SetKv { key, value }
1555            }
1556            SqlStatement::Admin(SqlAdminCommand::DeleteKv { key }) => SqlCommand::DeleteKv { key },
1557            SqlStatement::Admin(SqlAdminCommand::SetTenant(value)) => SqlCommand::SetTenant(value),
1558            SqlStatement::Admin(SqlAdminCommand::ShowTenant) => SqlCommand::ShowTenant,
1559            SqlStatement::Admin(SqlAdminCommand::TransactionControl(ctl)) => {
1560                SqlCommand::TransactionControl(ctl)
1561            }
1562            SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)) => SqlCommand::Maintenance(cmd),
1563            SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)) => SqlCommand::Vcs(cmd),
1564            SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)) => SqlCommand::CreateSchema(q),
1565            SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)) => SqlCommand::DropSchema(q),
1566            SqlStatement::Schema(SqlSchemaCommand::CreateSequence(q)) => {
1567                SqlCommand::CreateSequence(q)
1568            }
1569            SqlStatement::Schema(SqlSchemaCommand::DropSequence(q)) => SqlCommand::DropSequence(q),
1570            SqlStatement::Schema(SqlSchemaCommand::CopyFrom(q)) => SqlCommand::CopyFrom(q),
1571            SqlStatement::Schema(SqlSchemaCommand::CreateView(q)) => SqlCommand::CreateView(q),
1572            SqlStatement::Schema(SqlSchemaCommand::DropView(q)) => SqlCommand::DropView(q),
1573            SqlStatement::Schema(SqlSchemaCommand::RefreshMaterializedView(q)) => {
1574                SqlCommand::RefreshMaterializedView(q)
1575            }
1576            SqlStatement::Schema(SqlSchemaCommand::CreatePolicy(q)) => SqlCommand::CreatePolicy(q),
1577            SqlStatement::Schema(SqlSchemaCommand::DropPolicy(q)) => SqlCommand::DropPolicy(q),
1578            SqlStatement::Schema(SqlSchemaCommand::CreateServer(q)) => SqlCommand::CreateServer(q),
1579            SqlStatement::Schema(SqlSchemaCommand::DropServer(q)) => SqlCommand::DropServer(q),
1580            SqlStatement::Schema(SqlSchemaCommand::CreateForeignTable(q)) => {
1581                SqlCommand::CreateForeignTable(q)
1582            }
1583            SqlStatement::Schema(SqlSchemaCommand::DropForeignTable(q)) => {
1584                SqlCommand::DropForeignTable(q)
1585            }
1586            SqlStatement::Admin(SqlAdminCommand::Grant(s)) => SqlCommand::Grant(s),
1587            SqlStatement::Admin(SqlAdminCommand::Revoke(s)) => SqlCommand::Revoke(s),
1588            SqlStatement::Admin(SqlAdminCommand::AlterUser(s)) => SqlCommand::AlterUser(s),
1589            SqlStatement::Admin(SqlAdminCommand::CreateUser(s)) => SqlCommand::CreateUser(s),
1590            SqlStatement::Admin(SqlAdminCommand::IamPolicy(e)) => SqlCommand::IamPolicy(e),
1591            SqlStatement::Schema(SqlSchemaCommand::CreateMigration(q)) => {
1592                SqlCommand::CreateMigration(q)
1593            }
1594            SqlStatement::Schema(SqlSchemaCommand::ApplyMigration(q)) => {
1595                SqlCommand::ApplyMigration(q)
1596            }
1597            SqlStatement::Schema(SqlSchemaCommand::RollbackMigration(q)) => {
1598                SqlCommand::RollbackMigration(q)
1599            }
1600            SqlStatement::Schema(SqlSchemaCommand::ExplainMigration(q)) => {
1601                SqlCommand::ExplainMigration(q)
1602            }
1603        }
1604    }
1605
1606    pub fn into_query_expr(self) -> QueryExpr {
1607        self.into_command().into_query_expr()
1608    }
1609}
1610
1611impl FrontendStatement {
1612    pub fn into_query_expr(self) -> QueryExpr {
1613        match self {
1614            FrontendStatement::Sql(statement) => statement.into_query_expr(),
1615            FrontendStatement::Graph(query) => QueryExpr::Graph(query),
1616            FrontendStatement::GraphCommand(command) => QueryExpr::GraphCommand(command),
1617            FrontendStatement::Path(query) => QueryExpr::Path(query),
1618            FrontendStatement::Vector(query) => QueryExpr::Vector(query),
1619            FrontendStatement::Hybrid(query) => QueryExpr::Hybrid(query),
1620            FrontendStatement::Search(command) => QueryExpr::SearchCommand(command),
1621            FrontendStatement::Ask(query) => QueryExpr::Ask(query),
1622            FrontendStatement::QueueSelect(query) => QueryExpr::QueueSelect(query),
1623            FrontendStatement::QueueCommand(command) => QueryExpr::QueueCommand(command),
1624            FrontendStatement::EventsBackfill(query) => QueryExpr::EventsBackfill(query),
1625            FrontendStatement::EventsBackfillStatus { collection } => {
1626                QueryExpr::EventsBackfillStatus { collection }
1627            }
1628            FrontendStatement::TreeCommand(command) => QueryExpr::TreeCommand(command),
1629            FrontendStatement::ProbabilisticCommand(command) => {
1630                QueryExpr::ProbabilisticCommand(command)
1631            }
1632            FrontendStatement::KvCommand(command) => QueryExpr::KvCommand(command),
1633            FrontendStatement::ConfigCommand(command) => QueryExpr::ConfigCommand(command),
1634            FrontendStatement::Ranking(expr) => expr,
1635        }
1636    }
1637}
1638
1639pub fn parse_frontend(input: &str) -> Result<FrontendStatement, ParseError> {
1640    let mut parser = Parser::new(input)?;
1641    let statement = parser.parse_frontend_statement()?;
1642    if !parser.check(&Token::Eof) {
1643        return Err(ParseError::new(
1644            // F-05: `Token::Ident` / `Token::String` / `Token::JsonLiteral`
1645            // Display arms emit raw user bytes. Render via `{:?}` so
1646            // embedded CR/LF/NUL/quotes are escaped before the message
1647            // reaches downstream JSON / audit / log / gRPC sinks.
1648            format!("Unexpected token after query: {:?}", parser.current.token),
1649            parser.position(),
1650        ));
1651    }
1652    Ok(statement)
1653}
1654
1655fn is_vcs_command_head(name: &str) -> bool {
1656    name.eq_ignore_ascii_case("CHECKPOINT")
1657        || name.eq_ignore_ascii_case("CHECKOUT")
1658        || name.eq_ignore_ascii_case("MERGE")
1659        || name.eq_ignore_ascii_case("CHERRY")
1660        || name.eq_ignore_ascii_case("REVERT")
1661    // NOTE: RESOLVE is intentionally excluded here — it is shared with the
1662    // config surface (`RESOLVE CONFIG …`). The VCS `RESOLVE CONFLICT` shape is
1663    // dispatched by a dedicated arm that peeks for the CONFLICT keyword.
1664}
1665
1666impl SqlCommand {
1667    pub fn into_query_expr(self) -> QueryExpr {
1668        match self {
1669            SqlCommand::Select(query) => QueryExpr::Table(query),
1670            SqlCommand::Join(query) => QueryExpr::Join(query),
1671            SqlCommand::Insert(query) => QueryExpr::Insert(query),
1672            SqlCommand::Update(query) => QueryExpr::Update(query),
1673            SqlCommand::Delete(query) => QueryExpr::Delete(query),
1674            SqlCommand::ExplainAlter(query) => QueryExpr::ExplainAlter(query),
1675            SqlCommand::CreateTable(query) => QueryExpr::CreateTable(query),
1676            SqlCommand::CreateCollection(query) => QueryExpr::CreateCollection(query),
1677            SqlCommand::CreateVector(query) => QueryExpr::CreateVector(query),
1678            SqlCommand::DropTable(query) => QueryExpr::DropTable(query),
1679            SqlCommand::DropGraph(query) => QueryExpr::DropGraph(query),
1680            SqlCommand::DropVector(query) => QueryExpr::DropVector(query),
1681            SqlCommand::DropDocument(query) => QueryExpr::DropDocument(query),
1682            SqlCommand::DropKv(query) => QueryExpr::DropKv(query),
1683            SqlCommand::DropCollection(query) => QueryExpr::DropCollection(query),
1684            SqlCommand::Truncate(query) => QueryExpr::Truncate(query),
1685            SqlCommand::AlterTable(query) => QueryExpr::AlterTable(query),
1686            SqlCommand::CreateVcsRef(query) => QueryExpr::CreateVcsRef(query),
1687            SqlCommand::DropVcsRef(query) => QueryExpr::DropVcsRef(query),
1688            SqlCommand::CreateIndex(query) => QueryExpr::CreateIndex(query),
1689            SqlCommand::DropIndex(query) => QueryExpr::DropIndex(query),
1690            SqlCommand::CreateTimeSeries(query) => QueryExpr::CreateTimeSeries(query),
1691            SqlCommand::CreateMetric(query) => QueryExpr::CreateMetric(query),
1692            SqlCommand::AlterMetric(query) => QueryExpr::AlterMetric(query),
1693            SqlCommand::CreateSlo(query) => QueryExpr::CreateSlo(query),
1694            SqlCommand::DropTimeSeries(query) => QueryExpr::DropTimeSeries(query),
1695            SqlCommand::CreateQueue(query) => QueryExpr::CreateQueue(query),
1696            SqlCommand::AlterQueue(query) => QueryExpr::AlterQueue(query),
1697            SqlCommand::DropQueue(query) => QueryExpr::DropQueue(query),
1698            SqlCommand::CreateTree(query) => QueryExpr::CreateTree(query),
1699            SqlCommand::DropTree(query) => QueryExpr::DropTree(query),
1700            SqlCommand::Probabilistic(command) => QueryExpr::ProbabilisticCommand(command),
1701            SqlCommand::SetConfig { key, value } => QueryExpr::SetConfig { key, value },
1702            SqlCommand::ShowConfig { prefix, as_json } => QueryExpr::ShowConfig { prefix, as_json },
1703            SqlCommand::SetSecret { key, value } => QueryExpr::SetSecret { key, value },
1704            SqlCommand::DeleteSecret { key } => QueryExpr::DeleteSecret { key },
1705            SqlCommand::ShowSecrets { prefix } => QueryExpr::ShowSecrets { prefix },
1706            SqlCommand::SetKv { key, value } => QueryExpr::SetKv { key, value },
1707            SqlCommand::DeleteKv { key } => QueryExpr::DeleteKv { key },
1708            SqlCommand::SetTenant(value) => QueryExpr::SetTenant(value),
1709            SqlCommand::ShowTenant => QueryExpr::ShowTenant,
1710            SqlCommand::TransactionControl(ctl) => QueryExpr::TransactionControl(ctl),
1711            SqlCommand::Maintenance(cmd) => QueryExpr::MaintenanceCommand(cmd),
1712            SqlCommand::Vcs(cmd) => QueryExpr::VcsCommand(cmd),
1713            SqlCommand::CreateSchema(q) => QueryExpr::CreateSchema(q),
1714            SqlCommand::DropSchema(q) => QueryExpr::DropSchema(q),
1715            SqlCommand::CreateSequence(q) => QueryExpr::CreateSequence(q),
1716            SqlCommand::DropSequence(q) => QueryExpr::DropSequence(q),
1717            SqlCommand::CopyFrom(q) => QueryExpr::CopyFrom(q),
1718            SqlCommand::CreateView(q) => QueryExpr::CreateView(q),
1719            SqlCommand::DropView(q) => QueryExpr::DropView(q),
1720            SqlCommand::RefreshMaterializedView(q) => QueryExpr::RefreshMaterializedView(q),
1721            SqlCommand::CreatePolicy(q) => QueryExpr::CreatePolicy(q),
1722            SqlCommand::DropPolicy(q) => QueryExpr::DropPolicy(q),
1723            SqlCommand::CreateServer(q) => QueryExpr::CreateServer(q),
1724            SqlCommand::DropServer(q) => QueryExpr::DropServer(q),
1725            SqlCommand::CreateForeignTable(q) => QueryExpr::CreateForeignTable(q),
1726            SqlCommand::DropForeignTable(q) => QueryExpr::DropForeignTable(q),
1727            SqlCommand::Grant(s) => QueryExpr::Grant(s),
1728            SqlCommand::Revoke(s) => QueryExpr::Revoke(s),
1729            SqlCommand::AlterUser(s) => QueryExpr::AlterUser(s),
1730            SqlCommand::CreateUser(s) => QueryExpr::CreateUser(s),
1731            SqlCommand::IamPolicy(e) => e,
1732            SqlCommand::CreateMigration(q) => QueryExpr::CreateMigration(q),
1733            SqlCommand::ApplyMigration(q) => QueryExpr::ApplyMigration(q),
1734            SqlCommand::RollbackMigration(q) => QueryExpr::RollbackMigration(q),
1735            SqlCommand::ExplainMigration(q) => QueryExpr::ExplainMigration(q),
1736        }
1737    }
1738
1739    pub fn into_statement(self) -> SqlStatement {
1740        match self {
1741            SqlCommand::Select(query) => SqlStatement::Query(SqlQuery::Select(query)),
1742            SqlCommand::Join(query) => SqlStatement::Query(SqlQuery::Join(query)),
1743            SqlCommand::Insert(query) => SqlStatement::Mutation(SqlMutation::Insert(query)),
1744            SqlCommand::Update(query) => SqlStatement::Mutation(SqlMutation::Update(query)),
1745            SqlCommand::Delete(query) => SqlStatement::Mutation(SqlMutation::Delete(query)),
1746            SqlCommand::ExplainAlter(query) => {
1747                SqlStatement::Schema(SqlSchemaCommand::ExplainAlter(query))
1748            }
1749            SqlCommand::CreateTable(query) => {
1750                SqlStatement::Schema(SqlSchemaCommand::CreateTable(query))
1751            }
1752            SqlCommand::CreateCollection(query) => {
1753                SqlStatement::Schema(SqlSchemaCommand::CreateCollection(query))
1754            }
1755            SqlCommand::CreateVector(query) => {
1756                SqlStatement::Schema(SqlSchemaCommand::CreateVector(query))
1757            }
1758            SqlCommand::DropTable(query) => {
1759                SqlStatement::Schema(SqlSchemaCommand::DropTable(query))
1760            }
1761            SqlCommand::DropGraph(query) => {
1762                SqlStatement::Schema(SqlSchemaCommand::DropGraph(query))
1763            }
1764            SqlCommand::DropVector(query) => {
1765                SqlStatement::Schema(SqlSchemaCommand::DropVector(query))
1766            }
1767            SqlCommand::DropDocument(query) => {
1768                SqlStatement::Schema(SqlSchemaCommand::DropDocument(query))
1769            }
1770            SqlCommand::DropKv(query) => SqlStatement::Schema(SqlSchemaCommand::DropKv(query)),
1771            SqlCommand::DropCollection(query) => {
1772                SqlStatement::Schema(SqlSchemaCommand::DropCollection(query))
1773            }
1774            SqlCommand::Truncate(query) => SqlStatement::Schema(SqlSchemaCommand::Truncate(query)),
1775            SqlCommand::AlterTable(query) => {
1776                SqlStatement::Schema(SqlSchemaCommand::AlterTable(query))
1777            }
1778            SqlCommand::CreateVcsRef(query) => {
1779                SqlStatement::Schema(SqlSchemaCommand::CreateVcsRef(query))
1780            }
1781            SqlCommand::DropVcsRef(query) => {
1782                SqlStatement::Schema(SqlSchemaCommand::DropVcsRef(query))
1783            }
1784            SqlCommand::CreateIndex(query) => {
1785                SqlStatement::Schema(SqlSchemaCommand::CreateIndex(query))
1786            }
1787            SqlCommand::DropIndex(query) => {
1788                SqlStatement::Schema(SqlSchemaCommand::DropIndex(query))
1789            }
1790            SqlCommand::CreateTimeSeries(query) => {
1791                SqlStatement::Schema(SqlSchemaCommand::CreateTimeSeries(query))
1792            }
1793            SqlCommand::CreateMetric(query) => {
1794                SqlStatement::Schema(SqlSchemaCommand::CreateMetric(query))
1795            }
1796            SqlCommand::AlterMetric(query) => {
1797                SqlStatement::Schema(SqlSchemaCommand::AlterMetric(query))
1798            }
1799            SqlCommand::CreateSlo(query) => {
1800                SqlStatement::Schema(SqlSchemaCommand::CreateSlo(query))
1801            }
1802            SqlCommand::DropTimeSeries(query) => {
1803                SqlStatement::Schema(SqlSchemaCommand::DropTimeSeries(query))
1804            }
1805            SqlCommand::CreateQueue(query) => {
1806                SqlStatement::Schema(SqlSchemaCommand::CreateQueue(query))
1807            }
1808            SqlCommand::AlterQueue(query) => {
1809                SqlStatement::Schema(SqlSchemaCommand::AlterQueue(query))
1810            }
1811            SqlCommand::DropQueue(query) => {
1812                SqlStatement::Schema(SqlSchemaCommand::DropQueue(query))
1813            }
1814            SqlCommand::CreateTree(query) => {
1815                SqlStatement::Schema(SqlSchemaCommand::CreateTree(query))
1816            }
1817            SqlCommand::DropTree(query) => SqlStatement::Schema(SqlSchemaCommand::DropTree(query)),
1818            SqlCommand::Probabilistic(command) => {
1819                SqlStatement::Schema(SqlSchemaCommand::Probabilistic(command))
1820            }
1821            SqlCommand::SetConfig { key, value } => {
1822                SqlStatement::Admin(SqlAdminCommand::SetConfig { key, value })
1823            }
1824            SqlCommand::ShowConfig { prefix, as_json } => {
1825                SqlStatement::Admin(SqlAdminCommand::ShowConfig { prefix, as_json })
1826            }
1827            SqlCommand::SetSecret { key, value } => {
1828                SqlStatement::Admin(SqlAdminCommand::SetSecret { key, value })
1829            }
1830            SqlCommand::DeleteSecret { key } => {
1831                SqlStatement::Admin(SqlAdminCommand::DeleteSecret { key })
1832            }
1833            SqlCommand::ShowSecrets { prefix } => {
1834                SqlStatement::Admin(SqlAdminCommand::ShowSecrets { prefix })
1835            }
1836            SqlCommand::SetKv { key, value } => {
1837                SqlStatement::Admin(SqlAdminCommand::SetKv { key, value })
1838            }
1839            SqlCommand::DeleteKv { key } => SqlStatement::Admin(SqlAdminCommand::DeleteKv { key }),
1840            SqlCommand::SetTenant(value) => SqlStatement::Admin(SqlAdminCommand::SetTenant(value)),
1841            SqlCommand::ShowTenant => SqlStatement::Admin(SqlAdminCommand::ShowTenant),
1842            SqlCommand::TransactionControl(ctl) => {
1843                SqlStatement::Admin(SqlAdminCommand::TransactionControl(ctl))
1844            }
1845            SqlCommand::Maintenance(cmd) => SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)),
1846            SqlCommand::Vcs(cmd) => SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)),
1847            SqlCommand::CreateSchema(q) => SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)),
1848            SqlCommand::DropSchema(q) => SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)),
1849            SqlCommand::CreateSequence(q) => {
1850                SqlStatement::Schema(SqlSchemaCommand::CreateSequence(q))
1851            }
1852            SqlCommand::DropSequence(q) => SqlStatement::Schema(SqlSchemaCommand::DropSequence(q)),
1853            SqlCommand::CopyFrom(q) => SqlStatement::Schema(SqlSchemaCommand::CopyFrom(q)),
1854            SqlCommand::CreateView(q) => SqlStatement::Schema(SqlSchemaCommand::CreateView(q)),
1855            SqlCommand::DropView(q) => SqlStatement::Schema(SqlSchemaCommand::DropView(q)),
1856            SqlCommand::RefreshMaterializedView(q) => {
1857                SqlStatement::Schema(SqlSchemaCommand::RefreshMaterializedView(q))
1858            }
1859            SqlCommand::CreatePolicy(q) => SqlStatement::Schema(SqlSchemaCommand::CreatePolicy(q)),
1860            SqlCommand::DropPolicy(q) => SqlStatement::Schema(SqlSchemaCommand::DropPolicy(q)),
1861            SqlCommand::CreateServer(q) => SqlStatement::Schema(SqlSchemaCommand::CreateServer(q)),
1862            SqlCommand::DropServer(q) => SqlStatement::Schema(SqlSchemaCommand::DropServer(q)),
1863            SqlCommand::CreateForeignTable(q) => {
1864                SqlStatement::Schema(SqlSchemaCommand::CreateForeignTable(q))
1865            }
1866            SqlCommand::DropForeignTable(q) => {
1867                SqlStatement::Schema(SqlSchemaCommand::DropForeignTable(q))
1868            }
1869            SqlCommand::Grant(s) => SqlStatement::Admin(SqlAdminCommand::Grant(s)),
1870            SqlCommand::Revoke(s) => SqlStatement::Admin(SqlAdminCommand::Revoke(s)),
1871            SqlCommand::AlterUser(s) => SqlStatement::Admin(SqlAdminCommand::AlterUser(s)),
1872            SqlCommand::CreateUser(s) => SqlStatement::Admin(SqlAdminCommand::CreateUser(s)),
1873            SqlCommand::IamPolicy(e) => SqlStatement::Admin(SqlAdminCommand::IamPolicy(e)),
1874            SqlCommand::CreateMigration(q) => {
1875                SqlStatement::Schema(SqlSchemaCommand::CreateMigration(q))
1876            }
1877            SqlCommand::ApplyMigration(q) => {
1878                SqlStatement::Schema(SqlSchemaCommand::ApplyMigration(q))
1879            }
1880            SqlCommand::RollbackMigration(q) => {
1881                SqlStatement::Schema(SqlSchemaCommand::RollbackMigration(q))
1882            }
1883            SqlCommand::ExplainMigration(q) => {
1884                SqlStatement::Schema(SqlSchemaCommand::ExplainMigration(q))
1885            }
1886        }
1887    }
1888}
1889
1890impl<'a> Parser<'a> {
1891    fn parse_events_command(&mut self) -> Result<QueryExpr, ParseError> {
1892        self.expect_ident()?; // EVENTS
1893        if self.consume_ident_ci("STATUS")? {
1894            let mut query = TableQuery::new("red.subscriptions");
1895            let collection = match self.peek().clone() {
1896                Token::Ident(name) => {
1897                    self.advance()?;
1898                    Some(name)
1899                }
1900                Token::String(name) => {
1901                    self.advance()?;
1902                    Some(name)
1903                }
1904                _ => None,
1905            };
1906            self.parse_table_clauses(&mut query)?;
1907            if let Some(collection) = collection {
1908                let filter = Filter::compare(
1909                    FieldRef::column("red.subscriptions", "collection"),
1910                    CompareOp::Eq,
1911                    Value::text(collection),
1912                );
1913                let expr = filter_to_expr(&filter);
1914                query.where_expr = Some(match query.where_expr.take() {
1915                    Some(existing) => Expr::binop(BinOp::And, existing, expr),
1916                    None => expr,
1917                });
1918                query.filter = Some(match query.filter.take() {
1919                    Some(existing) => existing.and(filter),
1920                    None => filter,
1921                });
1922            }
1923            return Ok(QueryExpr::Table(query));
1924        }
1925
1926        if !self.consume_ident_ci("BACKFILL")? {
1927            return Err(ParseError::expected(
1928                vec!["BACKFILL", "STATUS"],
1929                self.peek(),
1930                self.position(),
1931            ));
1932        }
1933
1934        if self.consume_ident_ci("STATUS")? {
1935            let collection = self.expect_ident()?;
1936            return Ok(QueryExpr::EventsBackfillStatus { collection });
1937        }
1938
1939        let collection = self.expect_ident()?;
1940        let where_filter = if self.consume(&Token::Where)? {
1941            let mut parts = Vec::new();
1942            while !self.check(&Token::Eof) && !self.check(&Token::To) {
1943                parts.push(self.peek().to_string());
1944                self.advance()?;
1945            }
1946            if parts.is_empty() {
1947                return Err(ParseError::expected(
1948                    vec!["predicate"],
1949                    self.peek(),
1950                    self.position(),
1951                ));
1952            }
1953            Some(parts.join(" "))
1954        } else {
1955            None
1956        };
1957
1958        self.expect(Token::To)?;
1959        let target_queue = self.expect_ident()?;
1960        let limit = if self.consume(&Token::Limit)? {
1961            Some(self.parse_positive_integer("LIMIT")? as u64)
1962        } else {
1963            None
1964        };
1965
1966        Ok(QueryExpr::EventsBackfill(EventsBackfillQuery {
1967            collection,
1968            where_filter,
1969            target_queue,
1970            limit,
1971        }))
1972    }
1973
1974    /// Parse an optional `OPTIONS (key 'value', key2 'value2', ...)` clause
1975    /// used by Phase 3.2 FDW DDL statements. Returns an empty vec when the
1976    /// clause is absent. Values are always single-quoted string literals —
1977    /// consistent with PG's generic-options model.
1978    pub(crate) fn parse_fdw_options_clause(&mut self) -> Result<Vec<(String, String)>, ParseError> {
1979        if !self.consume(&Token::Options)? {
1980            return Ok(Vec::new());
1981        }
1982        self.expect(Token::LParen)?;
1983        let mut out: Vec<(String, String)> = Vec::new();
1984        loop {
1985            // Option keys frequently collide with reserved words
1986            // (`path`, `format`, `delimiter`, `header`, …) — accept
1987            // the keyword form and lowercase it so downstream
1988            // option-name matching stays case-insensitive.
1989            let was_ident = matches!(self.peek(), Token::Ident(_));
1990            let raw = self.expect_ident_or_keyword()?;
1991            let key = if was_ident {
1992                raw
1993            } else {
1994                raw.to_ascii_lowercase()
1995            };
1996            // Value is a single-quoted string literal.
1997            let value = self.parse_string()?;
1998            out.push((key, value));
1999            if !self.consume(&Token::Comma)? {
2000                break;
2001            }
2002        }
2003        self.expect(Token::RParen)?;
2004        Ok(out)
2005    }
2006
2007    /// Parse any top-level frontend statement through a single shared surface.
2008    pub fn parse_frontend_statement(&mut self) -> Result<FrontendStatement, ParseError> {
2009        self.enter_depth()?;
2010        let result = self.parse_frontend_statement_inner();
2011        self.exit_depth();
2012        result
2013    }
2014
2015    fn parse_frontend_statement_inner(&mut self) -> Result<FrontendStatement, ParseError> {
2016        match self.peek() {
2017            Token::Select => match self.parse_select_query()? {
2018                QueryExpr::Table(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
2019                    SqlQuery::Select(query),
2020                ))),
2021                QueryExpr::Join(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
2022                    SqlQuery::Join(query),
2023                ))),
2024                QueryExpr::QueueSelect(query) => Ok(FrontendStatement::QueueSelect(query)),
2025                other => Err(ParseError::new(
2026                    format!("internal: SELECT produced unexpected query kind {other:?}"),
2027                    self.position(),
2028                )),
2029            },
2030            Token::From
2031            | Token::Insert
2032            | Token::Update
2033            | Token::Truncate
2034            | Token::Create
2035            | Token::Drop
2036            | Token::Alter
2037            | Token::Set
2038            | Token::Begin
2039            | Token::Commit
2040            | Token::Rollback
2041            | Token::Savepoint
2042            | Token::Release
2043            | Token::Start
2044            | Token::Vacuum
2045            | Token::Analyze
2046            | Token::Copy
2047            | Token::Refresh => self.parse_sql_statement().map(FrontendStatement::Sql),
2048            Token::Explain => {
2049                if matches!(
2050                    self.peek_next()?,
2051                    Token::Ident(name) if name.eq_ignore_ascii_case("ASK")
2052                ) {
2053                    match self.parse_explain_ask_query()? {
2054                        QueryExpr::Ask(query) => Ok(FrontendStatement::Ask(query)),
2055                        other => Err(ParseError::new(
2056                            format!(
2057                                "internal: EXPLAIN ASK produced unexpected query kind {other:?}"
2058                            ),
2059                            self.position(),
2060                        )),
2061                    }
2062                } else {
2063                    self.parse_sql_statement().map(FrontendStatement::Sql)
2064                }
2065            }
2066            Token::Ident(name) if name.eq_ignore_ascii_case("SHOW") => {
2067                self.parse_sql_statement().map(FrontendStatement::Sql)
2068            }
2069            Token::Ident(name) if name.eq_ignore_ascii_case("RESET") => {
2070                self.parse_sql_statement().map(FrontendStatement::Sql)
2071            }
2072            Token::Ident(name)
2073                if name.eq_ignore_ascii_case("RANK")
2074                    || name.eq_ignore_ascii_case("APPROX")
2075                    || name.eq_ignore_ascii_case("APPROXIMATE")
2076                    || name.eq_ignore_ascii_case("ZRANK")
2077                    || name.eq_ignore_ascii_case("ZRANGE") =>
2078            {
2079                self.parse_ranking_read().map(FrontendStatement::Ranking)
2080            }
2081            Token::Desc => self.parse_sql_statement().map(FrontendStatement::Sql),
2082            Token::Ident(name)
2083                if name.eq_ignore_ascii_case("DESCRIBE") || name.eq_ignore_ascii_case("DESC") =>
2084            {
2085                self.parse_sql_statement().map(FrontendStatement::Sql)
2086            }
2087            Token::Ident(name)
2088                if name.eq_ignore_ascii_case("GRANT")
2089                    || name.eq_ignore_ascii_case("REVOKE")
2090                    || name.eq_ignore_ascii_case("SIMULATE")
2091                    || name.eq_ignore_ascii_case("LINT")
2092                    || name.eq_ignore_ascii_case("MIGRATE")
2093                    || name.eq_ignore_ascii_case("APPLY") =>
2094            {
2095                self.parse_sql_statement().map(FrontendStatement::Sql)
2096            }
2097            Token::Ident(name) if name.eq_ignore_ascii_case("WATCH") => {
2098                self.advance()?;
2099                if matches!(
2100                    self.peek(),
2101                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2102                ) {
2103                    match self.parse_config_watch_after_watch()? {
2104                        QueryExpr::ConfigCommand(command) => {
2105                            Ok(FrontendStatement::ConfigCommand(command))
2106                        }
2107                        other => Err(ParseError::new(
2108                            format!(
2109                                "internal: WATCH CONFIG produced unexpected query kind {other:?}"
2110                            ),
2111                            self.position(),
2112                        )),
2113                    }
2114                } else if matches!(
2115                    self.peek(),
2116                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2117                ) {
2118                    match self.parse_vault_watch_after_watch()? {
2119                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2120                        other => Err(ParseError::new(
2121                            format!(
2122                                "internal: WATCH VAULT produced unexpected query kind {other:?}"
2123                            ),
2124                            self.position(),
2125                        )),
2126                    }
2127                } else {
2128                    match self.parse_kv_watch(reddb_types::catalog::CollectionModel::Kv)? {
2129                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2130                        other => Err(ParseError::new(
2131                            format!("internal: WATCH produced unexpected query kind {other:?}"),
2132                            self.position(),
2133                        )),
2134                    }
2135                }
2136            }
2137            Token::List => {
2138                self.advance()?;
2139                if matches!(
2140                    self.peek(),
2141                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2142                ) {
2143                    match self.parse_config_list_after_list()? {
2144                        QueryExpr::ConfigCommand(command) => {
2145                            Ok(FrontendStatement::ConfigCommand(command))
2146                        }
2147                        other => Err(ParseError::new(
2148                            format!(
2149                                "internal: LIST CONFIG produced unexpected query kind {other:?}"
2150                            ),
2151                            self.position(),
2152                        )),
2153                    }
2154                } else if matches!(self.peek(), Token::Kv) {
2155                    match self.parse_kv_list_after_list()? {
2156                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2157                        other => Err(ParseError::new(
2158                            format!("internal: LIST KV produced unexpected query kind {other:?}"),
2159                            self.position(),
2160                        )),
2161                    }
2162                } else if matches!(
2163                    self.peek(),
2164                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2165                ) {
2166                    match self.parse_vault_list_after_list()? {
2167                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2168                        other => Err(ParseError::new(
2169                            format!(
2170                                "internal: LIST VAULT produced unexpected query kind {other:?}"
2171                            ),
2172                            self.position(),
2173                        )),
2174                    }
2175                } else {
2176                    Err(ParseError::expected(
2177                        vec!["CONFIG", "KV", "VAULT"],
2178                        self.peek(),
2179                        self.position(),
2180                    ))
2181                }
2182            }
2183            Token::Ident(name) if name.eq_ignore_ascii_case("LIST") => {
2184                self.advance()?;
2185                if matches!(
2186                    self.peek(),
2187                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2188                ) {
2189                    match self.parse_config_list_after_list()? {
2190                        QueryExpr::ConfigCommand(command) => {
2191                            Ok(FrontendStatement::ConfigCommand(command))
2192                        }
2193                        other => Err(ParseError::new(
2194                            format!(
2195                                "internal: LIST CONFIG produced unexpected query kind {other:?}"
2196                            ),
2197                            self.position(),
2198                        )),
2199                    }
2200                } else if matches!(self.peek(), Token::Kv) {
2201                    match self.parse_kv_list_after_list()? {
2202                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2203                        other => Err(ParseError::new(
2204                            format!("internal: LIST KV produced unexpected query kind {other:?}"),
2205                            self.position(),
2206                        )),
2207                    }
2208                } else if matches!(
2209                    self.peek(),
2210                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2211                ) {
2212                    match self.parse_vault_list_after_list()? {
2213                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2214                        other => Err(ParseError::new(
2215                            format!(
2216                                "internal: LIST VAULT produced unexpected query kind {other:?}"
2217                            ),
2218                            self.position(),
2219                        )),
2220                    }
2221                } else {
2222                    Err(ParseError::expected(
2223                        vec!["CONFIG", "KV", "VAULT"],
2224                        self.peek(),
2225                        self.position(),
2226                    ))
2227                }
2228            }
2229            Token::Ident(name) if name.eq_ignore_ascii_case("INVALIDATE") => {
2230                if matches!(
2231                    self.peek_next()?,
2232                    Token::Ident(next) if next.eq_ignore_ascii_case("CONFIG")
2233                ) {
2234                    match self.parse_config_command()? {
2235                        QueryExpr::ConfigCommand(command) => {
2236                            Ok(FrontendStatement::ConfigCommand(command))
2237                        }
2238                        other => Err(ParseError::new(
2239                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2240                            self.position(),
2241                        )),
2242                    }
2243                } else {
2244                    self.advance()?;
2245                    match self.parse_kv_invalidate_tags_after_invalidate()? {
2246                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2247                        other => Err(ParseError::new(
2248                            format!(
2249                                "internal: INVALIDATE produced unexpected query kind {other:?}"
2250                            ),
2251                            self.position(),
2252                        )),
2253                    }
2254                }
2255            }
2256            Token::Attach | Token::Detach => self.parse_sql_statement().map(FrontendStatement::Sql),
2257            Token::Match => match self.parse_match_query()? {
2258                QueryExpr::Graph(query) => Ok(FrontendStatement::Graph(query)),
2259                other => Err(ParseError::new(
2260                    format!("internal: MATCH produced unexpected query kind {other:?}"),
2261                    self.position(),
2262                )),
2263            },
2264            Token::Path => match self.parse_path_query()? {
2265                QueryExpr::Path(query) => Ok(FrontendStatement::Path(query)),
2266                other => Err(ParseError::new(
2267                    format!("internal: PATH produced unexpected query kind {other:?}"),
2268                    self.position(),
2269                )),
2270            },
2271            Token::Vector => match self.parse_vector_query()? {
2272                QueryExpr::Vector(query) => Ok(FrontendStatement::Vector(query)),
2273                other => Err(ParseError::new(
2274                    format!("internal: VECTOR produced unexpected query kind {other:?}"),
2275                    self.position(),
2276                )),
2277            },
2278            Token::Hybrid => match self.parse_hybrid_query()? {
2279                QueryExpr::Hybrid(query) => Ok(FrontendStatement::Hybrid(query)),
2280                other => Err(ParseError::new(
2281                    format!("internal: HYBRID produced unexpected query kind {other:?}"),
2282                    self.position(),
2283                )),
2284            },
2285            Token::Graph => match self.parse_graph_command()? {
2286                QueryExpr::GraphCommand(command) => Ok(FrontendStatement::GraphCommand(command)),
2287                other => Err(ParseError::new(
2288                    format!("internal: GRAPH produced unexpected query kind {other:?}"),
2289                    self.position(),
2290                )),
2291            },
2292            Token::Search => match self.parse_search_command()? {
2293                QueryExpr::SearchCommand(command) => Ok(FrontendStatement::Search(command)),
2294                other => Err(ParseError::new(
2295                    format!("internal: SEARCH produced unexpected query kind {other:?}"),
2296                    self.position(),
2297                )),
2298            },
2299            Token::Ident(name) if name.eq_ignore_ascii_case("ASK") => {
2300                match self.parse_ask_query()? {
2301                    QueryExpr::Ask(query) => Ok(FrontendStatement::Ask(query)),
2302                    other => Err(ParseError::new(
2303                        format!("internal: ASK produced unexpected query kind {other:?}"),
2304                        self.position(),
2305                    )),
2306                }
2307            }
2308            Token::Ident(name) if name.eq_ignore_ascii_case("UNSEAL") => {
2309                match self.parse_unseal_vault_command()? {
2310                    QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2311                    other => Err(ParseError::new(
2312                        format!("internal: UNSEAL VAULT produced unexpected query kind {other:?}"),
2313                        self.position(),
2314                    )),
2315                }
2316            }
2317            Token::Queue => match self.parse_queue_command()? {
2318                QueryExpr::QueueCommand(command) => Ok(FrontendStatement::QueueCommand(command)),
2319                other => Err(ParseError::new(
2320                    format!("internal: QUEUE produced unexpected query kind {other:?}"),
2321                    self.position(),
2322                )),
2323            },
2324            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => {
2325                match self.parse_events_command()? {
2326                    QueryExpr::Table(query) => Ok(FrontendStatement::Sql(SqlStatement::Query(
2327                        SqlQuery::Select(query),
2328                    ))),
2329                    QueryExpr::EventsBackfill(query) => {
2330                        Ok(FrontendStatement::EventsBackfill(query))
2331                    }
2332                    QueryExpr::EventsBackfillStatus { collection } => {
2333                        Ok(FrontendStatement::EventsBackfillStatus { collection })
2334                    }
2335                    other => Err(ParseError::new(
2336                        format!("internal: EVENTS produced unexpected query kind {other:?}"),
2337                        self.position(),
2338                    )),
2339                }
2340            }
2341            Token::Kv => match self.parse_kv_command()? {
2342                QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2343                other => Err(ParseError::new(
2344                    format!("internal: KV produced unexpected query kind {other:?}"),
2345                    self.position(),
2346                )),
2347            },
2348            Token::Delete => {
2349                if matches!(
2350                    self.peek_next()?,
2351                    Token::Ident(name) if name.eq_ignore_ascii_case("CONFIG")
2352                ) {
2353                    match self.parse_config_command()? {
2354                        QueryExpr::ConfigCommand(command) => {
2355                            Ok(FrontendStatement::ConfigCommand(command))
2356                        }
2357                        other => Err(ParseError::new(
2358                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2359                            self.position(),
2360                        )),
2361                    }
2362                } else if matches!(
2363                    self.peek_next()?,
2364                    Token::Ident(name) if name.eq_ignore_ascii_case("VAULT")
2365                ) {
2366                    match self.parse_vault_lifecycle_command()? {
2367                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2368                        other => Err(ParseError::new(
2369                            format!("internal: VAULT produced unexpected query kind {other:?}"),
2370                            self.position(),
2371                        )),
2372                    }
2373                } else {
2374                    self.parse_sql_statement().map(FrontendStatement::Sql)
2375                }
2376            }
2377            Token::Add => match self.parse_config_command()? {
2378                QueryExpr::ConfigCommand(command) => Ok(FrontendStatement::ConfigCommand(command)),
2379                other => Err(ParseError::new(
2380                    format!("internal: CONFIG produced unexpected query kind {other:?}"),
2381                    self.position(),
2382                )),
2383            },
2384            Token::Purge => match self.parse_vault_lifecycle_command()? {
2385                QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2386                other => Err(ParseError::new(
2387                    format!("internal: VAULT produced unexpected query kind {other:?}"),
2388                    self.position(),
2389                )),
2390            },
2391            Token::Ident(name)
2392                if name.eq_ignore_ascii_case("PUT")
2393                    || name.eq_ignore_ascii_case("GET")
2394                    || name.eq_ignore_ascii_case("RESOLVE")
2395                    || name.eq_ignore_ascii_case("ROTATE")
2396                    || name.eq_ignore_ascii_case("HISTORY")
2397                    || name.eq_ignore_ascii_case("PURGE")
2398                    || name.eq_ignore_ascii_case("INCR")
2399                    || name.eq_ignore_ascii_case("DECR")
2400                    || name.eq_ignore_ascii_case("INVALIDATE") =>
2401            {
2402                // `RESOLVE CONFLICT …` is the VCS working-set verb, not config
2403                // resolution — route it to the SQL/VCS command surface.
2404                if name.eq_ignore_ascii_case("RESOLVE")
2405                    && matches!(self.peek_next()?, Token::Ident(next) if next.eq_ignore_ascii_case("CONFLICT"))
2406                {
2407                    self.parse_sql_command()
2408                        .map(SqlCommand::into_statement)
2409                        .map(FrontendStatement::Sql)
2410                } else if matches!(
2411                    self.peek_next()?,
2412                    Token::Ident(next) if next.eq_ignore_ascii_case("VAULT")
2413                ) {
2414                    match self.parse_vault_lifecycle_command()? {
2415                        QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2416                        other => Err(ParseError::new(
2417                            format!("internal: VAULT produced unexpected query kind {other:?}"),
2418                            self.position(),
2419                        )),
2420                    }
2421                } else {
2422                    match self.parse_config_command()? {
2423                        QueryExpr::ConfigCommand(command) => {
2424                            Ok(FrontendStatement::ConfigCommand(command))
2425                        }
2426                        other => Err(ParseError::new(
2427                            format!("internal: CONFIG produced unexpected query kind {other:?}"),
2428                            self.position(),
2429                        )),
2430                    }
2431                }
2432            }
2433            Token::Ident(name) if name.eq_ignore_ascii_case("VAULT") => {
2434                match self.parse_vault_command()? {
2435                    QueryExpr::KvCommand(command) => Ok(FrontendStatement::KvCommand(command)),
2436                    other => Err(ParseError::new(
2437                        format!("internal: VAULT produced unexpected query kind {other:?}"),
2438                        self.position(),
2439                    )),
2440                }
2441            }
2442            Token::Tree => match self.parse_tree_command()? {
2443                QueryExpr::TreeCommand(command) => Ok(FrontendStatement::TreeCommand(command)),
2444                other => Err(ParseError::new(
2445                    format!("internal: TREE produced unexpected query kind {other:?}"),
2446                    self.position(),
2447                )),
2448            },
2449            Token::Ident(name) if name.eq_ignore_ascii_case("HLL") => {
2450                match self.parse_hll_command()? {
2451                    QueryExpr::ProbabilisticCommand(command) => {
2452                        Ok(FrontendStatement::ProbabilisticCommand(command))
2453                    }
2454                    other => Err(ParseError::new(
2455                        format!("internal: HLL produced unexpected query kind {other:?}"),
2456                        self.position(),
2457                    )),
2458                }
2459            }
2460            Token::Ident(name) if name.eq_ignore_ascii_case("SKETCH") => {
2461                match self.parse_sketch_command()? {
2462                    QueryExpr::ProbabilisticCommand(command) => {
2463                        Ok(FrontendStatement::ProbabilisticCommand(command))
2464                    }
2465                    other => Err(ParseError::new(
2466                        format!("internal: SKETCH produced unexpected query kind {other:?}"),
2467                        self.position(),
2468                    )),
2469                }
2470            }
2471            Token::Ident(name) if name.eq_ignore_ascii_case("FILTER") => {
2472                match self.parse_filter_command()? {
2473                    QueryExpr::ProbabilisticCommand(command) => {
2474                        Ok(FrontendStatement::ProbabilisticCommand(command))
2475                    }
2476                    other => Err(ParseError::new(
2477                        format!("internal: FILTER produced unexpected query kind {other:?}"),
2478                        self.position(),
2479                    )),
2480                }
2481            }
2482            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => self
2483                .parse_sql_command()
2484                .map(SqlCommand::into_statement)
2485                .map(FrontendStatement::Sql),
2486            Token::Ident(name)
2487                if is_vcs_command_head(name) || name.eq_ignore_ascii_case("RESET") =>
2488            {
2489                self.parse_sql_command()
2490                    .map(SqlCommand::into_statement)
2491                    .map(FrontendStatement::Sql)
2492            }
2493            other => Err(ParseError::expected(
2494                vec![
2495                    "SELECT",
2496                    "MATCH",
2497                    "PATH",
2498                    "FROM",
2499                    "VECTOR",
2500                    "HYBRID",
2501                    "INSERT",
2502                    "UPDATE",
2503                    "DELETE",
2504                    "TRUNCATE",
2505                    "CREATE",
2506                    "DROP",
2507                    "ALTER",
2508                    "GRAPH",
2509                    "SEARCH",
2510                    "ASK",
2511                    "QUEUE",
2512                    "EVENTS",
2513                    "KV",
2514                    "HLL",
2515                    "TREE",
2516                    "SKETCH",
2517                    "FILTER",
2518                    "SET",
2519                    "SHOW",
2520                    "RESET",
2521                    "CHECKPOINT",
2522                    "CHECKOUT",
2523                    "MERGE",
2524                    "CHERRY",
2525                    "REVERT",
2526                    "RESOLVE",
2527                    "DESCRIBE",
2528                    "DESC",
2529                    "RANK",
2530                    "ZRANK",
2531                    "ZRANGE",
2532                ],
2533                other,
2534                self.position(),
2535            )),
2536        }
2537    }
2538
2539    fn parse_ranking_read(&mut self) -> Result<QueryExpr, ParseError> {
2540        let head = self.expect_ident()?;
2541        if head.eq_ignore_ascii_case("RANK") {
2542            return self.parse_rank_after_rank(false);
2543        }
2544        if head.eq_ignore_ascii_case("APPROX") || head.eq_ignore_ascii_case("APPROXIMATE") {
2545            if !self.consume_ident_ci("RANK")? {
2546                return Err(ParseError::expected(
2547                    vec!["RANK"],
2548                    self.peek(),
2549                    self.position(),
2550                ));
2551            }
2552            return self.parse_rank_after_rank(true);
2553        }
2554        if head.eq_ignore_ascii_case("ZRANK") {
2555            return self.parse_zrank();
2556        }
2557        if head.eq_ignore_ascii_case("ZRANGE") {
2558            return self.parse_zrange();
2559        }
2560        Err(ParseError::expected(
2561            vec!["RANK", "APPROX RANK", "ZRANK", "ZRANGE"],
2562            self.peek(),
2563            self.position(),
2564        ))
2565    }
2566
2567    fn parse_rank_after_rank(&mut self, approximate: bool) -> Result<QueryExpr, ParseError> {
2568        if self.consume(&Token::Of)? {
2569            let entity_id = self.parse_u64_slot("rank entity id")?;
2570            self.expect(Token::In)?;
2571            let ranking = self.expect_ident()?;
2572            let query = RankOfQuery { ranking, entity_id };
2573            return Ok(if approximate {
2574                QueryExpr::ApproxRankOf(query)
2575            } else {
2576                QueryExpr::RankOf(query)
2577            });
2578        }
2579
2580        if !approximate && self.consume(&Token::Range)? {
2581            let lo = self.parse_positive_u64_slot("rank range lower bound")?;
2582            self.expect(Token::To)?;
2583            let hi = self.parse_positive_u64_slot("rank range upper bound")?;
2584            if hi < lo {
2585                return Err(ParseError::value_out_of_range(
2586                    "rank range upper bound",
2587                    "must be greater than or equal to the lower bound",
2588                    self.position(),
2589                ));
2590            }
2591            self.expect(Token::In)?;
2592            let ranking = self.expect_ident()?;
2593            return Ok(QueryExpr::RankRange(RankRangeQuery { ranking, lo, hi }));
2594        }
2595
2596        Err(ParseError::expected(
2597            if approximate {
2598                vec!["OF"]
2599            } else {
2600                vec!["OF", "RANGE"]
2601            },
2602            self.peek(),
2603            self.position(),
2604        ))
2605    }
2606
2607    fn parse_zrank(&mut self) -> Result<QueryExpr, ParseError> {
2608        let ranking = self.expect_ident()?;
2609        let entity_id = self.parse_u64_slot("ZRANK entity id")?;
2610        Ok(QueryExpr::RankOf(RankOfQuery { ranking, entity_id }))
2611    }
2612
2613    fn parse_zrange(&mut self) -> Result<QueryExpr, ParseError> {
2614        let ranking = self.expect_ident()?;
2615        let start = self.parse_u64_slot("ZRANGE start")?;
2616        let stop = self.parse_u64_slot("ZRANGE stop")?;
2617        if stop < start {
2618            return Err(ParseError::value_out_of_range(
2619                "ZRANGE stop",
2620                "must be greater than or equal to start",
2621                self.position(),
2622            ));
2623        }
2624        let _with_scores = self.consume_ident_ci("WITHSCORES")?;
2625        Ok(QueryExpr::RankRange(RankRangeQuery {
2626            ranking,
2627            lo: start + 1,
2628            hi: stop + 1,
2629        }))
2630    }
2631
2632    fn parse_positive_u64_slot(&mut self, field: &'static str) -> Result<u64, ParseError> {
2633        let value = self.parse_u64_slot(field)?;
2634        if value == 0 {
2635            return Err(ParseError::value_out_of_range(
2636                field,
2637                "must be a positive integer",
2638                self.position(),
2639            ));
2640        }
2641        Ok(value)
2642    }
2643
2644    fn parse_u64_slot(&mut self, field: &'static str) -> Result<u64, ParseError> {
2645        let pos = self.position();
2646        if matches!(self.peek(), Token::Minus | Token::Dash) {
2647            return Err(ParseError::value_out_of_range(
2648                field,
2649                "must be an unsigned integer",
2650                pos,
2651            ));
2652        }
2653        let raw = self.parse_integer()?;
2654        u64::try_from(raw)
2655            .map_err(|_| ParseError::value_out_of_range(field, "must be an unsigned integer", pos))
2656    }
2657
2658    #[inline(never)]
2659    fn parse_vcs_atom(&mut self, field: &'static str) -> Result<String, ParseError> {
2660        match self.peek().clone() {
2661            Token::Ident(value) | Token::String(value) => {
2662                self.advance()?;
2663                Ok(value)
2664            }
2665            Token::Integer(value) => {
2666                self.advance()?;
2667                Ok(value.to_string())
2668            }
2669            other => Err(ParseError::expected(vec![field], &other, self.position())),
2670        }
2671    }
2672
2673    #[inline(never)]
2674    fn parse_vcs_command(&mut self) -> Result<VcsCommand, ParseError> {
2675        let head = self.expect_ident()?;
2676        if head.eq_ignore_ascii_case("CHECKPOINT") {
2677            let message = self.parse_string()?;
2678            let author = if self.consume_ident_ci("AUTHOR")? {
2679                Some(self.parse_string()?)
2680            } else {
2681                None
2682            };
2683            return Ok(VcsCommand::Checkpoint { message, author });
2684        }
2685        if head.eq_ignore_ascii_case("CHECKOUT") {
2686            return Ok(VcsCommand::Checkout {
2687                target: self.parse_vcs_atom("commitish")?,
2688            });
2689        }
2690        if head.eq_ignore_ascii_case("RESET") {
2691            let mode = if self.consume_ident_ci("HARD")? {
2692                VcsResetMode::Hard
2693            } else if self.consume_ident_ci("SOFT")? {
2694                VcsResetMode::Soft
2695            } else {
2696                // MIXED is the default; consume the optional explicit keyword.
2697                let _ = self.consume_ident_ci("MIXED")?;
2698                VcsResetMode::Mixed
2699            };
2700            self.expect(Token::To)?;
2701            return Ok(VcsCommand::Reset {
2702                mode,
2703                target: self.parse_vcs_atom("commitish")?,
2704            });
2705        }
2706        if head.eq_ignore_ascii_case("MERGE") {
2707            return Ok(VcsCommand::Merge {
2708                branch: self.parse_vcs_atom("branch")?,
2709            });
2710        }
2711        if head.eq_ignore_ascii_case("CHERRY") {
2712            if !self.consume_ident_ci("PICK")? {
2713                return Err(ParseError::expected(
2714                    vec!["PICK"],
2715                    self.peek(),
2716                    self.position(),
2717                ));
2718            }
2719            return Ok(VcsCommand::CherryPick {
2720                commit: self.parse_vcs_atom("commit")?,
2721            });
2722        }
2723        if head.eq_ignore_ascii_case("REVERT") {
2724            return Ok(VcsCommand::Revert {
2725                commit: self.parse_vcs_atom("commit")?,
2726            });
2727        }
2728        if head.eq_ignore_ascii_case("RESOLVE") {
2729            if !self.consume_ident_ci("CONFLICT")? {
2730                return Err(ParseError::expected(
2731                    vec!["CONFLICT"],
2732                    self.peek(),
2733                    self.position(),
2734                ));
2735            }
2736            let key = self.parse_string()?;
2737            if !self.consume(&Token::Using)? {
2738                return Err(ParseError::expected(
2739                    vec!["USING"],
2740                    self.peek(),
2741                    self.position(),
2742                ));
2743            }
2744            let resolution = if self.consume_ident_ci("OURS")? {
2745                VcsConflictResolution::Ours
2746            } else if self.consume_ident_ci("THEIRS")? {
2747                VcsConflictResolution::Theirs
2748            } else {
2749                return Err(ParseError::expected(
2750                    vec!["OURS", "THEIRS"],
2751                    self.peek(),
2752                    self.position(),
2753                ));
2754            };
2755            return Ok(VcsCommand::ResolveConflict { key, resolution });
2756        }
2757        Err(ParseError::expected(
2758            vec![
2759                "CHECKPOINT",
2760                "CHECKOUT",
2761                "RESET",
2762                "MERGE",
2763                "CHERRY",
2764                "REVERT",
2765                "RESOLVE",
2766            ],
2767            self.peek(),
2768            self.position(),
2769        ))
2770    }
2771
2772    /// Parse any SQL/RQL-style command into the canonical SQL frontend IR.
2773    pub fn parse_sql_statement(&mut self) -> Result<SqlStatement, ParseError> {
2774        self.parse_sql_command().map(SqlCommand::into_statement)
2775    }
2776
2777    fn parse_dotted_admin_path(&mut self, lowercase: bool) -> Result<String, ParseError> {
2778        let mut path = self.expect_ident()?;
2779        while self.consume(&Token::Dot)? {
2780            let next = self.expect_ident_or_keyword()?;
2781            path = format!("{path}.{next}");
2782        }
2783        Ok(if lowercase {
2784            path.to_ascii_lowercase()
2785        } else {
2786            path
2787        })
2788    }
2789
2790    fn normalize_secret_admin_path(path: String) -> String {
2791        if let Some(rest) = path.strip_prefix("red.secrets.") {
2792            format!("red.secret.{rest}")
2793        } else if path == "red.secrets" {
2794            "red.secret".to_string()
2795        } else {
2796            path
2797        }
2798    }
2799
2800    /// Parse any SQL/RQL-style command through a single frontend module.
2801    /// Parse a `CREATE ...` statement. Split out of
2802    /// [`parse_sql_command`] so its very large per-arm locals
2803    /// (every CREATE variant's query struct) live in their own
2804    /// stack frame instead of inflating the dispatcher's frame.
2805    /// `parse_sql_command` recurses (CREATE VIEW ... AS <stmt>,
2806    /// nested subqueries), so a fat dispatcher frame stacked on
2807    /// itself overflowed small (2 MiB) worker-thread stacks (#635).
2808    #[inline(never)]
2809    fn parse_create_command(&mut self) -> Result<SqlCommand, ParseError> {
2810        let pos = self.position();
2811        self.advance()?;
2812
2813        // CREATE [OR REPLACE] [MATERIALIZED] VIEW [IF NOT EXISTS] name AS <select>
2814        // Detect the VIEW path early so OR REPLACE / MATERIALIZED modifiers
2815        // don't collide with other CREATE variants (TABLE, INDEX, etc.).
2816        let mut or_replace = false;
2817        if self.consume(&Token::Or)? || self.consume_ident_ci("OR")? {
2818            let _ = self.consume_ident_ci("REPLACE")?;
2819            or_replace = true;
2820        }
2821        let materialized = self.consume(&Token::Materialized)?;
2822        if self.check(&Token::View) {
2823            self.advance()?;
2824            let if_not_exists = self.match_if_not_exists()?;
2825            let name = self.expect_ident()?;
2826            // Issue #584 slice 12 — `WITH RETENTION <duration>`
2827            // on CREATE MATERIALIZED VIEW. Parsed before `AS`
2828            // so the SELECT body parser cannot consume the
2829            // trailing `WITH` for its own (TTL / METADATA /
2830            // …) clauses. Persisted on the view definition;
2831            // the physical sweep against view-backing rows
2832            // activates with the slice-9 row-storage follow-up.
2833            let mut retention_duration_ms: Option<u64> = None;
2834            if self.check(&Token::With) {
2835                self.advance()?;
2836                if !self.consume(&Token::Retention)? && !self.consume_ident_ci("RETENTION")? {
2837                    return Err(ParseError::expected(
2838                        vec!["RETENTION"],
2839                        self.peek(),
2840                        self.position(),
2841                    ));
2842                }
2843                if !materialized {
2844                    return Err(ParseError::new(
2845                        "WITH RETENTION is only valid on \
2846                                 CREATE MATERIALIZED VIEW"
2847                            .to_string(),
2848                        self.position(),
2849                    ));
2850                }
2851                let value = self.parse_float()?;
2852                let unit_mult = self.parse_duration_unit()?;
2853                retention_duration_ms = Some((value * unit_mult).round() as u64);
2854            }
2855            // Accept `AS` — the lexer promotes it to `Token::As`
2856            // (keyword) but some paths still see it as an ident.
2857            if !self.consume(&Token::As)? && !self.consume_ident_ci("AS")? {
2858                return Err(ParseError::expected(
2859                    vec!["AS"],
2860                    self.peek(),
2861                    self.position(),
2862                ));
2863            }
2864            // Recursive parse of the body. Any QueryExpr that the
2865            // rest of the grammar accepts is valid (Select, Join, etc.).
2866            let body = self.parse_sql_command()?.into_query_expr();
2867            // Optional `REFRESH EVERY <duration>` clause on
2868            // materialized views (issue #583 slice 10). The
2869            // background scheduler reads this off the view
2870            // descriptor and ticks the view on its cadence.
2871            let mut refresh_every_ms: Option<u64> = None;
2872            if self.check(&Token::Refresh) {
2873                if !materialized {
2874                    return Err(ParseError::new(
2875                        "REFRESH EVERY is only valid on \
2876                                 CREATE MATERIALIZED VIEW"
2877                            .to_string(),
2878                        self.position(),
2879                    ));
2880                }
2881                self.advance()?;
2882                if !self.consume_ident_ci("EVERY")? {
2883                    return Err(ParseError::expected(
2884                        vec!["EVERY"],
2885                        self.peek(),
2886                        self.position(),
2887                    ));
2888                }
2889                let value = self.parse_float()?;
2890                let unit_mult = self.parse_duration_unit()?;
2891                refresh_every_ms = Some((value * unit_mult).round() as u64);
2892            }
2893            return Ok(SqlCommand::CreateView(CreateViewQuery {
2894                name,
2895                query: Box::new(body),
2896                materialized,
2897                if_not_exists,
2898                or_replace,
2899                refresh_every_ms,
2900                retention_duration_ms,
2901            }));
2902        }
2903        // If OR REPLACE / MATERIALIZED was consumed but VIEW was not,
2904        // bail out — no other CREATE form accepts those modifiers.
2905        if or_replace || materialized {
2906            return Err(ParseError::expected(
2907                vec!["VIEW"],
2908                self.peek(),
2909                self.position(),
2910            ));
2911        }
2912
2913        if matches!(self.peek(), Token::Ident(name) if name.eq_ignore_ascii_case("USER")) {
2914            let stmt = self.parse_create_user_statement()?;
2915            Ok(SqlCommand::CreateUser(stmt))
2916        } else if self.consume_ident_ci("BRANCH")? {
2917            self.parse_create_vcs_ref(VcsRefKind::Branch)
2918        } else if self.consume_ident_ci("TAG")? {
2919            self.parse_create_vcs_ref(VcsRefKind::Tag)
2920        } else if self.check(&Token::Index) || self.check(&Token::Unique) {
2921            match self.parse_create_index_query()? {
2922                QueryExpr::CreateIndex(query) => Ok(SqlCommand::CreateIndex(query)),
2923                other => Err(ParseError::new(
2924                    format!("internal: CREATE INDEX produced unexpected kind {other:?}"),
2925                    self.position(),
2926                )),
2927            }
2928        } else if self.check(&Token::Table) {
2929            self.expect(Token::Table)?;
2930            match self.parse_create_table_body()? {
2931                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2932                other => Err(ParseError::new(
2933                    format!("internal: CREATE TABLE produced unexpected kind {other:?}"),
2934                    self.position(),
2935                )),
2936            }
2937        } else if self.check(&Token::Graph) {
2938            self.advance()?;
2939            match self.parse_create_collection_model_body(CollectionModel::Graph)? {
2940                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2941                other => Err(ParseError::new(
2942                    format!("internal: CREATE GRAPH produced unexpected kind {other:?}"),
2943                    self.position(),
2944                )),
2945            }
2946        } else if self.check(&Token::Document) {
2947            self.advance()?;
2948            match self.parse_create_collection_model_body(CollectionModel::Document)? {
2949                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2950                other => Err(ParseError::new(
2951                    format!("internal: CREATE DOCUMENT produced unexpected kind {other:?}"),
2952                    self.position(),
2953                )),
2954            }
2955        } else if self.check(&Token::Vector) {
2956            self.advance()?;
2957            match self.parse_create_vector_body()? {
2958                QueryExpr::CreateVector(query) => Ok(SqlCommand::CreateVector(query)),
2959                other => Err(ParseError::new(
2960                    format!("internal: CREATE VECTOR produced unexpected kind {other:?}"),
2961                    self.position(),
2962                )),
2963            }
2964        } else if self.check(&Token::Collection) {
2965            self.advance()?;
2966            match self.parse_create_collection_body()? {
2967                QueryExpr::CreateCollection(query) => Ok(SqlCommand::CreateCollection(query)),
2968                other => Err(ParseError::new(
2969                    format!("internal: CREATE COLLECTION produced unexpected kind {other:?}"),
2970                    self.position(),
2971                )),
2972            }
2973        } else if self.check(&Token::Kv) {
2974            self.advance()?;
2975            match self.parse_create_keyed_body(CollectionModel::Kv)? {
2976                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2977                other => Err(ParseError::new(
2978                    format!("internal: CREATE KV produced unexpected kind {other:?}"),
2979                    self.position(),
2980                )),
2981            }
2982        } else if self.consume_ident_ci("CONFIG")? {
2983            match self.parse_create_keyed_body(CollectionModel::Config)? {
2984                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2985                other => Err(ParseError::new(
2986                    format!("internal: CREATE CONFIG produced unexpected kind {other:?}"),
2987                    self.position(),
2988                )),
2989            }
2990        } else if self.consume_ident_ci("VAULT")? {
2991            match self.parse_create_keyed_body(CollectionModel::Vault)? {
2992                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
2993                other => Err(ParseError::new(
2994                    format!("internal: CREATE VAULT produced unexpected kind {other:?}"),
2995                    self.position(),
2996                )),
2997            }
2998        } else if self.check(&Token::Timeseries) {
2999            self.advance()?;
3000            match self.parse_create_timeseries_body()? {
3001                QueryExpr::CreateTimeSeries(query) => Ok(SqlCommand::CreateTimeSeries(query)),
3002                other => Err(ParseError::new(
3003                    format!("internal: CREATE TIMESERIES produced unexpected kind {other:?}"),
3004                    self.position(),
3005                )),
3006            }
3007        } else if self.check(&Token::Metric) {
3008            self.advance()?;
3009            match self.parse_create_metric_body()? {
3010                QueryExpr::CreateMetric(query) => Ok(SqlCommand::CreateMetric(query)),
3011                other => Err(ParseError::new(
3012                    format!("internal: CREATE METRIC produced unexpected kind {other:?}"),
3013                    self.position(),
3014                )),
3015            }
3016        } else if self.consume_ident_ci("METRICS")? {
3017            match self.parse_create_metrics_body()? {
3018                QueryExpr::CreateTable(query) => Ok(SqlCommand::CreateTable(query)),
3019                other => Err(ParseError::new(
3020                    format!("internal: CREATE METRICS produced unexpected kind {other:?}"),
3021                    self.position(),
3022                )),
3023            }
3024        } else if self.consume_ident_ci("SLO")? {
3025            match self.parse_create_slo_body()? {
3026                QueryExpr::CreateSlo(query) => Ok(SqlCommand::CreateSlo(query)),
3027                other => Err(ParseError::new(
3028                    format!("internal: CREATE SLO produced unexpected kind {other:?}"),
3029                    self.position(),
3030                )),
3031            }
3032        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("HYPERTABLE")) {
3033            self.advance()?;
3034            match self.parse_create_hypertable_body()? {
3035                QueryExpr::CreateTimeSeries(query) => Ok(SqlCommand::CreateTimeSeries(query)),
3036                other => Err(ParseError::new(
3037                    format!("internal: CREATE HYPERTABLE produced unexpected kind {other:?}"),
3038                    self.position(),
3039                )),
3040            }
3041        } else if self.check(&Token::Queue) {
3042            self.advance()?;
3043            match self.parse_create_queue_body()? {
3044                QueryExpr::CreateQueue(query) => Ok(SqlCommand::CreateQueue(query)),
3045                other => Err(ParseError::new(
3046                    format!("internal: CREATE QUEUE produced unexpected kind {other:?}"),
3047                    self.position(),
3048                )),
3049            }
3050        } else if self.check(&Token::Tree) {
3051            self.advance()?;
3052            match self.parse_create_tree_body()? {
3053                QueryExpr::CreateTree(query) => Ok(SqlCommand::CreateTree(query)),
3054                other => Err(ParseError::new(
3055                    format!("internal: CREATE TREE produced unexpected kind {other:?}"),
3056                    self.position(),
3057                )),
3058            }
3059        } else if matches!(self.peek(), Token::Ident(n) if
3060                    n.eq_ignore_ascii_case("HLL") ||
3061                    n.eq_ignore_ascii_case("SKETCH") ||
3062                    n.eq_ignore_ascii_case("FILTER"))
3063        {
3064            match self.parse_create_probabilistic()? {
3065                QueryExpr::ProbabilisticCommand(command) => Ok(SqlCommand::Probabilistic(command)),
3066                other => Err(ParseError::new(
3067                    format!("internal: CREATE probabilistic produced unexpected kind {other:?}"),
3068                    self.position(),
3069                )),
3070            }
3071        } else if self.check(&Token::Schema) {
3072            // CREATE SCHEMA [IF NOT EXISTS] name
3073            self.advance()?;
3074            let if_not_exists = self.match_if_not_exists()?;
3075            let name = self.expect_ident()?;
3076            Ok(SqlCommand::CreateSchema(CreateSchemaQuery {
3077                name,
3078                if_not_exists,
3079            }))
3080        } else if self.check(&Token::Policy) {
3081            // Two forms share the leading `CREATE POLICY` tokens:
3082            //   * IAM:   CREATE POLICY '<id>' AS '<json>'          (string literal id)
3083            //   * RLS:   CREATE POLICY <name> ON <target> ...      (bare ident name)
3084            // Disambiguate by peeking the token after POLICY.
3085            self.advance()?;
3086            if matches!(self.peek(), Token::String(_)) {
3087                // IAM form — short-circuit out of the SQL command stack.
3088                let expr = self.parse_create_iam_policy_after_keywords()?;
3089                // Inline command-wrapping: produce a synthetic SqlCommand by
3090                // routing through a generic IAM admin holder. We don't
3091                // have a dedicated SqlCommand variant for IAM yet, so we
3092                // bounce through the existing Grant-shaped Admin slot
3093                // which expects no further tokens.
3094                return Ok(SqlCommand::IamPolicy(expr));
3095            }
3096            let name = self.expect_ident()?;
3097            self.expect(Token::On)?;
3098
3099            let (target_kind, table) = {
3100                use crate::ast::PolicyTargetKind;
3101                let kw = match self.peek() {
3102                    Token::Ident(s) => Some(s.to_ascii_uppercase()),
3103                    _ => None,
3104                };
3105                let kind = kw.as_deref().and_then(|k| match k {
3106                    "NODES" => Some(PolicyTargetKind::Nodes),
3107                    "EDGES" => Some(PolicyTargetKind::Edges),
3108                    "VECTORS" => Some(PolicyTargetKind::Vectors),
3109                    "MESSAGES" => Some(PolicyTargetKind::Messages),
3110                    "POINTS" => Some(PolicyTargetKind::Points),
3111                    "DOCUMENTS" => Some(PolicyTargetKind::Documents),
3112                    _ => None,
3113                });
3114                if let Some(k) = kind {
3115                    self.advance()?;
3116                    self.expect(Token::Of)?;
3117                    let coll = self.expect_ident()?;
3118                    (k, coll)
3119                } else {
3120                    let coll = self.expect_ident()?;
3121                    (PolicyTargetKind::Table, coll)
3122                }
3123            };
3124
3125            let action = if self.consume(&Token::For)? {
3126                let a = match self.peek() {
3127                    Token::Select => {
3128                        self.advance()?;
3129                        Some(PolicyAction::Select)
3130                    }
3131                    Token::Insert => {
3132                        self.advance()?;
3133                        Some(PolicyAction::Insert)
3134                    }
3135                    Token::Update => {
3136                        self.advance()?;
3137                        Some(PolicyAction::Update)
3138                    }
3139                    Token::Delete => {
3140                        self.advance()?;
3141                        Some(PolicyAction::Delete)
3142                    }
3143                    Token::All => {
3144                        self.advance()?;
3145                        None
3146                    }
3147                    _ => None,
3148                };
3149                a
3150            } else {
3151                None
3152            };
3153
3154            let role = if self.consume(&Token::To)? {
3155                Some(self.expect_ident()?)
3156            } else {
3157                None
3158            };
3159
3160            self.expect(Token::Using)?;
3161            self.expect(Token::LParen)?;
3162            let filter = self.parse_filter()?;
3163            self.expect(Token::RParen)?;
3164
3165            Ok(SqlCommand::CreatePolicy(CreatePolicyQuery {
3166                name,
3167                table,
3168                action,
3169                role,
3170                using: Box::new(filter),
3171                target_kind,
3172            }))
3173        } else if self.check(&Token::Server) {
3174            // CREATE SERVER [IF NOT EXISTS] name
3175            //   FOREIGN DATA WRAPPER kind
3176            //   [OPTIONS (key 'value', ...)]
3177            self.advance()?;
3178            let if_not_exists = self.match_if_not_exists()?;
3179            let name = self.expect_ident()?;
3180            self.expect(Token::Foreign)?;
3181            self.expect(Token::Data)?;
3182            self.expect(Token::Wrapper)?;
3183            let wrapper = self.expect_ident()?;
3184            let options = self.parse_fdw_options_clause()?;
3185            Ok(SqlCommand::CreateServer(CreateServerQuery {
3186                name,
3187                wrapper,
3188                options,
3189                if_not_exists,
3190            }))
3191        } else if self.check(&Token::Foreign) {
3192            // CREATE FOREIGN TABLE [IF NOT EXISTS] name (cols)
3193            //   SERVER server_name
3194            //   [OPTIONS (key 'value', ...)]
3195            self.advance()?;
3196            self.expect(Token::Table)?;
3197            let if_not_exists = self.match_if_not_exists()?;
3198            let name = self.expect_ident()?;
3199            self.expect(Token::LParen)?;
3200            let mut columns = Vec::new();
3201            loop {
3202                let col_name = self.expect_ident()?;
3203                let data_type = self.expect_ident_or_keyword()?;
3204                // Inline NOT NULL check — the CREATE TABLE path's helper is
3205                // private and coupling to it just for FDW columns isn't worth it.
3206                let mut not_null = false;
3207                if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("NOT")) {
3208                    self.advance()?;
3209                    if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("NULL")) {
3210                        self.advance()?;
3211                        not_null = true;
3212                    }
3213                }
3214                columns.push(ForeignColumnDef {
3215                    name: col_name,
3216                    data_type,
3217                    not_null,
3218                });
3219                if !self.consume(&Token::Comma)? {
3220                    break;
3221                }
3222            }
3223            self.expect(Token::RParen)?;
3224            self.expect(Token::Server)?;
3225            let server = self.expect_ident()?;
3226            let options = self.parse_fdw_options_clause()?;
3227            Ok(SqlCommand::CreateForeignTable(CreateForeignTableQuery {
3228                name,
3229                server,
3230                columns,
3231                options,
3232                if_not_exists,
3233            }))
3234        } else if self.check(&Token::Sequence) {
3235            // CREATE SEQUENCE [IF NOT EXISTS] name
3236            //   [START [WITH] n] [INCREMENT [BY] n]
3237            self.advance()?;
3238            let if_not_exists = self.match_if_not_exists()?;
3239            let name = self.expect_ident()?;
3240            let mut start: i64 = 1;
3241            let mut increment: i64 = 1;
3242            // Loop over optional clauses in any order.
3243            loop {
3244                if self.consume(&Token::Start)? {
3245                    // Accept `START 100` or `START WITH 100`.
3246                    let _ = self.consume(&Token::With)? || self.consume_ident_ci("WITH")?;
3247                    start = self.parse_integer()?;
3248                } else if self.consume(&Token::Increment)? {
3249                    // Accept `INCREMENT 5` or `INCREMENT BY 5`.
3250                    let _ = self.consume(&Token::By)? || self.consume_ident_ci("BY")?;
3251                    increment = self.parse_integer()?;
3252                } else {
3253                    break;
3254                }
3255            }
3256            Ok(SqlCommand::CreateSequence(CreateSequenceQuery {
3257                name,
3258                if_not_exists,
3259                start,
3260                increment,
3261            }))
3262        } else if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION")) {
3263            self.advance()?; // consume MIGRATION
3264            match self.parse_create_migration_body()? {
3265                QueryExpr::CreateMigration(q) => Ok(SqlCommand::CreateMigration(q)),
3266                other => Err(ParseError::new(
3267                    format!("internal: CREATE MIGRATION produced unexpected kind {other:?}"),
3268                    self.position(),
3269                )),
3270            }
3271        } else if let Some(reason) = analytics_v0_non_goal_create(self.peek()) {
3272            // Issue #789 — enforce Analytics v0 non-goals at the parser
3273            // surface. The parent PRD (#782) explicitly excludes generic
3274            // analytics objects, a new event storage model, cohorts,
3275            // funnels, SLA contracts, and adapters from v0. Reject these
3276            // CREATE forms here with a stable, non-goal-specific message
3277            // so accidental use surfaces an obvious "out of scope for v0"
3278            // error rather than the generic CREATE fallback.
3279            Err(ParseError::new(reason, self.position()))
3280        } else if let Some(err) =
3281            ParseError::unsupported_recognized_token(self.peek(), self.position())
3282        {
3283            Err(err)
3284        } else {
3285            Err(ParseError::expected(
3286                vec![
3287                    "TABLE",
3288                    "GRAPH",
3289                    "VECTOR",
3290                    "DOCUMENT",
3291                    "KV",
3292                    "COLLECTION",
3293                    "INDEX",
3294                    "UNIQUE",
3295                    "METRIC",
3296                    "TIMESERIES",
3297                    "QUEUE",
3298                    "TREE",
3299                    "HLL",
3300                    "SKETCH",
3301                    "FILTER",
3302                    "SCHEMA",
3303                    "SEQUENCE",
3304                    "USER",
3305                    "BRANCH",
3306                    "TAG",
3307                    "MIGRATION",
3308                ],
3309                self.peek(),
3310                pos,
3311            ))
3312        }
3313    }
3314
3315    pub fn parse_sql_command(&mut self) -> Result<SqlCommand, ParseError> {
3316        self.enter_depth()?;
3317        let result = self.parse_sql_command_inner();
3318        self.exit_depth();
3319        result
3320    }
3321
3322    /// Desugar a `SHOW BRANCHES` / `SHOW TAGS` statement into a scan of the
3323    /// matching VCS virtual table. Kept out of `parse_sql_command_inner` and
3324    /// marked `#[inline(never)]` so its `TableQuery` locals do not inflate
3325    /// that already-huge match frame, which is paid twice on the nested
3326    /// `CREATE MATERIALIZED VIEW ... AS SELECT` parse path (issue #1569).
3327    #[inline(never)]
3328    fn parse_show_vcs_ref_table(&mut self, table: &str) -> Result<SqlCommand, ParseError> {
3329        let mut query = TableQuery::new(table);
3330        self.parse_table_clauses(&mut query)?;
3331        Ok(SqlCommand::Select(query))
3332    }
3333
3334    /// Parse the body of a `CREATE BRANCH` / `CREATE TAG` statement.
3335    /// Kept out of `parse_create_command`/`parse_sql_command_inner` and marked
3336    /// `#[inline(never)]` so its `QueryExpr`/`SqlCommand` locals do not inflate
3337    /// those already-huge match frames, which are paid twice on the nested
3338    /// `CREATE MATERIALIZED VIEW ... AS SELECT` parse path (issue #1570).
3339    #[inline(never)]
3340    fn parse_create_vcs_ref(&mut self, kind: VcsRefKind) -> Result<SqlCommand, ParseError> {
3341        match self.parse_create_vcs_ref_body(kind)? {
3342            QueryExpr::CreateVcsRef(query) => Ok(SqlCommand::CreateVcsRef(query)),
3343            other => {
3344                let noun = match kind {
3345                    VcsRefKind::Branch => "BRANCH",
3346                    VcsRefKind::Tag => "TAG",
3347                };
3348                Err(ParseError::new(
3349                    format!("internal: CREATE {noun} produced unexpected kind {other:?}"),
3350                    self.position(),
3351                ))
3352            }
3353        }
3354    }
3355
3356    /// Parse the body of a `DROP BRANCH` / `DROP TAG` statement.
3357    /// Kept out of `parse_sql_command_inner` and marked `#[inline(never)]` for
3358    /// the same stack-frame reason as [`Self::parse_create_vcs_ref`] (#1570).
3359    #[inline(never)]
3360    fn parse_drop_vcs_ref(&mut self, kind: VcsRefKind) -> Result<SqlCommand, ParseError> {
3361        match self.parse_drop_vcs_ref_body(kind)? {
3362            QueryExpr::DropVcsRef(query) => Ok(SqlCommand::DropVcsRef(query)),
3363            other => {
3364                let noun = match kind {
3365                    VcsRefKind::Branch => "BRANCH",
3366                    VcsRefKind::Tag => "TAG",
3367                };
3368                Err(ParseError::new(
3369                    format!("internal: DROP {noun} produced unexpected kind {other:?}"),
3370                    self.position(),
3371                ))
3372            }
3373        }
3374    }
3375
3376    fn parse_sql_command_inner(&mut self) -> Result<SqlCommand, ParseError> {
3377        match self.peek() {
3378            Token::Select => match self.parse_select_query()? {
3379                QueryExpr::Table(query) => Ok(SqlCommand::Select(query)),
3380                QueryExpr::Join(query) => Ok(SqlCommand::Join(query)),
3381                other => Err(ParseError::new(
3382                    format!("internal: SELECT produced unexpected query kind {other:?}"),
3383                    self.position(),
3384                )),
3385            },
3386            Token::From => match self.parse_from_query()? {
3387                QueryExpr::Table(query) => Ok(SqlCommand::Select(query)),
3388                QueryExpr::Join(query) => Ok(SqlCommand::Join(query)),
3389                other => Err(ParseError::new(
3390                    format!("internal: FROM produced unexpected query kind {other:?}"),
3391                    self.position(),
3392                )),
3393            },
3394            Token::Insert => match self.parse_insert_query()? {
3395                QueryExpr::Insert(query) => Ok(SqlCommand::Insert(query)),
3396                other => Err(ParseError::new(
3397                    format!("internal: INSERT produced unexpected query kind {other:?}"),
3398                    self.position(),
3399                )),
3400            },
3401            Token::Update => match self.parse_update_query()? {
3402                QueryExpr::Update(query) => Ok(SqlCommand::Update(query)),
3403                other => Err(ParseError::new(
3404                    format!("internal: UPDATE produced unexpected query kind {other:?}"),
3405                    self.position(),
3406                )),
3407            },
3408            Token::Delete => {
3409                if matches!(self.peek_next()?, Token::Ident(n) if n.eq_ignore_ascii_case("SECRET"))
3410                {
3411                    self.advance()?; // DELETE
3412                    self.advance()?; // SECRET
3413                    let key =
3414                        Self::normalize_secret_admin_path(self.parse_dotted_admin_path(true)?);
3415                    Ok(SqlCommand::DeleteSecret { key })
3416                } else if matches!(self.peek_next()?, Token::Kv) {
3417                    self.advance()?; // DELETE
3418                    self.advance()?; // KV
3419                    let key = self.parse_dotted_admin_path(true)?;
3420                    Ok(SqlCommand::DeleteKv { key })
3421                } else {
3422                    match self.parse_delete_query()? {
3423                        QueryExpr::Delete(query) => Ok(SqlCommand::Delete(query)),
3424                        other => Err(ParseError::new(
3425                            format!("internal: DELETE produced unexpected query kind {other:?}"),
3426                            self.position(),
3427                        )),
3428                    }
3429                }
3430            }
3431            Token::Truncate => {
3432                self.advance()?;
3433                let model = if self.consume(&Token::Table)? {
3434                    Some(CollectionModel::Table)
3435                } else if self.consume(&Token::Graph)? {
3436                    Some(CollectionModel::Graph)
3437                } else if self.consume(&Token::Vector)? {
3438                    Some(CollectionModel::Vector)
3439                } else if self.consume(&Token::Document)? {
3440                    Some(CollectionModel::Document)
3441                } else if self.consume(&Token::Timeseries)? {
3442                    Some(CollectionModel::TimeSeries)
3443                } else if self.consume_ident_ci("METRICS")? {
3444                    Some(CollectionModel::Metrics)
3445                } else if self.consume(&Token::Kv)? {
3446                    Some(CollectionModel::Kv)
3447                } else if self.consume(&Token::Queue)? {
3448                    Some(CollectionModel::Queue)
3449                } else if self.consume(&Token::Collection)? {
3450                    None
3451                } else {
3452                    return Err(ParseError::expected(
3453                        vec![
3454                            "TABLE",
3455                            "GRAPH",
3456                            "VECTOR",
3457                            "DOCUMENT",
3458                            "TIMESERIES",
3459                            "METRICS",
3460                            "KV",
3461                            "QUEUE",
3462                            "COLLECTION",
3463                        ],
3464                        self.peek(),
3465                        self.position(),
3466                    ));
3467                };
3468                match self.parse_truncate_body(model)? {
3469                    QueryExpr::Truncate(query) => Ok(SqlCommand::Truncate(query)),
3470                    other => Err(ParseError::new(
3471                        format!("internal: TRUNCATE produced unexpected kind {other:?}"),
3472                        self.position(),
3473                    )),
3474                }
3475            }
3476            Token::Explain => {
3477                // Peek ahead: EXPLAIN MIGRATION name → ExplainMigration
3478                // EXPLAIN ALTER FOR ... → ExplainAlter (existing path)
3479                if matches!(self.peek_next()?, Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION"))
3480                {
3481                    self.advance()?; // consume EXPLAIN
3482                    match self.parse_explain_migration_after_keyword()? {
3483                        QueryExpr::ExplainMigration(q) => Ok(SqlCommand::ExplainMigration(q)),
3484                        other => Err(ParseError::new(
3485                            format!(
3486                                "internal: EXPLAIN MIGRATION produced unexpected kind {other:?}"
3487                            ),
3488                            self.position(),
3489                        )),
3490                    }
3491                } else {
3492                    match self.parse_explain_alter_query()? {
3493                        QueryExpr::ExplainAlter(query) => Ok(SqlCommand::ExplainAlter(query)),
3494                        other => Err(ParseError::new(
3495                            format!("internal: EXPLAIN produced unexpected query kind {other:?}"),
3496                            self.position(),
3497                        )),
3498                    }
3499                }
3500            }
3501            Token::Create => self.parse_create_command(),
3502            Token::Drop => {
3503                let pos = self.position();
3504                self.advance()?;
3505
3506                // DROP [MATERIALIZED] VIEW [IF EXISTS] name
3507                let materialized = self.consume(&Token::Materialized)?;
3508                if self.check(&Token::View) {
3509                    self.advance()?;
3510                    let if_exists = self.match_if_exists()?;
3511                    let name = self.expect_ident()?;
3512                    return Ok(SqlCommand::DropView(DropViewQuery {
3513                        name,
3514                        materialized,
3515                        if_exists,
3516                    }));
3517                }
3518                if materialized {
3519                    return Err(ParseError::expected(
3520                        vec!["VIEW"],
3521                        self.peek(),
3522                        self.position(),
3523                    ));
3524                }
3525
3526                if self.consume_ident_ci("BRANCH")? {
3527                    self.parse_drop_vcs_ref(VcsRefKind::Branch)
3528                } else if self.consume_ident_ci("TAG")? {
3529                    self.parse_drop_vcs_ref(VcsRefKind::Tag)
3530                } else if self.check(&Token::Index) {
3531                    match self.parse_drop_index_query()? {
3532                        QueryExpr::DropIndex(query) => Ok(SqlCommand::DropIndex(query)),
3533                        other => Err(ParseError::new(
3534                            format!("internal: DROP INDEX produced unexpected kind {other:?}"),
3535                            self.position(),
3536                        )),
3537                    }
3538                } else if self.check(&Token::Table) {
3539                    self.expect(Token::Table)?;
3540                    match self.parse_drop_table_body()? {
3541                        QueryExpr::DropTable(query) => Ok(SqlCommand::DropTable(query)),
3542                        other => Err(ParseError::new(
3543                            format!("internal: DROP TABLE produced unexpected kind {other:?}"),
3544                            self.position(),
3545                        )),
3546                    }
3547                } else if self.check(&Token::Graph) {
3548                    self.advance()?;
3549                    match self.parse_drop_graph_body()? {
3550                        QueryExpr::DropGraph(query) => Ok(SqlCommand::DropGraph(query)),
3551                        other => Err(ParseError::new(
3552                            format!("internal: DROP GRAPH produced unexpected kind {other:?}"),
3553                            self.position(),
3554                        )),
3555                    }
3556                } else if self.check(&Token::Vector) {
3557                    self.advance()?;
3558                    match self.parse_drop_vector_body()? {
3559                        QueryExpr::DropVector(query) => Ok(SqlCommand::DropVector(query)),
3560                        other => Err(ParseError::new(
3561                            format!("internal: DROP VECTOR produced unexpected kind {other:?}"),
3562                            self.position(),
3563                        )),
3564                    }
3565                } else if self.check(&Token::Document) {
3566                    self.advance()?;
3567                    match self.parse_drop_document_body()? {
3568                        QueryExpr::DropDocument(query) => Ok(SqlCommand::DropDocument(query)),
3569                        other => Err(ParseError::new(
3570                            format!("internal: DROP DOCUMENT produced unexpected kind {other:?}"),
3571                            self.position(),
3572                        )),
3573                    }
3574                } else if self.check(&Token::Kv) {
3575                    self.advance()?;
3576                    match self.parse_drop_kv_body()? {
3577                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3578                        other => Err(ParseError::new(
3579                            format!("internal: DROP KV produced unexpected kind {other:?}"),
3580                            self.position(),
3581                        )),
3582                    }
3583                } else if self.consume_ident_ci("CONFIG")? {
3584                    match self.parse_drop_keyed_body(CollectionModel::Config)? {
3585                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3586                        other => Err(ParseError::new(
3587                            format!("internal: DROP CONFIG produced unexpected kind {other:?}"),
3588                            self.position(),
3589                        )),
3590                    }
3591                } else if self.consume_ident_ci("VAULT")? {
3592                    match self.parse_drop_keyed_body(CollectionModel::Vault)? {
3593                        QueryExpr::DropKv(query) => Ok(SqlCommand::DropKv(query)),
3594                        other => Err(ParseError::new(
3595                            format!("internal: DROP VAULT produced unexpected kind {other:?}"),
3596                            self.position(),
3597                        )),
3598                    }
3599                } else if self.check(&Token::Collection) {
3600                    self.advance()?;
3601                    match self.parse_drop_collection_body()? {
3602                        QueryExpr::DropCollection(query) => Ok(SqlCommand::DropCollection(query)),
3603                        other => Err(ParseError::new(
3604                            format!("internal: DROP COLLECTION produced unexpected kind {other:?}"),
3605                            self.position(),
3606                        )),
3607                    }
3608                } else if self.check(&Token::Timeseries) {
3609                    self.advance()?;
3610                    match self.parse_drop_timeseries_body()? {
3611                        QueryExpr::DropTimeSeries(query) => Ok(SqlCommand::DropTimeSeries(query)),
3612                        other => Err(ParseError::new(
3613                            format!("internal: DROP TIMESERIES produced unexpected kind {other:?}"),
3614                            self.position(),
3615                        )),
3616                    }
3617                } else if self.consume_ident_ci("METRICS")? {
3618                    match self.parse_drop_collection_model_body(Some(CollectionModel::Metrics))? {
3619                        QueryExpr::DropCollection(query) => Ok(SqlCommand::DropCollection(query)),
3620                        other => Err(ParseError::new(
3621                            format!("internal: DROP METRICS produced unexpected kind {other:?}"),
3622                            self.position(),
3623                        )),
3624                    }
3625                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("HYPERTABLE"))
3626                {
3627                    // DROP HYPERTABLE name reuses the same AST as
3628                    // DROP TIMESERIES — runtime clears the registry
3629                    // entry *and* drops the backing collection.
3630                    self.advance()?;
3631                    match self.parse_drop_timeseries_body()? {
3632                        QueryExpr::DropTimeSeries(query) => Ok(SqlCommand::DropTimeSeries(query)),
3633                        other => Err(ParseError::new(
3634                            format!("internal: DROP HYPERTABLE produced unexpected kind {other:?}"),
3635                            self.position(),
3636                        )),
3637                    }
3638                } else if self.check(&Token::Queue) {
3639                    self.advance()?;
3640                    match self.parse_drop_queue_body()? {
3641                        QueryExpr::DropQueue(query) => Ok(SqlCommand::DropQueue(query)),
3642                        other => Err(ParseError::new(
3643                            format!("internal: DROP QUEUE produced unexpected kind {other:?}"),
3644                            self.position(),
3645                        )),
3646                    }
3647                } else if self.check(&Token::Tree) {
3648                    self.advance()?;
3649                    match self.parse_drop_tree_body()? {
3650                        QueryExpr::DropTree(query) => Ok(SqlCommand::DropTree(query)),
3651                        other => Err(ParseError::new(
3652                            format!("internal: DROP TREE produced unexpected kind {other:?}"),
3653                            self.position(),
3654                        )),
3655                    }
3656                } else if matches!(self.peek(), Token::Ident(n) if
3657                    n.eq_ignore_ascii_case("HLL") ||
3658                    n.eq_ignore_ascii_case("SKETCH") ||
3659                    n.eq_ignore_ascii_case("FILTER"))
3660                {
3661                    match self.parse_drop_probabilistic()? {
3662                        QueryExpr::ProbabilisticCommand(command) => {
3663                            Ok(SqlCommand::Probabilistic(command))
3664                        }
3665                        other => Err(ParseError::new(
3666                            format!(
3667                                "internal: DROP probabilistic produced unexpected kind {other:?}"
3668                            ),
3669                            self.position(),
3670                        )),
3671                    }
3672                } else if self.check(&Token::Schema) {
3673                    // DROP SCHEMA [IF EXISTS] name [CASCADE]
3674                    self.advance()?;
3675                    let if_exists = self.match_if_exists()?;
3676                    let name = self.expect_ident()?;
3677                    let cascade = self.consume(&Token::Cascade)?;
3678                    Ok(SqlCommand::DropSchema(DropSchemaQuery {
3679                        name,
3680                        if_exists,
3681                        cascade,
3682                    }))
3683                } else if self.check(&Token::Policy) {
3684                    // Two forms:
3685                    //   * IAM:   DROP POLICY '<id>'
3686                    //   * RLS:   DROP POLICY [IF EXISTS] name ON table
3687                    self.advance()?;
3688                    if matches!(self.peek(), Token::String(_)) {
3689                        let expr = self.parse_drop_iam_policy_after_keywords()?;
3690                        return Ok(SqlCommand::IamPolicy(expr));
3691                    }
3692                    let if_exists = self.match_if_exists()?;
3693                    let name = self.expect_ident()?;
3694                    self.expect(Token::On)?;
3695                    let table = self.expect_ident()?;
3696                    Ok(SqlCommand::DropPolicy(DropPolicyQuery {
3697                        name,
3698                        table,
3699                        if_exists,
3700                    }))
3701                } else if self.check(&Token::Server) {
3702                    // DROP SERVER [IF EXISTS] name [CASCADE]
3703                    self.advance()?;
3704                    let if_exists = self.match_if_exists()?;
3705                    let name = self.expect_ident()?;
3706                    let cascade = self.consume(&Token::Cascade)?;
3707                    Ok(SqlCommand::DropServer(DropServerQuery {
3708                        name,
3709                        if_exists,
3710                        cascade,
3711                    }))
3712                } else if self.check(&Token::Foreign) {
3713                    // DROP FOREIGN TABLE [IF EXISTS] name
3714                    self.advance()?;
3715                    self.expect(Token::Table)?;
3716                    let if_exists = self.match_if_exists()?;
3717                    let name = self.expect_ident()?;
3718                    Ok(SqlCommand::DropForeignTable(DropForeignTableQuery {
3719                        name,
3720                        if_exists,
3721                    }))
3722                } else if self.check(&Token::Sequence) {
3723                    // DROP SEQUENCE [IF EXISTS] name
3724                    self.advance()?;
3725                    let if_exists = self.match_if_exists()?;
3726                    let name = self.expect_ident()?;
3727                    Ok(SqlCommand::DropSequence(DropSequenceQuery {
3728                        name,
3729                        if_exists,
3730                    }))
3731                } else if let Some(err) =
3732                    ParseError::unsupported_recognized_token(self.peek(), self.position())
3733                {
3734                    Err(err)
3735                } else {
3736                    Err(ParseError::expected(
3737                        vec![
3738                            "TABLE",
3739                            "INDEX",
3740                            "TIMESERIES",
3741                            "QUEUE",
3742                            "TREE",
3743                            "HLL",
3744                            "SKETCH",
3745                            "FILTER",
3746                            "SCHEMA",
3747                            "SEQUENCE",
3748                            "BRANCH",
3749                            "TAG",
3750                        ],
3751                        self.peek(),
3752                        pos,
3753                    ))
3754                }
3755            }
3756            Token::Alter => {
3757                // Disambiguate ALTER USER / ALTER QUEUE / ALTER TABLE without
3758                // committing to a path until we've seen the target.
3759                // We peek the *next* token (without consuming) and
3760                // dispatch accordingly.
3761                let next = self.peek_next()?.clone();
3762                if matches!(next, Token::Ident(ref s) if s.eq_ignore_ascii_case("USER")) {
3763                    self.advance()?; // consume ALTER
3764                    let stmt = self.parse_alter_user_statement()?;
3765                    Ok(SqlCommand::AlterUser(stmt))
3766                } else if matches!(next, Token::Queue) {
3767                    self.advance()?; // consume ALTER
3768                    self.advance()?; // consume QUEUE
3769                    match self.parse_alter_queue_body()? {
3770                        QueryExpr::AlterQueue(query) => Ok(SqlCommand::AlterQueue(query)),
3771                        other => Err(ParseError::new(
3772                            format!("internal: ALTER QUEUE produced unexpected kind {other:?}"),
3773                            self.position(),
3774                        )),
3775                    }
3776                } else if matches!(next, Token::Metric) {
3777                    self.advance()?; // consume ALTER
3778                    self.advance()?; // consume METRIC
3779                    match self.parse_alter_metric_body()? {
3780                        QueryExpr::AlterMetric(query) => Ok(SqlCommand::AlterMetric(query)),
3781                        other => Err(ParseError::new(
3782                            format!("internal: ALTER METRIC produced unexpected kind {other:?}"),
3783                            self.position(),
3784                        )),
3785                    }
3786                } else if matches!(next, Token::Graph) {
3787                    // Issue #801 — `ALTER GRAPH name ADD|DROP ANALYTICS ...`
3788                    // shares the AlterTable AST so analytics-config lifecycle
3789                    // mutations dispatch through the existing executor path.
3790                    match self.parse_alter_graph_query()? {
3791                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3792                        other => Err(ParseError::new(
3793                            format!(
3794                                "internal: ALTER GRAPH produced unexpected query kind {other:?}"
3795                            ),
3796                            self.position(),
3797                        )),
3798                    }
3799                } else if matches!(next, Token::Table)
3800                    || matches!(next, Token::Collection)
3801                    || matches!(next, Token::Ident(ref s) if s.eq_ignore_ascii_case("COLLECTION"))
3802                {
3803                    // Issue #522 — `ALTER COLLECTION` shares the AlterTable
3804                    // AST so signer-registry mutations dispatch through the
3805                    // existing executor. The DDL parser body accepts either
3806                    // keyword interchangeably for the open-vocabulary alters
3807                    // we own (currently `ADD|REVOKE SIGNER`).
3808                    match self.parse_alter_table_query()? {
3809                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3810                        other => Err(ParseError::new(
3811                            format!(
3812                                "internal: ALTER TABLE produced unexpected query kind {other:?}"
3813                            ),
3814                            self.position(),
3815                        )),
3816                    }
3817                } else if let Some(err) =
3818                    ParseError::unsupported_recognized_token(&next, self.position())
3819                {
3820                    Err(err)
3821                } else {
3822                    match self.parse_alter_table_query()? {
3823                        QueryExpr::AlterTable(query) => Ok(SqlCommand::AlterTable(query)),
3824                        other => Err(ParseError::new(
3825                            format!("internal: ALTER produced unexpected query kind {other:?}"),
3826                            self.position(),
3827                        )),
3828                    }
3829                }
3830            }
3831            Token::Ident(name) if name.eq_ignore_ascii_case("GRANT") => {
3832                let stmt = self.parse_grant_statement()?;
3833                Ok(SqlCommand::Grant(stmt))
3834            }
3835            Token::Ident(name) if name.eq_ignore_ascii_case("REVOKE") => {
3836                let stmt = self.parse_revoke_statement()?;
3837                Ok(SqlCommand::Revoke(stmt))
3838            }
3839            Token::Ident(name) if name.eq_ignore_ascii_case("EVENTS") => {
3840                self.advance()?;
3841                if self.consume_ident_ci("BACKFILL")? {
3842                    return Err(ParseError::new(
3843                        "EVENTS BACKFILL STATUS is not implemented; EVENTS BACKFILL runtime is available but durable progress tracking is not"
3844                            .to_string(),
3845                        self.position(),
3846                    ));
3847                }
3848                if !self.consume_ident_ci("STATUS")? {
3849                    return Err(ParseError::expected(
3850                        vec!["STATUS"],
3851                        self.peek(),
3852                        self.position(),
3853                    ));
3854                }
3855
3856                let mut query = TableQuery::new("red.subscriptions");
3857                let collection = match self.peek().clone() {
3858                    Token::Ident(name) => {
3859                        self.advance()?;
3860                        Some(name)
3861                    }
3862                    Token::String(name) => {
3863                        self.advance()?;
3864                        Some(name)
3865                    }
3866                    _ => None,
3867                };
3868                self.parse_table_clauses(&mut query)?;
3869                if let Some(collection) = collection {
3870                    let filter = Filter::compare(
3871                        FieldRef::column("red.subscriptions", "collection"),
3872                        CompareOp::Eq,
3873                        Value::text(collection),
3874                    );
3875                    let expr = filter_to_expr(&filter);
3876                    query.where_expr = Some(match query.where_expr.take() {
3877                        Some(existing) => Expr::binop(BinOp::And, existing, expr),
3878                        None => expr,
3879                    });
3880                    query.filter = Some(match query.filter.take() {
3881                        Some(existing) => existing.and(filter),
3882                        None => filter,
3883                    });
3884                }
3885                Ok(SqlCommand::Select(query))
3886            }
3887            Token::Attach => {
3888                let expr = self.parse_attach_policy()?;
3889                Ok(SqlCommand::IamPolicy(expr))
3890            }
3891            Token::Detach => {
3892                let expr = self.parse_detach_policy()?;
3893                Ok(SqlCommand::IamPolicy(expr))
3894            }
3895            Token::Ident(name) if name.eq_ignore_ascii_case("SIMULATE") => {
3896                let expr = self.parse_simulate_policy()?;
3897                Ok(SqlCommand::IamPolicy(expr))
3898            }
3899            Token::Ident(name) if name.eq_ignore_ascii_case("LINT") => {
3900                let expr = self.parse_lint_policy()?;
3901                Ok(SqlCommand::IamPolicy(expr))
3902            }
3903            Token::Ident(name) if name.eq_ignore_ascii_case("MIGRATE") => {
3904                // `MIGRATE POLICY MODE TO ...` is the S5B (#714) path.
3905                // Lookahead one token because `MIGRATE` is otherwise
3906                // unused at this layer.
3907                let next = self.peek_next()?.clone();
3908                let is_policy_mode = matches!(&next, Token::Policy)
3909                    || matches!(&next, Token::Ident(name)
3910                        if name.eq_ignore_ascii_case("POLICY"));
3911                if is_policy_mode {
3912                    let expr = self.parse_migrate_policy_mode()?;
3913                    return Ok(SqlCommand::IamPolicy(expr));
3914                }
3915                Err(ParseError::expected(
3916                    vec!["POLICY"],
3917                    self.peek(),
3918                    self.position(),
3919                ))
3920            }
3921            Token::Ident(name) if is_vcs_command_head(name) => {
3922                self.parse_vcs_command().map(SqlCommand::Vcs)
3923            }
3924            // RESOLVE CONFLICT is the VCS working-set verb. The frontend only
3925            // routes RESOLVE here when CONFLICT follows; plain RESOLVE / RESOLVE
3926            // CONFIG stays on the config surface and never reaches this arm.
3927            Token::Ident(name) if name.eq_ignore_ascii_case("RESOLVE") => {
3928                self.parse_vcs_command().map(SqlCommand::Vcs)
3929            }
3930            Token::Set => {
3931                self.advance()?;
3932                if self.consume_ident_ci("CONFIG")? {
3933                    let full_key = self.parse_dotted_admin_path(true)?;
3934                    self.expect(Token::Eq)?;
3935                    let value = self.parse_literal_value()?;
3936                    Ok(SqlCommand::SetConfig {
3937                        key: full_key,
3938                        value,
3939                    })
3940                } else if self.consume_ident_ci("SECRET")? {
3941                    let key =
3942                        Self::normalize_secret_admin_path(self.parse_dotted_admin_path(true)?);
3943                    self.expect(Token::Eq)?;
3944                    let value = self.parse_literal_value()?;
3945                    Ok(SqlCommand::SetSecret { key, value })
3946                } else if self.consume(&Token::Kv)? {
3947                    let key = self.parse_dotted_admin_path(true)?;
3948                    self.expect(Token::Eq)?;
3949                    let value = self.parse_literal_value()?;
3950                    Ok(SqlCommand::SetKv { key, value })
3951                } else if self.consume_ident_ci("TENANT")? {
3952                    // SET TENANT 'id'  |  SET TENANT = 'id'  |
3953                    // SET TENANT NULL  |  SET TENANT = NULL
3954                    let _ = self.consume(&Token::Eq)?;
3955                    if self.consume_ident_ci("NULL")? {
3956                        Ok(SqlCommand::SetTenant(None))
3957                    } else {
3958                        let value = self.parse_literal_value()?;
3959                        match value {
3960                            Value::Text(s) => Ok(SqlCommand::SetTenant(Some(s.to_string()))),
3961                            Value::Null => Ok(SqlCommand::SetTenant(None)),
3962                            other => Err(ParseError::new(
3963                                format!("SET TENANT expects a text literal or NULL, got {other:?}"),
3964                                self.position(),
3965                            )),
3966                        }
3967                    }
3968                } else {
3969                    Err(ParseError::expected(
3970                        vec!["CONFIG", "SECRET", "KV", "TENANT"],
3971                        self.peek(),
3972                        self.position(),
3973                    ))
3974                }
3975            }
3976            Token::Ident(name) if name.eq_ignore_ascii_case("APPLY") => {
3977                self.advance()?;
3978                match self.parse_apply_migration()? {
3979                    QueryExpr::ApplyMigration(q) => Ok(SqlCommand::ApplyMigration(q)),
3980                    other => Err(ParseError::new(
3981                        format!("internal: APPLY MIGRATION produced unexpected kind {other:?}"),
3982                        self.position(),
3983                    )),
3984                }
3985            }
3986            Token::Ident(name) if name.eq_ignore_ascii_case("RESET") => {
3987                // RESET TENANT clears session-local tenant state; every
3988                // other RESET shape in this parser belongs to the VCS
3989                // working-set command (`RESET [HARD|SOFT|MIXED] TO ...`).
3990                if matches!(self.peek_next()?, Token::Ident(next) if next.eq_ignore_ascii_case("TENANT"))
3991                {
3992                    self.advance()?;
3993                    self.advance()?;
3994                    Ok(SqlCommand::SetTenant(None))
3995                } else {
3996                    self.parse_vcs_command().map(SqlCommand::Vcs)
3997                }
3998            }
3999            Token::Ident(name)
4000                if name.eq_ignore_ascii_case("DESCRIBE") || name.eq_ignore_ascii_case("DESC") =>
4001            {
4002                self.advance()?;
4003                let collection = self.parse_dotted_admin_path(false)?;
4004                let mut query = TableQuery::new("red.describe");
4005                query.filter = Some(Filter::compare(
4006                    FieldRef::column("", "collection"),
4007                    CompareOp::Eq,
4008                    Value::text(collection),
4009                ));
4010                Ok(SqlCommand::Select(query))
4011            }
4012            Token::Desc => {
4013                self.advance()?;
4014                let collection = self.parse_dotted_admin_path(false)?;
4015                let mut query = TableQuery::new("red.describe");
4016                query.filter = Some(Filter::compare(
4017                    FieldRef::column("", "collection"),
4018                    CompareOp::Eq,
4019                    Value::text(collection),
4020                ));
4021                Ok(SqlCommand::Select(query))
4022            }
4023            Token::Ident(name) if name.eq_ignore_ascii_case("SHOW") => {
4024                self.advance()?;
4025                if self.consume(&Token::Create)? || self.consume_ident_ci("CREATE")? {
4026                    if !(self.consume(&Token::Table)? || self.consume_ident_ci("TABLE")?) {
4027                        return Err(ParseError::expected(
4028                            vec!["TABLE"],
4029                            self.peek(),
4030                            self.position(),
4031                        ));
4032                    }
4033                    let collection = self.parse_dotted_admin_path(false)?;
4034                    let mut query = TableQuery::new("red.show_create");
4035                    query.filter = Some(Filter::compare(
4036                        FieldRef::column("", "collection"),
4037                        CompareOp::Eq,
4038                        Value::text(collection),
4039                    ));
4040                    Ok(SqlCommand::Select(query))
4041                } else if self.consume_ident_ci("CONFIG")? {
4042                    // Accept dotted prefixes the same way SET CONFIG does
4043                    // (`SHOW CONFIG durability.mode`), and empty prefix
4044                    // (`SHOW CONFIG`) for a catalog-wide listing.
4045                    let prefix = if !(self.check(&Token::Eof)
4046                        || self.check(&Token::As)
4047                        || self.check(&Token::Format))
4048                    {
4049                        let first = self.expect_ident()?;
4050                        let mut full = first;
4051                        while self.consume(&Token::Dot)? {
4052                            let next = self.expect_ident_or_keyword()?;
4053                            full = format!("{full}.{next}");
4054                        }
4055                        // Match SET CONFIG: lowercase so keyword segments
4056                        // come out consistent with the stored keys.
4057                        Some(full.to_ascii_lowercase())
4058                    } else {
4059                        None
4060                    };
4061                    let as_json = if self.consume(&Token::As)? || self.consume(&Token::Format)? {
4062                        if !self.consume(&Token::Json)? {
4063                            return Err(ParseError::expected(
4064                                vec!["JSON"],
4065                                self.peek(),
4066                                self.position(),
4067                            ));
4068                        }
4069                        true
4070                    } else {
4071                        false
4072                    };
4073                    Ok(SqlCommand::ShowConfig { prefix, as_json })
4074                } else if self.consume_ident_ci("COLLECTIONS")? {
4075                    let mut query = TableQuery::new("red.collections");
4076                    let include_internal = if self.consume_ident_ci("INCLUDING")? {
4077                        if !self.consume_ident_ci("INTERNAL")? {
4078                            return Err(ParseError::expected(
4079                                vec!["INTERNAL"],
4080                                self.peek(),
4081                                self.position(),
4082                            ));
4083                        }
4084                        true
4085                    } else {
4086                        false
4087                    };
4088                    self.parse_table_clauses(&mut query)?;
4089                    if !include_internal {
4090                        let user_filter = query.filter.take();
4091                        let hide_internal = crate::ast::Filter::Compare {
4092                            field: FieldRef::column("", "internal"),
4093                            op: CompareOp::Eq,
4094                            value: Value::Boolean(false),
4095                        };
4096                        query.filter = Some(match user_filter {
4097                            Some(filter) => filter.and(hide_internal),
4098                            None => hide_internal,
4099                        });
4100                    }
4101                    Ok(SqlCommand::Select(query))
4102                } else if self.consume_ident_ci("TABLES")? {
4103                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4104                        self, "table",
4105                    )?))
4106                } else if self.consume_ident_ci("QUEUES")? {
4107                    // Issue #535 — `SHOW QUEUES` desugars to the
4108                    // `red.queues` virtual table (queue-shaped
4109                    // columns), not the filtered `red.collections`
4110                    // view. `INCLUDING INTERNAL` mirrors the
4111                    // `SHOW COLLECTIONS` opt-in: without it, DLQ
4112                    // targets and other auto-created queues are
4113                    // hidden via the `internal = false` filter.
4114                    let mut query = TableQuery::new("red.queues");
4115                    let include_internal = if self.consume_ident_ci("INCLUDING")? {
4116                        if !self.consume_ident_ci("INTERNAL")? {
4117                            return Err(ParseError::expected(
4118                                vec!["INTERNAL"],
4119                                self.peek(),
4120                                self.position(),
4121                            ));
4122                        }
4123                        true
4124                    } else {
4125                        false
4126                    };
4127                    self.parse_table_clauses(&mut query)?;
4128                    if !include_internal {
4129                        let hide_internal = Filter::Compare {
4130                            field: FieldRef::column("", "internal"),
4131                            op: CompareOp::Eq,
4132                            value: Value::Boolean(false),
4133                        };
4134                        add_table_filter(&mut query, hide_internal);
4135                    }
4136                    Ok(SqlCommand::Select(query))
4137                } else if self.consume_ident_ci("BRANCHES")? {
4138                    self.parse_show_vcs_ref_table("red.branches")
4139                } else if self.consume_ident_ci("TAGS")? {
4140                    self.parse_show_vcs_ref_table("red.tags")
4141                } else if self.consume(&Token::Vectors)? || self.consume_ident_ci("VECTORS")? {
4142                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4143                        self, "vector",
4144                    )?))
4145                } else if self.consume_ident_ci("DOCUMENTS")? {
4146                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4147                        self, "document",
4148                    )?))
4149                } else if self.consume(&Token::Timeseries)?
4150                    || self.consume_ident_ci("TIMESERIES")?
4151                {
4152                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4153                        self,
4154                        "timeseries",
4155                    )?))
4156                } else if self.consume_ident_ci("GRAPHS")? {
4157                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4158                        self, "graph",
4159                    )?))
4160                } else if self.consume_ident_ci("CONFIGS")? {
4161                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4162                        self, "config",
4163                    )?))
4164                } else if self.consume_ident_ci("VAULTS")? {
4165                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4166                        self, "vault",
4167                    )?))
4168                } else if self.consume(&Token::Kv)?
4169                    || self.consume_ident_ci("KV")?
4170                    || self.consume_ident_ci("KVS")?
4171                {
4172                    Ok(SqlCommand::Select(parse_show_collections_by_model(
4173                        self, "kv",
4174                    )?))
4175                } else if self.consume(&Token::Schema)? || self.consume_ident_ci("SCHEMA")? {
4176                    let collection = self.parse_dotted_admin_path(false)?;
4177                    let mut query = TableQuery::new("red.columns");
4178                    query.filter = Some(Filter::compare(
4179                        FieldRef::column("", "collection"),
4180                        CompareOp::Eq,
4181                        Value::text(collection),
4182                    ));
4183                    Ok(SqlCommand::Select(query))
4184                } else if self.consume_ident_ci("INDICES")? || self.consume_ident_ci("INDEXES")? {
4185                    let mut query = TableQuery::new("red.show_indexes");
4186                    if self.consume(&Token::On)? {
4187                        let collection = self.expect_ident_or_keyword()?;
4188                        let filter = Filter::Compare {
4189                            field: FieldRef::column("", "table"),
4190                            op: CompareOp::Eq,
4191                            value: Value::text(collection),
4192                        };
4193                        query.where_expr = Some(filter_to_expr(&filter));
4194                        query.filter = Some(filter);
4195                    }
4196                    self.parse_table_clauses(&mut query)?;
4197                    Ok(SqlCommand::Select(query))
4198                } else if self.consume_ident_ci("POLICIES")? {
4199                    if self.consume(&Token::For)? || self.consume_ident_ci("FOR")? {
4200                        let principal = self.parse_iam_principal_kind()?;
4201                        return Ok(SqlCommand::IamPolicy(QueryExpr::ShowPolicies {
4202                            filter: Some(principal),
4203                        }));
4204                    }
4205                    let mut query = TableQuery::new("red.policies");
4206                    let collection_filter =
4207                        if self.consume(&Token::On)? || self.consume_ident_ci("ON")? {
4208                            let collection = self.parse_dotted_admin_path(false)?;
4209                            Some(Filter::Compare {
4210                                field: FieldRef::TableColumn {
4211                                    table: String::new(),
4212                                    column: "collection".to_string(),
4213                                },
4214                                op: CompareOp::Eq,
4215                                value: Value::text(collection),
4216                            })
4217                        } else {
4218                            None
4219                        };
4220                    self.parse_table_clauses(&mut query)?;
4221                    if let Some(collection_filter) = collection_filter {
4222                        let combined = match query.filter.take() {
4223                            Some(existing) => {
4224                                Filter::And(Box::new(collection_filter), Box::new(existing))
4225                            }
4226                            None => collection_filter,
4227                        };
4228                        query.where_expr = Some(filter_to_expr(&combined));
4229                        query.filter = Some(combined);
4230                    }
4231                    Ok(SqlCommand::Select(query))
4232                } else if self.consume_ident_ci("STATS")? {
4233                    let mut query = TableQuery::new("red.stats");
4234                    let collection = match self.peek().clone() {
4235                        Token::Ident(name) => {
4236                            self.advance()?;
4237                            Some(name)
4238                        }
4239                        Token::String(name) => {
4240                            self.advance()?;
4241                            Some(name)
4242                        }
4243                        _ => None,
4244                    };
4245                    self.parse_table_clauses(&mut query)?;
4246                    if let Some(collection) = collection {
4247                        let filter = Filter::compare(
4248                            FieldRef::column("red.stats", "collection"),
4249                            CompareOp::Eq,
4250                            Value::text(collection),
4251                        );
4252                        let expr = filter_to_expr(&filter);
4253                        query.where_expr = Some(match query.where_expr.take() {
4254                            Some(existing) => Expr::binop(BinOp::And, existing, expr),
4255                            None => expr,
4256                        });
4257                        query.filter = Some(match query.filter.take() {
4258                            Some(existing) => existing.and(filter),
4259                            None => filter,
4260                        });
4261                    }
4262                    Ok(SqlCommand::Select(query))
4263                } else if self.consume_ident_ci("SAMPLE")? {
4264                    let mut query = TableQuery::new(&self.expect_ident()?);
4265                    query.limit = if self.consume(&Token::Limit)? {
4266                        Some(self.parse_integer()? as u64)
4267                    } else {
4268                        Some(10)
4269                    };
4270                    Ok(SqlCommand::Select(query))
4271                } else if self.consume_ident_ci("SECRET")? || self.consume_ident_ci("SECRETS")? {
4272                    let prefix = if !self.check(&Token::Eof) {
4273                        Some(Self::normalize_secret_admin_path(
4274                            self.parse_dotted_admin_path(true)?,
4275                        ))
4276                    } else {
4277                        None
4278                    };
4279                    Ok(SqlCommand::ShowSecrets { prefix })
4280                } else if self.consume_ident_ci("TENANT")? {
4281                    Ok(SqlCommand::ShowTenant)
4282                } else if let Some(expr) = self.parse_show_iam_after_show()? {
4283                    Ok(SqlCommand::IamPolicy(expr))
4284                } else {
4285                    Err(ParseError::expected(
4286                        vec![
4287                            "CONFIG",
4288                            "SECRET",
4289                            "SECRETS",
4290                            "COLLECTIONS",
4291                            "TABLES",
4292                            "QUEUES",
4293                            "VECTORS",
4294                            "DOCUMENTS",
4295                            "TIMESERIES",
4296                            "GRAPHS",
4297                            "KV",
4298                            "SCHEMA",
4299                            "INDICES",
4300                            "INDEXES",
4301                            "SAMPLE",
4302                            "POLICIES",
4303                            "STATS",
4304                            "TENANT",
4305                            "EFFECTIVE",
4306                        ],
4307                        self.peek(),
4308                        self.position(),
4309                    ))
4310                }
4311            }
4312            // Transaction control statements (Phase 1.1 PG parity).
4313            // BEGIN [WORK | TRANSACTION] [ISOLATION LEVEL <mode>]
4314            // START TRANSACTION [ISOLATION LEVEL <mode>]
4315            //
4316            // We only implement SNAPSHOT ISOLATION (our default). We
4317            // accept READ UNCOMMITTED / READ COMMITTED / REPEATABLE
4318            // READ / SNAPSHOT as PG-compatible no-ops, but reject
4319            // SERIALIZABLE outright — the previous behaviour of
4320            // silently degrading to snapshot made the parser
4321            // dishonest. Real SSI (Serializable Snapshot Isolation)
4322            // is tracked as a future milestone.
4323            Token::Begin | Token::Start => {
4324                self.advance()?;
4325                let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4326                // Optional ISOLATION LEVEL clause.
4327                if self.consume_ident_ci("ISOLATION")? {
4328                    self.expect(Token::Level)?;
4329                    // The level identifier can span multiple words
4330                    // (READ UNCOMMITTED / READ COMMITTED / REPEATABLE
4331                    // READ). Collect them case-insensitively.
4332                    let mut parts: Vec<String> = Vec::new();
4333                    if self.consume_ident_ci("READ")? {
4334                        parts.push("READ".to_string());
4335                        if self.consume_ident_ci("UNCOMMITTED")? {
4336                            parts.push("UNCOMMITTED".to_string());
4337                        } else if self.consume_ident_ci("COMMITTED")? {
4338                            parts.push("COMMITTED".to_string());
4339                        } else {
4340                            return Err(ParseError::expected(
4341                                vec!["UNCOMMITTED", "COMMITTED"],
4342                                self.peek(),
4343                                self.position(),
4344                            ));
4345                        }
4346                    } else if self.consume_ident_ci("REPEATABLE")? {
4347                        parts.push("REPEATABLE".to_string());
4348                        if !self.consume_ident_ci("READ")? {
4349                            return Err(ParseError::expected(
4350                                vec!["READ"],
4351                                self.peek(),
4352                                self.position(),
4353                            ));
4354                        }
4355                        parts.push("READ".to_string());
4356                    } else if self.consume_ident_ci("SNAPSHOT")? {
4357                        parts.push("SNAPSHOT".to_string());
4358                    } else if self.consume_ident_ci("SERIALIZABLE")? {
4359                        return Err(ParseError::new(
4360                            "ISOLATION LEVEL SERIALIZABLE is not yet supported — reddb \
4361                             currently provides SNAPSHOT ISOLATION (which PG calls \
4362                             REPEATABLE READ). Use REPEATABLE READ / SNAPSHOT / \
4363                             READ COMMITTED, or omit ISOLATION LEVEL for the default."
4364                                .to_string(),
4365                            self.position(),
4366                        ));
4367                    } else {
4368                        return Err(ParseError::expected(
4369                            vec!["READ", "REPEATABLE", "SNAPSHOT", "SERIALIZABLE"],
4370                            self.peek(),
4371                            self.position(),
4372                        ));
4373                    }
4374                    // All accepted modes map to our snapshot engine today.
4375                    let _ = parts;
4376                }
4377                Ok(SqlCommand::TransactionControl(TxnControl::Begin))
4378            }
4379            // COMMIT [WORK | TRANSACTION]
4380            Token::Commit => {
4381                self.advance()?;
4382                let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4383                Ok(SqlCommand::TransactionControl(TxnControl::Commit))
4384            }
4385            // ROLLBACK [WORK | TRANSACTION] [TO [SAVEPOINT] name]
4386            // ROLLBACK MIGRATION name
4387            Token::Rollback => {
4388                self.advance()?;
4389                if matches!(self.peek(), Token::Ident(n) if n.eq_ignore_ascii_case("MIGRATION")) {
4390                    match self.parse_rollback_migration_after_keyword()? {
4391                        QueryExpr::RollbackMigration(q) => Ok(SqlCommand::RollbackMigration(q)),
4392                        other => Err(ParseError::new(
4393                            format!(
4394                                "internal: ROLLBACK MIGRATION produced unexpected kind {other:?}"
4395                            ),
4396                            self.position(),
4397                        )),
4398                    }
4399                } else {
4400                    let _ = self.consume(&Token::Work)? || self.consume(&Token::Transaction)?;
4401                    if self.consume(&Token::To)? {
4402                        let _ = self.consume(&Token::Savepoint)?;
4403                        let name = self.expect_ident()?;
4404                        Ok(SqlCommand::TransactionControl(
4405                            TxnControl::RollbackToSavepoint(name),
4406                        ))
4407                    } else {
4408                        Ok(SqlCommand::TransactionControl(TxnControl::Rollback))
4409                    }
4410                }
4411            }
4412            // SAVEPOINT name
4413            Token::Savepoint => {
4414                self.advance()?;
4415                let name = self.expect_ident()?;
4416                Ok(SqlCommand::TransactionControl(TxnControl::Savepoint(name)))
4417            }
4418            // RELEASE [SAVEPOINT] name
4419            Token::Release => {
4420                self.advance()?;
4421                let _ = self.consume(&Token::Savepoint)?;
4422                let name = self.expect_ident()?;
4423                Ok(SqlCommand::TransactionControl(
4424                    TxnControl::ReleaseSavepoint(name),
4425                ))
4426            }
4427            // VACUUM [FULL] [table]
4428            Token::Vacuum => {
4429                self.advance()?;
4430                let full = self.consume(&Token::Full)?;
4431                let target = if self.check(&Token::Eof) {
4432                    None
4433                } else {
4434                    Some(self.expect_ident()?)
4435                };
4436                Ok(SqlCommand::Maintenance(MaintenanceCommand::Vacuum {
4437                    target,
4438                    full,
4439                }))
4440            }
4441            // REFRESH MATERIALIZED VIEW name
4442            Token::Refresh => {
4443                self.advance()?;
4444                self.expect(Token::Materialized)?;
4445                self.expect(Token::View)?;
4446                let name = self.expect_ident()?;
4447                Ok(SqlCommand::RefreshMaterializedView(
4448                    RefreshMaterializedViewQuery { name },
4449                ))
4450            }
4451            // ANALYZE [table]
4452            Token::Analyze => {
4453                self.advance()?;
4454                let target = if self.check(&Token::Eof) {
4455                    None
4456                } else {
4457                    Some(self.expect_ident()?)
4458                };
4459                Ok(SqlCommand::Maintenance(MaintenanceCommand::Analyze {
4460                    target,
4461                }))
4462            }
4463            // COPY table FROM 'path' [WITH (...)] [DELIMITER 'x'] [HEADER [true|false]]
4464            //
4465            // Accepts both PG-style `WITH (FORMAT csv, HEADER true)` and the
4466            // short-form `DELIMITER ',' HEADER`. The only supported format
4467            // today is CSV.
4468            Token::Copy => {
4469                self.advance()?;
4470                let table = self.expect_ident()?;
4471                self.expect(Token::From)?;
4472                let path = self.parse_string()?;
4473
4474                let mut delimiter: Option<char> = None;
4475                let mut has_header = false;
4476                let format = CopyFormat::Csv;
4477
4478                // Optional `WITH (FORMAT csv, HEADER true, DELIMITER ',')` block.
4479                // `WITH` is a reserved keyword token — accept both the keyword
4480                // form and the ident form that non-CTE callers sometimes emit.
4481                if self.consume(&Token::With)? || self.consume_ident_ci("WITH")? {
4482                    self.expect(Token::LParen)?;
4483                    loop {
4484                        if self.consume(&Token::Format)? || self.consume_ident_ci("FORMAT")? {
4485                            let _ = self.consume(&Token::Eq)?;
4486                            // Only CSV for now — accept the ident and move on.
4487                            let _ = self.expect_ident()?;
4488                        } else if self.consume(&Token::Header)? {
4489                            let _ = self.consume(&Token::Eq)?;
4490                            // Accept `HEADER`, `HEADER = true`, `HEADER = false`,
4491                            // or an ident spelling of true/false.
4492                            has_header = match self.peek().clone() {
4493                                Token::True => {
4494                                    self.advance()?;
4495                                    true
4496                                }
4497                                Token::False => {
4498                                    self.advance()?;
4499                                    false
4500                                }
4501                                Token::Ident(ref n) if n.eq_ignore_ascii_case("true") => {
4502                                    self.advance()?;
4503                                    true
4504                                }
4505                                Token::Ident(ref n) if n.eq_ignore_ascii_case("false") => {
4506                                    self.advance()?;
4507                                    false
4508                                }
4509                                _ => true,
4510                            };
4511                        } else if self.consume(&Token::Delimiter)? {
4512                            let _ = self.consume(&Token::Eq)?;
4513                            let s = self.parse_string()?;
4514                            delimiter = s.chars().next();
4515                        } else {
4516                            break;
4517                        }
4518                        if !self.consume(&Token::Comma)? {
4519                            break;
4520                        }
4521                    }
4522                    self.expect(Token::RParen)?;
4523                }
4524
4525                // Short form clauses outside WITH (in either order).
4526                loop {
4527                    if self.consume(&Token::Delimiter)? {
4528                        let s = self.parse_string()?;
4529                        delimiter = s.chars().next();
4530                    } else if self.consume(&Token::Header)? {
4531                        has_header = true;
4532                    } else {
4533                        break;
4534                    }
4535                }
4536
4537                Ok(SqlCommand::CopyFrom(CopyFromQuery {
4538                    table,
4539                    path,
4540                    format,
4541                    delimiter,
4542                    has_header,
4543                }))
4544            }
4545            other => Err(ParseError::expected(
4546                vec![
4547                    "SELECT",
4548                    "FROM",
4549                    "INSERT",
4550                    "UPDATE",
4551                    "DELETE",
4552                    "EXPLAIN",
4553                    "CREATE",
4554                    "DROP",
4555                    "ALTER",
4556                    "SET",
4557                    "SHOW",
4558                    "BEGIN",
4559                    "COMMIT",
4560                    "ROLLBACK",
4561                    "SAVEPOINT",
4562                    "RELEASE",
4563                    "START",
4564                    "VACUUM",
4565                    "ANALYZE",
4566                    "COPY",
4567                    "REFRESH",
4568                    "DESCRIBE",
4569                    "DESC",
4570                ],
4571                other,
4572                self.position(),
4573            )),
4574        }
4575    }
4576}