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