Skip to main content

reddb_server/storage/query/
core.rs

1use std::fmt;
2
3use super::builders::{GraphQueryBuilder, PathQueryBuilder, TableQueryBuilder};
4use crate::catalog::CollectionModel;
5pub use crate::storage::engine::distance::DistanceMetric;
6pub use crate::storage::engine::vector_metadata::MetadataFilter;
7pub use crate::storage::queue::QueueMode;
8use crate::storage::schema::{SqlTypeName, Value};
9
10/// Root query expression
11#[derive(Debug, Clone)]
12#[allow(clippy::large_enum_variant)]
13pub enum QueryExpr {
14    /// Pure table query: SELECT ... FROM ...
15    Table(TableQuery),
16    /// Pure graph query: MATCH ... RETURN ...
17    Graph(GraphQuery),
18    /// Join between table and graph
19    Join(JoinQuery),
20    /// Path query: PATH FROM ... TO ...
21    Path(PathQuery),
22    /// Vector similarity search
23    Vector(VectorQuery),
24    /// Hybrid query combining structured and vector search
25    Hybrid(HybridQuery),
26    /// INSERT INTO table (cols) VALUES (vals)
27    Insert(InsertQuery),
28    /// UPDATE table SET col=val WHERE filter
29    Update(UpdateQuery),
30    /// DELETE FROM table WHERE filter
31    Delete(DeleteQuery),
32    /// CREATE TABLE name (columns)
33    CreateTable(CreateTableQuery),
34    /// CREATE COLLECTION name KIND kind
35    CreateCollection(CreateCollectionQuery),
36    /// CREATE VECTOR name DIM n [METRIC metric]
37    CreateVector(CreateVectorQuery),
38    /// DROP TABLE name
39    DropTable(DropTableQuery),
40    /// DROP GRAPH name
41    DropGraph(DropGraphQuery),
42    /// DROP VECTOR name
43    DropVector(DropVectorQuery),
44    /// DROP DOCUMENT name
45    DropDocument(DropDocumentQuery),
46    /// DROP KV name
47    DropKv(DropKvQuery),
48    /// DROP COLLECTION name
49    DropCollection(DropCollectionQuery),
50    /// TRUNCATE [model] name
51    Truncate(TruncateQuery),
52    /// ALTER TABLE name ADD/DROP/RENAME COLUMN
53    AlterTable(AlterTableQuery),
54    /// GRAPH subcommand (NEIGHBORHOOD, SHORTEST_PATH, etc.)
55    GraphCommand(GraphCommand),
56    /// SEARCH subcommand (SIMILAR, TEXT, HYBRID)
57    SearchCommand(SearchCommand),
58    /// ASK 'question' — RAG query with LLM synthesis
59    Ask(AskQuery),
60    /// CREATE INDEX name ON table (columns) USING type
61    CreateIndex(CreateIndexQuery),
62    /// DROP INDEX name ON table
63    DropIndex(DropIndexQuery),
64    /// Probabilistic data structure commands (HLL, SKETCH, FILTER)
65    ProbabilisticCommand(ProbabilisticCommand),
66    /// CREATE TIMESERIES name [RETENTION duration] [CHUNK_SIZE n]
67    CreateTimeSeries(CreateTimeSeriesQuery),
68    /// DROP TIMESERIES name
69    DropTimeSeries(DropTimeSeriesQuery),
70    /// CREATE QUEUE name [MAX_SIZE n] [PRIORITY] [WITH TTL duration]
71    CreateQueue(CreateQueueQuery),
72    /// ALTER QUEUE name SET MODE [FANOUT|WORK]
73    AlterQueue(AlterQueueQuery),
74    /// DROP QUEUE name
75    DropQueue(DropQueueQuery),
76    /// Read-only queue projection: SELECT ... FROM QUEUE name
77    QueueSelect(QueueSelectQuery),
78    /// QUEUE subcommand (PUSH, POP, PEEK, LEN, PURGE, GROUP, READ, ACK, NACK)
79    QueueCommand(QueueCommand),
80    /// KV subcommand (PUT, GET, DELETE)
81    KvCommand(KvCommand),
82    /// CONFIG keyed command (PUT, GET, ROTATE, DELETE, HISTORY)
83    ConfigCommand(ConfigCommand),
84    /// CREATE TREE name IN collection ROOT ... MAX_CHILDREN n
85    CreateTree(CreateTreeQuery),
86    /// DROP TREE name IN collection
87    DropTree(DropTreeQuery),
88    /// TREE subcommand (INSERT, MOVE, DELETE, VALIDATE, REBALANCE)
89    TreeCommand(TreeCommand),
90    /// SET CONFIG key = value
91    SetConfig { key: String, value: Value },
92    /// SHOW CONFIG [prefix]
93    ShowConfig { prefix: Option<String> },
94    /// SET SECRET key = value
95    SetSecret { key: String, value: Value },
96    /// DELETE SECRET key
97    DeleteSecret { key: String },
98    /// SHOW SECRET[S] [prefix]
99    ShowSecrets { prefix: Option<String> },
100    /// `SET TENANT 'id'` / `SET TENANT = 'id'` / `RESET TENANT`
101    ///
102    /// Session-scoped multi-tenancy handle. Populates a per-connection
103    /// thread-local that `CURRENT_TENANT()` reads and that RLS
104    /// policies combine with via `USING (tenant_id = CURRENT_TENANT())`.
105    /// `None` clears the current tenant (RESET TENANT or SET TENANT
106    /// NULL). Unlike `SetConfig` this is *not* persisted to red_config —
107    /// it lives for the connection's lifetime only.
108    SetTenant(Option<String>),
109    /// `SHOW TENANT` — returns the thread-local tenant id (or NULL).
110    ShowTenant,
111    /// EXPLAIN ALTER FOR CREATE TABLE name (...) [FORMAT JSON]
112    ///
113    /// Pure read command that diffs the embedded `CREATE TABLE`
114    /// statement against the live `CollectionContract` of the
115    /// table with the same name and returns the `ALTER TABLE`
116    /// operations that would close the gap. Never executes
117    /// anything — output is text (default) or JSON depending on
118    /// the optional `FORMAT JSON` suffix. Powers the Purple
119    /// framework's migration generator and any other client that
120    /// wants reddb to own the schema-diff rules.
121    ExplainAlter(ExplainAlterQuery),
122    /// CREATE MIGRATION name [DEPENDS ON dep1, dep2] [BATCH n ROWS] [NO ROLLBACK] body
123    CreateMigration(CreateMigrationQuery),
124    /// APPLY MIGRATION name | APPLY MIGRATION *  [FOR TENANT id]
125    ApplyMigration(ApplyMigrationQuery),
126    /// ROLLBACK MIGRATION name
127    RollbackMigration(RollbackMigrationQuery),
128    /// EXPLAIN MIGRATION name
129    ExplainMigration(ExplainMigrationQuery),
130    /// `EVENTS BACKFILL collection [WHERE pred] TO queue [LIMIT n]`.
131    EventsBackfill(EventsBackfillQuery),
132    /// `EVENTS BACKFILL STATUS collection` placeholder for the status slice.
133    EventsBackfillStatus { collection: String },
134    /// Transaction control: BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE, ROLLBACK TO.
135    ///
136    /// Phase 1.1 (PG parity): parser + dispatch are wired so clients (psql, JDBC, etc.)
137    /// can issue these statements without errors. Real isolation/atomicity semantics
138    /// arrive with Phase 2.3 MVCC. Until then statements behave as autocommit (each
139    /// DML is its own transaction); BEGIN/COMMIT/ROLLBACK return success but do NOT
140    /// provide rollback-on-failure guarantees across multiple statements.
141    TransactionControl(TxnControl),
142    /// Maintenance commands: VACUUM [FULL] [table], ANALYZE [table].
143    ///
144    /// Phase 1.2 (PG parity): `VACUUM` triggers segment/page flush + planner stats
145    /// refresh. `ANALYZE` refreshes planner statistics (histograms, null counts,
146    /// distinct estimates). Both accept an optional table target; omitting the
147    /// target iterates every collection.
148    MaintenanceCommand(MaintenanceCommand),
149    /// `CREATE SCHEMA [IF NOT EXISTS] name`
150    ///
151    /// Phase 1.3 (PG parity): schemas are logical namespaces stored in
152    /// `red_config` under the key `schema.{name}`. Tables created inside a
153    /// schema use `schema.table` qualified names (collection name = "schema.table").
154    CreateSchema(CreateSchemaQuery),
155    /// `DROP SCHEMA [IF EXISTS] name [CASCADE]`
156    DropSchema(DropSchemaQuery),
157    /// `CREATE SEQUENCE [IF NOT EXISTS] name [START [WITH] n] [INCREMENT [BY] n]`
158    ///
159    /// Phase 1.3 (PG parity): sequences are 64-bit monotonic counters persisted
160    /// in `red_config` under the key `sequence.{name}`. Values are produced by
161    /// the scalar functions `nextval('name')` and `currval('name')`.
162    CreateSequence(CreateSequenceQuery),
163    /// `DROP SEQUENCE [IF EXISTS] name`
164    DropSequence(DropSequenceQuery),
165    /// `COPY table FROM 'path' [WITH ...]` — CSV import (Phase 1.5 PG parity).
166    ///
167    /// Supported options: `DELIMITER c`, `HEADER [true|false]`. Rows stream
168    /// into the named collection via the `CsvImporter`.
169    CopyFrom(CopyFromQuery),
170    /// `CREATE [OR REPLACE] [MATERIALIZED] VIEW [IF NOT EXISTS] name AS SELECT ...`
171    ///
172    /// Phase 2.1 (PG parity): views are stored as `view.{name}` entries in
173    /// `red_config`. Materialized views additionally allocate a slot in the
174    /// shared `MaterializedViewCache`; `REFRESH MATERIALIZED VIEW` re-runs
175    /// the underlying query and repopulates the cache.
176    CreateView(CreateViewQuery),
177    /// `DROP [MATERIALIZED] VIEW [IF EXISTS] name`
178    DropView(DropViewQuery),
179    /// `REFRESH MATERIALIZED VIEW name`
180    ///
181    /// Re-executes the view's query and writes the result into the cache.
182    RefreshMaterializedView(RefreshMaterializedViewQuery),
183    /// `CREATE POLICY name ON table [FOR action] [TO role] USING (filter)`
184    ///
185    /// Phase 2.5 (PG parity): row-level security policy definition.
186    /// Evaluated at read time — when the table has RLS enabled, all
187    /// matching policies for the current role are combined with OR and
188    /// AND-ed into the query's WHERE clause.
189    CreatePolicy(CreatePolicyQuery),
190    /// `DROP POLICY [IF EXISTS] name ON table`
191    DropPolicy(DropPolicyQuery),
192    /// `CREATE SERVER name FOREIGN DATA WRAPPER kind OPTIONS (...)`
193    /// (Phase 3.2 PG parity). Registers a named foreign-data-wrapper
194    /// instance in the runtime's `ForeignTableRegistry`.
195    CreateServer(CreateServerQuery),
196    /// `DROP SERVER [IF EXISTS] name [CASCADE]`
197    DropServer(DropServerQuery),
198    /// `CREATE FOREIGN TABLE name (cols) SERVER srv OPTIONS (...)`
199    /// (Phase 3.2 PG parity). Makes `name` resolvable as a foreign table
200    /// via the parent server's `ForeignDataWrapper`.
201    CreateForeignTable(CreateForeignTableQuery),
202    /// `DROP FOREIGN TABLE [IF EXISTS] name`
203    DropForeignTable(DropForeignTableQuery),
204    /// `GRANT { actions | ALL [PRIVILEGES] }
205    ///   ON { TABLE | SCHEMA | DATABASE | FUNCTION } object_list
206    ///   TO grant_principal_list
207    ///   [WITH GRANT OPTION]`
208    ///
209    /// Granular RBAC primitive layered on top of the legacy 3-role model.
210    /// See `crate::auth::privileges` for the resolution algorithm.
211    Grant(GrantStmt),
212    /// `REVOKE [GRANT OPTION FOR] { actions | ALL } ON … FROM …`
213    Revoke(RevokeStmt),
214    /// `ALTER USER name [VALID UNTIL 'ts'] [CONNECTION LIMIT n]
215    ///   [ENABLE | DISABLE] [SET search_path = ...]`
216    AlterUser(AlterUserStmt),
217    // ----- IAM policy DDL (Agent #28 / IAM kernel integration) -----
218    /// `CREATE POLICY '<id>' AS '<json>'` — installs an IAM policy
219    /// document in the AuthStore. Distinct from the RLS-flavoured
220    /// `CreatePolicy(CreatePolicyQuery)` above (which uses
221    /// `CREATE POLICY name ON table ...`); the parser disambiguates
222    /// at parse time by inspecting the token after the policy name.
223    CreateIamPolicy { id: String, json: String },
224    /// `DROP POLICY '<id>'` — removes an IAM policy and its
225    /// attachments.
226    DropIamPolicy { id: String },
227    /// `ATTACH POLICY '<id>' TO USER <name>` /
228    /// `ATTACH POLICY '<id>' TO GROUP <name>`.
229    AttachPolicy {
230        policy_id: String,
231        principal: PolicyPrincipalRef,
232    },
233    /// `DETACH POLICY '<id>' FROM USER <name>` /
234    /// `DETACH POLICY '<id>' FROM GROUP <name>`.
235    DetachPolicy {
236        policy_id: String,
237        principal: PolicyPrincipalRef,
238    },
239    /// `SHOW POLICIES [FOR USER <name> | FOR GROUP <name>]`.
240    ShowPolicies { filter: Option<PolicyPrincipalRef> },
241    /// `SHOW EFFECTIVE PERMISSIONS FOR <name> [ON <kind>:<name>]`.
242    ShowEffectivePermissions {
243        user: PolicyUserRef,
244        resource: Option<PolicyResourceRef>,
245    },
246    /// `SIMULATE <name> ACTION <verb> ON <kind>:<name>`.
247    SimulatePolicy {
248        user: PolicyUserRef,
249        action: String,
250        resource: PolicyResourceRef,
251    },
252}
253
254/// Tenant-qualified user reference for IAM policy SQL DDL.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct PolicyUserRef {
257    pub tenant: Option<String>,
258    pub username: String,
259}
260
261/// Resource reference (`<kind>:<name>`) used in `SIMULATE` /
262/// `SHOW EFFECTIVE PERMISSIONS`.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct PolicyResourceRef {
265    pub kind: String,
266    pub name: String,
267}
268
269/// Principal target for ATTACH / DETACH / SHOW POLICIES filter.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub enum PolicyPrincipalRef {
272    User(PolicyUserRef),
273    Group(String),
274}
275
276// ---------------------------------------------------------------------------
277// GRANT / REVOKE / ALTER USER AST
278// ---------------------------------------------------------------------------
279
280/// Object class targeted by a GRANT/REVOKE.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum GrantObjectKind {
283    Table,
284    Schema,
285    Database,
286    Function,
287}
288
289/// One target object in a `GRANT ... ON ... <object_list>` clause.
290///
291/// `name` follows the parser's standard `[schema.]object` shape; the
292/// optional `schema` is only populated for `Table` / `Function` and
293/// stays `None` when the user wrote a bare identifier.
294#[derive(Debug, Clone)]
295pub struct GrantObject {
296    pub schema: Option<String>,
297    pub name: String,
298}
299
300/// Principal target of a GRANT (i.e. the recipient).
301#[derive(Debug, Clone)]
302pub enum GrantPrincipalRef {
303    /// `TO username` — username may include an `@tenant` suffix.
304    User {
305        tenant: Option<String>,
306        name: String,
307    },
308    /// `TO PUBLIC`.
309    Public,
310    /// `TO GROUP groupname` (parsed today, enforcement deferred).
311    Group(String),
312}
313
314/// `GRANT` statement AST.
315#[derive(Debug, Clone)]
316pub struct GrantStmt {
317    /// Privilege keywords as the user typed them, normalised to upper
318    /// case. Matches the `Action` set in `crate::auth::privileges`. An
319    /// empty list together with `all = true` represents `ALL [PRIVILEGES]`.
320    pub actions: Vec<String>,
321    /// Optional column list — populates the AST for column-level
322    /// grants but enforcement is deferred (stretch goal).
323    pub columns: Option<Vec<String>>,
324    pub object_kind: GrantObjectKind,
325    pub objects: Vec<GrantObject>,
326    pub principals: Vec<GrantPrincipalRef>,
327    pub with_grant_option: bool,
328    /// `true` when the privilege list was `ALL [PRIVILEGES]`.
329    pub all: bool,
330}
331
332/// `REVOKE` statement AST.
333#[derive(Debug, Clone)]
334pub struct RevokeStmt {
335    pub actions: Vec<String>,
336    pub columns: Option<Vec<String>>,
337    pub object_kind: GrantObjectKind,
338    pub objects: Vec<GrantObject>,
339    pub principals: Vec<GrantPrincipalRef>,
340    /// `REVOKE GRANT OPTION FOR ...` — strips just the grant option,
341    /// keeping the underlying privilege.
342    pub grant_option_for: bool,
343    pub all: bool,
344}
345
346/// One attribute setting under `ALTER USER`.
347#[derive(Debug, Clone)]
348pub enum AlterUserAttribute {
349    ValidUntil(String),
350    ConnectionLimit(i64),
351    Enable,
352    Disable,
353    SetSearchPath(String),
354    AddGroup(String),
355    DropGroup(String),
356    /// Reset password (carry the new plaintext until the runtime
357    /// hands it to AuthStore::change_password). Out of scope for the
358    /// initial milestone — present so the parser can accept the
359    /// keyword without a follow-up grammar change.
360    Password(String),
361}
362
363/// `ALTER USER` statement AST.
364#[derive(Debug, Clone)]
365pub struct AlterUserStmt {
366    pub tenant: Option<String>,
367    pub username: String,
368    pub attributes: Vec<AlterUserAttribute>,
369}
370
371#[derive(Debug, Clone)]
372pub struct CreateServerQuery {
373    pub name: String,
374    /// Wrapper kind declared in `FOREIGN DATA WRAPPER <kind>`.
375    pub wrapper: String,
376    /// Generic `(key 'value', ...)` option bag.
377    pub options: Vec<(String, String)>,
378    pub if_not_exists: bool,
379}
380
381#[derive(Debug, Clone)]
382pub struct DropServerQuery {
383    pub name: String,
384    pub if_exists: bool,
385    pub cascade: bool,
386}
387
388#[derive(Debug, Clone)]
389pub struct CreateForeignTableQuery {
390    pub name: String,
391    pub server: String,
392    pub columns: Vec<ForeignColumnDef>,
393    pub options: Vec<(String, String)>,
394    pub if_not_exists: bool,
395}
396
397#[derive(Debug, Clone)]
398pub struct ForeignColumnDef {
399    pub name: String,
400    pub data_type: String,
401    pub not_null: bool,
402}
403
404#[derive(Debug, Clone)]
405pub struct DropForeignTableQuery {
406    pub name: String,
407    pub if_exists: bool,
408}
409
410/// Row-level security policy definition.
411#[derive(Debug, Clone)]
412pub struct CreatePolicyQuery {
413    pub name: String,
414    pub table: String,
415    /// Which action this policy gates. `None` = `ALL` (applies to all four).
416    pub action: Option<PolicyAction>,
417    /// Role the policy applies to. `None` = all roles.
418    pub role: Option<String>,
419    /// Boolean predicate the row must satisfy.
420    pub using: Box<Filter>,
421    /// Entity kind this policy targets (Phase 2.5.5 RLS universal).
422    /// `CREATE POLICY p ON t ...` defaults to `Table`; writing
423    /// `ON NODES OF g` / `ON VECTORS OF v` / `ON MESSAGES OF q` /
424    /// `ON POINTS OF ts` / `ON EDGES OF g` targets the matching
425    /// non-tabular kind. The evaluator filters polices by kind so
426    /// a graph policy only gates graph reads, vector policy only
427    /// gates vector reads, etc.
428    pub target_kind: PolicyTargetKind,
429}
430
431/// Which flavour of entity a policy governs (Phase 2.5.5).
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
433pub enum PolicyTargetKind {
434    Table,
435    Nodes,
436    Edges,
437    Vectors,
438    Messages,
439    Points,
440    Documents,
441}
442
443impl PolicyTargetKind {
444    /// Lowercase identifier for UX — used in messages and the
445    /// `red_config.rls.policies.*` persistence key.
446    pub fn as_ident(&self) -> &'static str {
447        match self {
448            Self::Table => "table",
449            Self::Nodes => "nodes",
450            Self::Edges => "edges",
451            Self::Vectors => "vectors",
452            Self::Messages => "messages",
453            Self::Points => "points",
454            Self::Documents => "documents",
455        }
456    }
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub enum PolicyAction {
461    Select,
462    Insert,
463    Update,
464    Delete,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq)]
468pub struct DropPolicyQuery {
469    pub name: String,
470    pub table: String,
471    pub if_exists: bool,
472}
473
474#[derive(Debug, Clone)]
475pub struct CreateViewQuery {
476    pub name: String,
477    /// Parsed `SELECT ...` body. Stored as a boxed `QueryExpr` so the
478    /// runtime can substitute the tree directly when a query references
479    /// this view (no re-parsing per read).
480    pub query: Box<QueryExpr>,
481    pub materialized: bool,
482    pub if_not_exists: bool,
483    /// `CREATE OR REPLACE VIEW` — overwrites any existing definition.
484    pub or_replace: bool,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct DropViewQuery {
489    pub name: String,
490    pub materialized: bool,
491    pub if_exists: bool,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct RefreshMaterializedViewQuery {
496    pub name: String,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct CopyFromQuery {
501    pub table: String,
502    pub path: String,
503    pub format: CopyFormat,
504    pub delimiter: Option<char>,
505    pub has_header: bool,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
509pub enum CopyFormat {
510    Csv,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct CreateSchemaQuery {
515    pub name: String,
516    pub if_not_exists: bool,
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
520pub struct DropSchemaQuery {
521    pub name: String,
522    pub if_exists: bool,
523    pub cascade: bool,
524}
525
526#[derive(Debug, Clone, PartialEq, Eq)]
527pub struct CreateSequenceQuery {
528    pub name: String,
529    pub if_not_exists: bool,
530    /// First value produced by `nextval`. Default 1.
531    pub start: i64,
532    /// Added to the current value on each `nextval`. Default 1.
533    pub increment: i64,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct DropSequenceQuery {
538    pub name: String,
539    pub if_exists: bool,
540}
541
542/// Transaction-control statement variants. See [`QueryExpr::TransactionControl`].
543#[derive(Debug, Clone, PartialEq, Eq)]
544pub enum TxnControl {
545    /// `BEGIN [WORK | TRANSACTION]`, `START TRANSACTION`
546    Begin,
547    /// `COMMIT [WORK | TRANSACTION]`, `END`
548    Commit,
549    /// `ROLLBACK [WORK | TRANSACTION]`
550    Rollback,
551    /// `SAVEPOINT name`
552    Savepoint(String),
553    /// `RELEASE [SAVEPOINT] name`
554    ReleaseSavepoint(String),
555    /// `ROLLBACK TO [SAVEPOINT] name`
556    RollbackToSavepoint(String),
557}
558
559/// Maintenance command variants. See [`QueryExpr::MaintenanceCommand`].
560#[derive(Debug, Clone, PartialEq, Eq)]
561pub enum MaintenanceCommand {
562    /// `VACUUM [FULL] [table]`
563    ///
564    /// Triggers segment compaction and planner stats refresh. `FULL` additionally
565    /// forces a full pager sync. Target `None` applies to every collection.
566    Vacuum { target: Option<String>, full: bool },
567    /// `ANALYZE [table]`
568    ///
569    /// Refreshes planner statistics (histogram, distinct estimates, null counts).
570    /// Target `None` re-analyzes every collection.
571    Analyze { target: Option<String> },
572}
573
574/// AST node for `EXPLAIN ALTER FOR <CreateTableStmt> [FORMAT JSON]`.
575///
576/// `target` carries the CREATE TABLE structure exactly as the
577/// parser produces it for a regular CREATE — full reuse of
578/// `parse_create_table_body`. `format` determines whether the
579/// executor emits a `ALTER TABLE …;`-flavored text payload
580/// (the default — copy-paste friendly into the REPL) or a
581/// structured JSON object (machine-friendly).
582#[derive(Debug, Clone)]
583pub struct ExplainAlterQuery {
584    pub target: CreateTableQuery,
585    pub format: ExplainFormat,
586}
587
588/// Output format requested for an `EXPLAIN ALTER` command.
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
590pub enum ExplainFormat {
591    /// Plain SQL text — `ALTER TABLE …;` lines plus header
592    /// comments and rename hints. Default; copy-paste friendly.
593    #[default]
594    Sql,
595    /// Structured JSON object with `operations`,
596    /// `rename_candidates`, `summary`. Machine-friendly for
597    /// driver code (Purple migration generator, dashboards,
598    /// CLI tools).
599    Json,
600}
601
602#[derive(Debug, Clone, PartialEq)]
603pub struct CreateMigrationQuery {
604    pub name: String,
605    pub body: String,
606    pub depends_on: Vec<String>,
607    pub batch_size: Option<u64>,
608    pub no_rollback: bool,
609}
610
611#[derive(Debug, Clone, PartialEq)]
612pub struct ApplyMigrationQuery {
613    pub target: ApplyMigrationTarget,
614    pub for_tenant: Option<String>,
615}
616
617#[derive(Debug, Clone, PartialEq)]
618pub enum ApplyMigrationTarget {
619    Named(String),
620    All,
621}
622
623#[derive(Debug, Clone, PartialEq)]
624pub struct RollbackMigrationQuery {
625    pub name: String,
626}
627
628#[derive(Debug, Clone, PartialEq)]
629pub struct ExplainMigrationQuery {
630    pub name: String,
631}
632
633/// Probabilistic data structure commands
634#[derive(Debug, Clone)]
635pub enum ProbabilisticCommand {
636    // HyperLogLog
637    CreateHll {
638        name: String,
639        precision: u8,
640        if_not_exists: bool,
641    },
642    HllAdd {
643        name: String,
644        elements: Vec<String>,
645    },
646    HllCount {
647        names: Vec<String>,
648    },
649    HllMerge {
650        dest: String,
651        sources: Vec<String>,
652    },
653    HllInfo {
654        name: String,
655    },
656    DropHll {
657        name: String,
658        if_exists: bool,
659    },
660
661    // Count-Min Sketch (Fase 7)
662    CreateSketch {
663        name: String,
664        width: usize,
665        depth: usize,
666        if_not_exists: bool,
667    },
668    SketchAdd {
669        name: String,
670        element: String,
671        count: u64,
672    },
673    SketchCount {
674        name: String,
675        element: String,
676    },
677    SketchMerge {
678        dest: String,
679        sources: Vec<String>,
680    },
681    SketchInfo {
682        name: String,
683    },
684    DropSketch {
685        name: String,
686        if_exists: bool,
687    },
688
689    // Cuckoo Filter (Fase 8)
690    CreateFilter {
691        name: String,
692        capacity: usize,
693        if_not_exists: bool,
694    },
695    FilterAdd {
696        name: String,
697        element: String,
698    },
699    FilterCheck {
700        name: String,
701        element: String,
702    },
703    FilterDelete {
704        name: String,
705        element: String,
706    },
707    FilterCount {
708        name: String,
709    },
710    FilterInfo {
711        name: String,
712    },
713    DropFilter {
714        name: String,
715        if_exists: bool,
716    },
717}
718
719/// Index type for CREATE INDEX ... USING <type>
720#[derive(Debug, Clone, PartialEq, Eq)]
721pub enum IndexMethod {
722    BTree,
723    Hash,
724    Bitmap,
725    RTree,
726}
727
728impl fmt::Display for IndexMethod {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        match self {
731            Self::BTree => write!(f, "BTREE"),
732            Self::Hash => write!(f, "HASH"),
733            Self::Bitmap => write!(f, "BITMAP"),
734            Self::RTree => write!(f, "RTREE"),
735        }
736    }
737}
738
739/// CREATE INDEX [UNIQUE] [IF NOT EXISTS] name ON table (col1, col2, ...) [USING method]
740#[derive(Debug, Clone)]
741pub struct CreateIndexQuery {
742    pub name: String,
743    pub table: String,
744    pub columns: Vec<String>,
745    pub method: IndexMethod,
746    pub unique: bool,
747    pub if_not_exists: bool,
748}
749
750/// DROP INDEX [IF EXISTS] name ON table
751#[derive(Debug, Clone)]
752pub struct DropIndexQuery {
753    pub name: String,
754    pub table: String,
755    pub if_exists: bool,
756}
757
758/// ASK 'question' [USING provider] [MODEL 'model'] [DEPTH n] [LIMIT n] [MIN_SCORE x]
759///                [COLLECTION col] [TEMPERATURE x] [SEED n] [STRICT ON|OFF] [STREAM]
760///                [CACHE TTL '5m' | NOCACHE]
761///
762/// `temperature` and `seed` are per-query overrides resolved by the
763/// `DeterminismDecider` (issue #400). The parser merely surfaces the
764/// requested values; capability-based dropping happens at decide time.
765#[derive(Debug, Clone)]
766pub struct AskQuery {
767    /// `EXPLAIN ASK '...'` returns the retrieval/provider/cost plan
768    /// without making the LLM call.
769    pub explain: bool,
770    pub question: String,
771    /// Optional `$N` / `?` parameter slot for the question text.
772    pub question_param: Option<usize>,
773    pub provider: Option<String>,
774    pub model: Option<String>,
775    pub depth: Option<usize>,
776    pub limit: Option<usize>,
777    pub min_score: Option<f32>,
778    pub collection: Option<String>,
779    /// Per-query temperature override (`ASK '...' TEMPERATURE 0.7`).
780    /// `None` means fall back to `ask.default_temperature`.
781    pub temperature: Option<f32>,
782    /// Per-query seed override (`ASK '...' SEED 42`). `None` means the
783    /// decider derives one from `hash(question + sources_fingerprint)`.
784    pub seed: Option<u64>,
785    /// Strict citation validation is on by default. `STRICT OFF` keeps
786    /// citation diagnostics as warnings and skips retry/error handling.
787    pub strict: bool,
788    /// HTTP-only SSE response requested via `ASK '...' STREAM`.
789    pub stream: bool,
790    /// Per-query answer-cache override.
791    pub cache: AskCacheClause,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq)]
795pub enum AskCacheClause {
796    Default,
797    CacheTtl(String),
798    NoCache,
799}
800
801impl Default for AskCacheClause {
802    fn default() -> Self {
803        Self::Default
804    }
805}
806
807impl QueryExpr {
808    /// Create a table query
809    pub fn table(name: &str) -> TableQueryBuilder {
810        TableQueryBuilder::new(name)
811    }
812
813    /// Create a graph query
814    pub fn graph() -> GraphQueryBuilder {
815        GraphQueryBuilder::new()
816    }
817
818    /// Create a path query
819    pub fn path(from: NodeSelector, to: NodeSelector) -> PathQueryBuilder {
820        PathQueryBuilder::new(from, to)
821    }
822}
823
824// ============================================================================
825// Table Query
826// ============================================================================
827
828/// Table query: SELECT columns FROM table WHERE filter ORDER BY ... LIMIT ...
829#[derive(Debug, Clone)]
830pub struct TableQuery {
831    /// Table name. Legacy slot — still populated even when `source`
832    /// is set to a subquery so existing call sites that read
833    /// `query.table.as_str()` keep compiling. When `source` is
834    /// `Some(TableSource::Subquery(…))`, this field holds a synthetic
835    /// sentinel name (`"__subq_NNNN"`) that runtime code must never
836    /// resolve against the real schema registry.
837    pub table: String,
838    /// Fase 2 Week 3: structured table source. `None` means the
839    /// legacy `table` field is authoritative. `Some(Name)` is the
840    /// same information as `table` but in typed form. `Some(Subquery)`
841    /// wires a `(SELECT …) AS alias` in a FROM position — the Fase
842    /// 1.7 unlock.
843    pub source: Option<TableSource>,
844    /// Optional table alias
845    pub alias: Option<String>,
846    /// Canonical SQL select list.
847    pub select_items: Vec<SelectItem>,
848    /// Columns to select (empty = all)
849    pub columns: Vec<Projection>,
850    /// Canonical SQL WHERE clause.
851    pub where_expr: Option<super::Expr>,
852    /// Filter condition
853    pub filter: Option<Filter>,
854    /// Canonical SQL GROUP BY items.
855    pub group_by_exprs: Vec<super::Expr>,
856    /// GROUP BY fields
857    pub group_by: Vec<String>,
858    /// Canonical SQL HAVING clause.
859    pub having_expr: Option<super::Expr>,
860    /// HAVING filter (applied after grouping)
861    pub having: Option<Filter>,
862    /// Order by clauses
863    pub order_by: Vec<OrderByClause>,
864    /// Limit
865    pub limit: Option<u64>,
866    /// User-supplied-parameter slot for `LIMIT $N`. Set by the parser
867    /// when the LIMIT clause references `$N`/`?` instead of a literal;
868    /// cleared by the binder (`user_params::bind`) after substituting
869    /// the parameter into `limit`. Mirrors the `limit_param` slot on
870    /// `SearchCommand` variants — see #361 slice 11.
871    pub limit_param: Option<usize>,
872    /// Offset
873    pub offset: Option<u64>,
874    /// User-supplied-parameter slot for `OFFSET $N`. Same lifecycle as
875    /// `limit_param`. See #361 slice 11.
876    pub offset_param: Option<usize>,
877    /// WITH EXPAND options (graph traversal, cross-ref following)
878    pub expand: Option<ExpandOptions>,
879    /// Time-travel anchor. When present the executor resolves this
880    /// to an MVCC xid and evaluates the query against that snapshot
881    /// instead of the current one. Mirrors git's `AS OF` semantics.
882    pub as_of: Option<AsOfClause>,
883}
884
885/// Source spec for `AS OF` — parsed form sits in `TableQuery`, then
886/// `vcs_resolve_as_of` turns it into an MVCC xid at execute time.
887#[derive(Debug, Clone)]
888pub enum AsOfClause {
889    /// Explicit commit hash literal: `AS OF COMMIT '<hex>'`.
890    Commit(String),
891    /// Branch or ref: `AS OF BRANCH 'main'` or `AS OF 'refs/heads/main'`.
892    Branch(String),
893    /// Tag: `AS OF TAG 'v1.0'`.
894    Tag(String),
895    /// Unix epoch milliseconds: `AS OF TIMESTAMP 1710000000000`.
896    TimestampMs(i64),
897    /// Raw MVCC snapshot xid: `AS OF SNAPSHOT 12345`.
898    Snapshot(u64),
899}
900
901/// Structured FROM source for a `TableQuery`. Additive alongside the
902/// legacy `TableQuery.table: String` slot — callers that understand
903/// this type can branch on subqueries; callers that only read `table`
904/// fall back to the synthetic sentinel name and, for subqueries,
905/// produce an "unknown table" error until they migrate.
906#[derive(Debug, Clone)]
907pub enum TableSource {
908    /// Plain table reference — equivalent to the legacy `String` form.
909    Name(String),
910    /// A subquery in FROM position: `FROM (SELECT …) AS alias`.
911    Subquery(Box<QueryExpr>),
912}
913
914/// Options for WITH EXPAND clause on SELECT queries.
915#[derive(Debug, Clone, Default)]
916pub struct ExpandOptions {
917    /// Expand via graph edges (WITH EXPAND GRAPH)
918    pub graph: bool,
919    /// Graph expansion depth (DEPTH n)
920    pub graph_depth: usize,
921    /// Expand via cross-references (WITH EXPAND CROSS_REFS)
922    pub cross_refs: bool,
923    /// Index hint from the optimizer (which index to prefer for this query)
924    pub index_hint: Option<crate::storage::query::planner::optimizer::IndexHint>,
925}
926
927impl TableQuery {
928    /// Create a new table query
929    pub fn new(table: &str) -> Self {
930        Self {
931            table: table.to_string(),
932            source: None,
933            alias: None,
934            select_items: Vec::new(),
935            columns: Vec::new(),
936            where_expr: None,
937            filter: None,
938            group_by_exprs: Vec::new(),
939            group_by: Vec::new(),
940            having_expr: None,
941            having: None,
942            order_by: Vec::new(),
943            limit: None,
944            limit_param: None,
945            offset: None,
946            offset_param: None,
947            expand: None,
948            as_of: None,
949        }
950    }
951
952    /// Create a TableQuery that wraps a subquery in FROM position.
953    /// The legacy `table` slot holds a synthetic sentinel so code that
954    /// only reads `table.as_str()` errors loudly with a
955    /// recognisable marker instead of silently treating it as a
956    /// real collection.
957    pub fn from_subquery(subquery: QueryExpr, alias: Option<String>) -> Self {
958        let sentinel = match &alias {
959            Some(a) => format!("__subq_{a}"),
960            None => "__subq_anon".to_string(),
961        };
962        Self {
963            table: sentinel,
964            source: Some(TableSource::Subquery(Box::new(subquery))),
965            alias,
966            select_items: Vec::new(),
967            columns: Vec::new(),
968            where_expr: None,
969            filter: None,
970            group_by_exprs: Vec::new(),
971            group_by: Vec::new(),
972            having_expr: None,
973            having: None,
974            order_by: Vec::new(),
975            limit: None,
976            limit_param: None,
977            offset: None,
978            offset_param: None,
979            expand: None,
980            as_of: None,
981        }
982    }
983}
984
985/// Canonical SQL select item for table queries.
986#[derive(Debug, Clone, PartialEq)]
987pub enum SelectItem {
988    Wildcard,
989    Expr {
990        expr: super::Expr,
991        alias: Option<String>,
992    },
993}
994
995// ============================================================================
996// Graph Query
997// ============================================================================
998
999/// Graph query: MATCH pattern WHERE filter RETURN projection
1000#[derive(Debug, Clone)]
1001pub struct GraphQuery {
1002    /// Optional outer alias when used as a join source
1003    pub alias: Option<String>,
1004    /// Graph pattern to match
1005    pub pattern: GraphPattern,
1006    /// Filter condition
1007    pub filter: Option<Filter>,
1008    /// Return projections
1009    pub return_: Vec<Projection>,
1010    /// Optional row limit
1011    pub limit: Option<u64>,
1012}
1013
1014impl GraphQuery {
1015    /// Create a new graph query
1016    pub fn new(pattern: GraphPattern) -> Self {
1017        Self {
1018            alias: None,
1019            pattern,
1020            filter: None,
1021            return_: Vec::new(),
1022            limit: None,
1023        }
1024    }
1025
1026    /// Set outer alias
1027    pub fn alias(mut self, alias: &str) -> Self {
1028        self.alias = Some(alias.to_string());
1029        self
1030    }
1031}
1032
1033/// Graph pattern: collection of node and edge patterns
1034#[derive(Debug, Clone, Default)]
1035pub struct GraphPattern {
1036    /// Node patterns
1037    pub nodes: Vec<NodePattern>,
1038    /// Edge patterns connecting nodes
1039    pub edges: Vec<EdgePattern>,
1040}
1041
1042impl GraphPattern {
1043    /// Create an empty pattern
1044    pub fn new() -> Self {
1045        Self::default()
1046    }
1047
1048    /// Add a node pattern
1049    pub fn node(mut self, pattern: NodePattern) -> Self {
1050        self.nodes.push(pattern);
1051        self
1052    }
1053
1054    /// Add an edge pattern
1055    pub fn edge(mut self, pattern: EdgePattern) -> Self {
1056        self.edges.push(pattern);
1057        self
1058    }
1059}
1060
1061/// Node pattern: (alias:Type {properties})
1062#[derive(Debug, Clone)]
1063pub struct NodePattern {
1064    /// Variable alias for this node
1065    pub alias: String,
1066    /// Optional label filter. Stored as the user-supplied label string so
1067    /// the parser is registry-free; executors resolve it against the live
1068    /// [`crate::storage::engine::graph_store::LabelRegistry`].
1069    pub node_label: Option<String>,
1070    /// Property filters
1071    pub properties: Vec<PropertyFilter>,
1072}
1073
1074impl NodePattern {
1075    /// Create a new node pattern
1076    pub fn new(alias: &str) -> Self {
1077        Self {
1078            alias: alias.to_string(),
1079            node_label: None,
1080            properties: Vec::new(),
1081        }
1082    }
1083
1084    /// Set the label filter (string form — preferred).
1085    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1086        self.node_label = Some(label.into());
1087        self
1088    }
1089
1090    /// Add property filter
1091    pub fn with_property(mut self, name: &str, op: CompareOp, value: Value) -> Self {
1092        self.properties.push(PropertyFilter {
1093            name: name.to_string(),
1094            op,
1095            value,
1096        });
1097        self
1098    }
1099}
1100
1101/// Edge pattern: -[alias:Type*min..max]->
1102#[derive(Debug, Clone)]
1103pub struct EdgePattern {
1104    /// Optional alias for this edge
1105    pub alias: Option<String>,
1106    /// Source node alias
1107    pub from: String,
1108    /// Target node alias
1109    pub to: String,
1110    /// Optional label filter (user-supplied string).
1111    pub edge_label: Option<String>,
1112    /// Edge direction
1113    pub direction: EdgeDirection,
1114    /// Minimum hops (for variable-length patterns)
1115    pub min_hops: u32,
1116    /// Maximum hops (for variable-length patterns)
1117    pub max_hops: u32,
1118}
1119
1120impl EdgePattern {
1121    /// Create a new edge pattern
1122    pub fn new(from: &str, to: &str) -> Self {
1123        Self {
1124            alias: None,
1125            from: from.to_string(),
1126            to: to.to_string(),
1127            edge_label: None,
1128            direction: EdgeDirection::Outgoing,
1129            min_hops: 1,
1130            max_hops: 1,
1131        }
1132    }
1133
1134    /// Set label filter (string form — preferred).
1135    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1136        self.edge_label = Some(label.into());
1137        self
1138    }
1139
1140    /// Set direction
1141    pub fn direction(mut self, dir: EdgeDirection) -> Self {
1142        self.direction = dir;
1143        self
1144    }
1145
1146    /// Set hop range for variable-length patterns
1147    pub fn hops(mut self, min: u32, max: u32) -> Self {
1148        self.min_hops = min;
1149        self.max_hops = max;
1150        self
1151    }
1152
1153    /// Set alias
1154    pub fn alias(mut self, alias: &str) -> Self {
1155        self.alias = Some(alias.to_string());
1156        self
1157    }
1158}
1159
1160/// Edge direction
1161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1162pub enum EdgeDirection {
1163    /// Outgoing: (a)-[r]->(b)
1164    Outgoing,
1165    /// Incoming: (a)<-[r]-(b)
1166    Incoming,
1167    /// Both: (a)-[r]-(b)
1168    Both,
1169}
1170
1171/// Property filter: name op value
1172#[derive(Debug, Clone)]
1173pub struct PropertyFilter {
1174    pub name: String,
1175    pub op: CompareOp,
1176    pub value: Value,
1177}
1178
1179// ============================================================================
1180// Join Query
1181// ============================================================================
1182
1183/// Join query: combines table and graph queries
1184#[derive(Debug, Clone)]
1185pub struct JoinQuery {
1186    /// Left side (typically table)
1187    pub left: Box<QueryExpr>,
1188    /// Right side (typically graph)
1189    pub right: Box<QueryExpr>,
1190    /// Join type
1191    pub join_type: JoinType,
1192    /// Join condition
1193    pub on: JoinCondition,
1194    /// Post-join filter condition
1195    pub filter: Option<Filter>,
1196    /// Post-join ordering
1197    pub order_by: Vec<OrderByClause>,
1198    /// Post-join limit
1199    pub limit: Option<u64>,
1200    /// Post-join offset
1201    pub offset: Option<u64>,
1202    /// Canonical SQL RETURN projection.
1203    pub return_items: Vec<SelectItem>,
1204    /// Post-join projection
1205    pub return_: Vec<Projection>,
1206}
1207
1208impl JoinQuery {
1209    /// Create a new join query
1210    pub fn new(left: QueryExpr, right: QueryExpr, on: JoinCondition) -> Self {
1211        Self {
1212            left: Box::new(left),
1213            right: Box::new(right),
1214            join_type: JoinType::Inner,
1215            on,
1216            filter: None,
1217            order_by: Vec::new(),
1218            limit: None,
1219            offset: None,
1220            return_items: Vec::new(),
1221            return_: Vec::new(),
1222        }
1223    }
1224
1225    /// Set join type
1226    pub fn join_type(mut self, jt: JoinType) -> Self {
1227        self.join_type = jt;
1228        self
1229    }
1230}
1231
1232/// Join type
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum JoinType {
1235    /// Inner join — only matching pairs emitted
1236    Inner,
1237    /// Left outer join — every left row, matched or padded with nulls on the right
1238    LeftOuter,
1239    /// Right outer join — every right row, matched or padded with nulls on the left
1240    RightOuter,
1241    /// Full outer join — LeftOuter ∪ RightOuter, each unmatched side padded
1242    FullOuter,
1243    /// Cross join — Cartesian product, no predicate
1244    Cross,
1245}
1246
1247/// Join condition: how to match rows with nodes
1248#[derive(Debug, Clone)]
1249pub struct JoinCondition {
1250    /// Left field (table side)
1251    pub left_field: FieldRef,
1252    /// Right field (graph side)
1253    pub right_field: FieldRef,
1254}
1255
1256impl JoinCondition {
1257    /// Create a new join condition
1258    pub fn new(left: FieldRef, right: FieldRef) -> Self {
1259        Self {
1260            left_field: left,
1261            right_field: right,
1262        }
1263    }
1264}
1265
1266/// Reference to a field (table column, node property, or edge property)
1267#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1268pub enum FieldRef {
1269    /// Table column: table.column
1270    TableColumn { table: String, column: String },
1271    /// Node property: alias.property
1272    NodeProperty { alias: String, property: String },
1273    /// Edge property: alias.property
1274    EdgeProperty { alias: String, property: String },
1275    /// Node ID: alias.id
1276    NodeId { alias: String },
1277}
1278
1279impl FieldRef {
1280    /// Create a table column reference
1281    pub fn column(table: &str, column: &str) -> Self {
1282        Self::TableColumn {
1283            table: table.to_string(),
1284            column: column.to_string(),
1285        }
1286    }
1287
1288    /// Create a node property reference
1289    pub fn node_prop(alias: &str, property: &str) -> Self {
1290        Self::NodeProperty {
1291            alias: alias.to_string(),
1292            property: property.to_string(),
1293        }
1294    }
1295
1296    /// Create a node ID reference
1297    pub fn node_id(alias: &str) -> Self {
1298        Self::NodeId {
1299            alias: alias.to_string(),
1300        }
1301    }
1302
1303    /// Create an edge property reference
1304    pub fn edge_prop(alias: &str, property: &str) -> Self {
1305        Self::EdgeProperty {
1306            alias: alias.to_string(),
1307            property: property.to_string(),
1308        }
1309    }
1310}
1311
1312// ============================================================================
1313// Path Query
1314// ============================================================================
1315
1316/// Path query: find paths between nodes
1317#[derive(Debug, Clone)]
1318pub struct PathQuery {
1319    /// Optional outer alias when used as a join source
1320    pub alias: Option<String>,
1321    /// Source node selector
1322    pub from: NodeSelector,
1323    /// Target node selector
1324    pub to: NodeSelector,
1325    /// Edge labels to traverse (empty = any). Strings are resolved against
1326    /// the runtime registry by the executor.
1327    pub via: Vec<String>,
1328    /// Maximum path length
1329    pub max_length: u32,
1330    /// Filter on paths
1331    pub filter: Option<Filter>,
1332    /// Return projections
1333    pub return_: Vec<Projection>,
1334}
1335
1336impl PathQuery {
1337    /// Create a new path query
1338    pub fn new(from: NodeSelector, to: NodeSelector) -> Self {
1339        Self {
1340            alias: None,
1341            from,
1342            to,
1343            via: Vec::new(),
1344            max_length: 10,
1345            filter: None,
1346            return_: Vec::new(),
1347        }
1348    }
1349
1350    /// Set outer alias
1351    pub fn alias(mut self, alias: &str) -> Self {
1352        self.alias = Some(alias.to_string());
1353        self
1354    }
1355
1356    /// Add an edge label constraint to traverse (string form).
1357    pub fn via_label(mut self, label: impl Into<String>) -> Self {
1358        self.via.push(label.into());
1359        self
1360    }
1361}
1362
1363/// Node selector for path queries
1364#[derive(Debug, Clone)]
1365pub enum NodeSelector {
1366    /// By node ID
1367    ById(String),
1368    /// By node label and property
1369    ByType {
1370        node_label: String,
1371        filter: Option<PropertyFilter>,
1372    },
1373    /// By table row (linked node)
1374    ByRow { table: String, row_id: u64 },
1375}
1376
1377impl NodeSelector {
1378    /// Select by node ID
1379    pub fn by_id(id: &str) -> Self {
1380        Self::ById(id.to_string())
1381    }
1382
1383    /// Select by label string (preferred).
1384    pub fn by_label(label: impl Into<String>) -> Self {
1385        Self::ByType {
1386            node_label: label.into(),
1387            filter: None,
1388        }
1389    }
1390
1391    /// Select by table row
1392    pub fn by_row(table: &str, row_id: u64) -> Self {
1393        Self::ByRow {
1394            table: table.to_string(),
1395            row_id,
1396        }
1397    }
1398}
1399
1400// ============================================================================
1401// Vector Query
1402// ============================================================================
1403
1404/// Vector similarity search query
1405///
1406/// ```text
1407/// VECTOR SEARCH embeddings
1408/// SIMILAR TO [0.1, 0.2, ..., 0.5]
1409/// WHERE metadata.source = 'nmap'
1410/// LIMIT 10
1411/// ```
1412#[derive(Debug, Clone)]
1413pub struct VectorQuery {
1414    /// Optional outer alias when used as a join source
1415    pub alias: Option<String>,
1416    /// Collection name to search
1417    pub collection: String,
1418    /// Query vector (or reference to get vector from)
1419    pub query_vector: VectorSource,
1420    /// Number of results to return
1421    pub k: usize,
1422    /// Metadata filter
1423    pub filter: Option<MetadataFilter>,
1424    /// Distance metric to use (defaults to collection's metric)
1425    pub metric: Option<DistanceMetric>,
1426    /// Include vectors in results
1427    pub include_vectors: bool,
1428    /// Include metadata in results
1429    pub include_metadata: bool,
1430    /// Minimum similarity threshold (optional)
1431    pub threshold: Option<f32>,
1432}
1433
1434impl VectorQuery {
1435    /// Create a new vector query
1436    pub fn new(collection: &str, query: VectorSource) -> Self {
1437        Self {
1438            alias: None,
1439            collection: collection.to_string(),
1440            query_vector: query,
1441            k: 10,
1442            filter: None,
1443            metric: None,
1444            include_vectors: false,
1445            include_metadata: true,
1446            threshold: None,
1447        }
1448    }
1449
1450    /// Set the number of results
1451    pub fn limit(mut self, k: usize) -> Self {
1452        self.k = k;
1453        self
1454    }
1455
1456    /// Set metadata filter
1457    pub fn with_filter(mut self, filter: MetadataFilter) -> Self {
1458        self.filter = Some(filter);
1459        self
1460    }
1461
1462    /// Include vectors in results
1463    pub fn with_vectors(mut self) -> Self {
1464        self.include_vectors = true;
1465        self
1466    }
1467
1468    /// Set similarity threshold
1469    pub fn min_similarity(mut self, threshold: f32) -> Self {
1470        self.threshold = Some(threshold);
1471        self
1472    }
1473
1474    /// Set outer alias
1475    pub fn alias(mut self, alias: &str) -> Self {
1476        self.alias = Some(alias.to_string());
1477        self
1478    }
1479}
1480
1481/// Source of query vector
1482#[derive(Debug, Clone)]
1483pub enum VectorSource {
1484    /// Literal vector values
1485    Literal(Vec<f32>),
1486    /// Text to embed (requires embedding function)
1487    Text(String),
1488    /// Reference to another vector by ID
1489    Reference { collection: String, vector_id: u64 },
1490    /// From a subquery result
1491    Subquery(Box<QueryExpr>),
1492}
1493
1494impl VectorSource {
1495    /// Create from literal vector
1496    pub fn literal(values: Vec<f32>) -> Self {
1497        Self::Literal(values)
1498    }
1499
1500    /// Create from text (to be embedded)
1501    pub fn text(s: &str) -> Self {
1502        Self::Text(s.to_string())
1503    }
1504
1505    /// Reference another vector
1506    pub fn reference(collection: &str, vector_id: u64) -> Self {
1507        Self::Reference {
1508            collection: collection.to_string(),
1509            vector_id,
1510        }
1511    }
1512}
1513
1514// ============================================================================
1515// Hybrid Query
1516// ============================================================================
1517
1518/// Hybrid query combining structured (table/graph) and vector search
1519///
1520/// ```text
1521/// FROM hosts h
1522/// JOIN VECTOR embeddings e ON h.id = e.metadata.host_id
1523/// SIMILAR TO 'ssh vulnerability'
1524/// WHERE h.os = 'Linux'
1525/// RETURN h.*, e.distance
1526/// ```
1527#[derive(Debug, Clone)]
1528pub struct HybridQuery {
1529    /// Optional outer alias when used as a join source
1530    pub alias: Option<String>,
1531    /// Structured query part (table/graph)
1532    pub structured: Box<QueryExpr>,
1533    /// Vector search part
1534    pub vector: VectorQuery,
1535    /// How to combine results
1536    pub fusion: FusionStrategy,
1537    /// Final result limit
1538    pub limit: Option<usize>,
1539}
1540
1541impl HybridQuery {
1542    /// Create a new hybrid query
1543    pub fn new(structured: QueryExpr, vector: VectorQuery) -> Self {
1544        Self {
1545            alias: None,
1546            structured: Box::new(structured),
1547            vector,
1548            fusion: FusionStrategy::Rerank { weight: 0.5 },
1549            limit: None,
1550        }
1551    }
1552
1553    /// Set fusion strategy
1554    pub fn with_fusion(mut self, fusion: FusionStrategy) -> Self {
1555        self.fusion = fusion;
1556        self
1557    }
1558
1559    /// Set result limit
1560    pub fn limit(mut self, limit: usize) -> Self {
1561        self.limit = Some(limit);
1562        self
1563    }
1564
1565    /// Set outer alias
1566    pub fn alias(mut self, alias: &str) -> Self {
1567        self.alias = Some(alias.to_string());
1568        self
1569    }
1570}
1571
1572/// Strategy for combining structured and vector search results
1573#[derive(Debug, Clone)]
1574pub enum FusionStrategy {
1575    /// Vector similarity re-ranks structured results
1576    /// weight: 0.0 = pure structured, 1.0 = pure vector
1577    Rerank { weight: f32 },
1578    /// Filter with structured query, then search vectors among filtered
1579    FilterThenSearch,
1580    /// Search vectors first, then filter with structured query
1581    SearchThenFilter,
1582    /// Reciprocal Rank Fusion
1583    /// k: RRF constant (typically 60)
1584    RRF { k: u32 },
1585    /// Intersection: only return results that match both
1586    Intersection,
1587    /// Union: return results from either (with combined scores)
1588    Union {
1589        structured_weight: f32,
1590        vector_weight: f32,
1591    },
1592}
1593
1594impl Default for FusionStrategy {
1595    fn default() -> Self {
1596        Self::Rerank { weight: 0.5 }
1597    }
1598}
1599
1600// ============================================================================
1601// DML/DDL Query Types
1602// ============================================================================
1603
1604/// Entity type qualifier for INSERT statements
1605#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1606pub enum InsertEntityType {
1607    /// Default: plain row
1608    #[default]
1609    Row,
1610    /// INSERT INTO t NODE (...)
1611    Node,
1612    /// INSERT INTO t EDGE (...)
1613    Edge,
1614    /// INSERT INTO t VECTOR (...)
1615    Vector,
1616    /// INSERT INTO t DOCUMENT (...)
1617    Document,
1618    /// INSERT INTO t KV (...)
1619    Kv,
1620}
1621
1622/// Explicit item-kind qualifier for UPDATE statements.
1623#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1624pub enum UpdateTarget {
1625    /// Default: table/document/KV row-shaped items.
1626    #[default]
1627    Rows,
1628    /// UPDATE t DOCUMENTS SET ...
1629    Documents,
1630    /// UPDATE t KV SET ...
1631    Kv,
1632    /// UPDATE t NODES SET ...
1633    Nodes,
1634    /// UPDATE t EDGES SET ...
1635    Edges,
1636}
1637
1638/// An item in a RETURNING clause: either `*` (all columns) or a named column.
1639#[derive(Debug, Clone, PartialEq)]
1640pub enum ReturningItem {
1641    /// RETURNING *
1642    All,
1643    /// RETURNING col
1644    Column(String),
1645}
1646
1647/// INSERT INTO table (columns) VALUES (row1), (row2), ... [WITH TTL duration] [WITH METADATA (k=v)]
1648#[derive(Debug, Clone)]
1649pub struct InsertQuery {
1650    /// Target table name
1651    pub table: String,
1652    /// Entity type qualifier
1653    pub entity_type: InsertEntityType,
1654    /// Column names
1655    pub columns: Vec<String>,
1656    /// Canonical SQL rows of expressions.
1657    pub value_exprs: Vec<Vec<super::Expr>>,
1658    /// Rows of values (each inner Vec is one row)
1659    pub values: Vec<Vec<Value>>,
1660    /// Optional RETURNING clause items.
1661    pub returning: Option<Vec<ReturningItem>>,
1662    /// Optional TTL in milliseconds (from WITH TTL clause)
1663    pub ttl_ms: Option<u64>,
1664    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1665    pub expires_at_ms: Option<u64>,
1666    /// Optional metadata key-value pairs (from WITH METADATA clause)
1667    pub with_metadata: Vec<(String, Value)>,
1668    /// Auto-embed fields on insert (from WITH AUTO EMBED clause)
1669    pub auto_embed: Option<AutoEmbedConfig>,
1670    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1671    pub suppress_events: bool,
1672}
1673
1674/// Configuration for automatic embedding generation on INSERT.
1675#[derive(Debug, Clone)]
1676pub struct AutoEmbedConfig {
1677    /// Fields to extract text from for embedding
1678    pub fields: Vec<String>,
1679    /// AI provider (e.g. "openai")
1680    pub provider: String,
1681    /// Optional model override
1682    pub model: Option<String>,
1683}
1684
1685/// EVENTS BACKFILL collection [WHERE pred] TO queue [LIMIT n]
1686#[derive(Debug, Clone)]
1687pub struct EventsBackfillQuery {
1688    pub collection: String,
1689    pub where_filter: Option<String>,
1690    pub target_queue: String,
1691    pub limit: Option<u64>,
1692}
1693
1694/// UPDATE table SET col=val, ... WHERE filter [WITH TTL duration] [WITH METADATA (...)]
1695#[derive(Debug, Clone)]
1696pub struct UpdateQuery {
1697    /// Target table name
1698    pub table: String,
1699    /// Explicit item-kind target. Omitted targets default to rows.
1700    pub target: UpdateTarget,
1701    /// Canonical SQL assignments.
1702    pub assignment_exprs: Vec<(String, super::Expr)>,
1703    /// Per-assignment compound operator for `SET col += expr` forms.
1704    /// `None` means ordinary `SET col = expr`.
1705    pub compound_assignment_ops: Vec<Option<super::BinOp>>,
1706    /// Best-effort literal-only cache of assignments. Non-foldable expressions
1707    /// are preserved exclusively in `assignment_exprs` and evaluated later
1708    /// against the row pre-image by the runtime.
1709    pub assignments: Vec<(String, Value)>,
1710    /// Canonical SQL WHERE clause.
1711    pub where_expr: Option<super::Expr>,
1712    /// Optional WHERE filter
1713    pub filter: Option<Filter>,
1714    /// Optional TTL in milliseconds (from WITH TTL clause)
1715    pub ttl_ms: Option<u64>,
1716    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1717    pub expires_at_ms: Option<u64>,
1718    /// Optional metadata key-value pairs (from WITH METADATA clause)
1719    pub with_metadata: Vec<(String, Value)>,
1720    /// Optional RETURNING clause items.
1721    pub returning: Option<Vec<ReturningItem>>,
1722    /// Optional deterministic target ordering for limited UPDATE batches.
1723    pub order_by: Vec<OrderByClause>,
1724    /// Optional `LIMIT N` cap. Caps the number of targets the executor
1725    /// will mutate in a single statement. Required by `BATCH N ROWS`
1726    /// data migrations (#37) which run the same UPDATE body in a
1727    /// loop, advancing a checkpoint between batches.
1728    pub limit: Option<u64>,
1729    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1730    pub suppress_events: bool,
1731}
1732
1733/// DELETE FROM table WHERE filter
1734#[derive(Debug, Clone)]
1735pub struct DeleteQuery {
1736    /// Target table name
1737    pub table: String,
1738    /// Canonical SQL WHERE clause.
1739    pub where_expr: Option<super::Expr>,
1740    /// Optional WHERE filter
1741    pub filter: Option<Filter>,
1742    /// Optional RETURNING clause items.
1743    pub returning: Option<Vec<ReturningItem>>,
1744    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1745    pub suppress_events: bool,
1746}
1747
1748/// CREATE TABLE name (columns) or CREATE {KV|CONFIG|VAULT} name
1749#[derive(Debug, Clone)]
1750pub struct CreateTableQuery {
1751    /// Declared collection model. Defaults to Table for CREATE TABLE.
1752    pub collection_model: CollectionModel,
1753    /// Table name
1754    pub name: String,
1755    /// Column definitions
1756    pub columns: Vec<CreateColumnDef>,
1757    /// IF NOT EXISTS flag
1758    pub if_not_exists: bool,
1759    /// Optional default TTL applied to newly inserted items in this collection.
1760    pub default_ttl_ms: Option<u64>,
1761    /// Metrics rollup tiers declared by `CREATE METRICS ... DOWNSAMPLE`.
1762    /// Uses the existing time-series policy spelling: target:source:aggregation.
1763    pub metrics_rollup_policies: Vec<String>,
1764    /// Fields to prioritize in the context index (WITH CONTEXT INDEX ON (f1, f2))
1765    pub context_index_fields: Vec<String>,
1766    /// Enables the global context index for this table
1767    /// (`WITH context_index = true`). Default false — pure OLTP tables
1768    /// skip the tokenisation / 3-way RwLock write storm on every insert.
1769    /// Having `context_index_fields` non-empty also enables it implicitly.
1770    pub context_index_enabled: bool,
1771    /// When true, CREATE TABLE implicitly adds two user-visible columns
1772    /// `created_at` and `updated_at` (BIGINT unix-ms). The runtime
1773    /// populates them from `UnifiedEntity::created_at/updated_at` on
1774    /// every write; `created_at` is immutable after insert.
1775    /// Enabled via `WITH timestamps = true` in the DDL.
1776    pub timestamps: bool,
1777    /// Partitioning spec (Phase 2.2 PG parity).
1778    ///
1779    /// When present the table is the *parent* of a partition tree — every
1780    /// child partition is registered via `ALTER TABLE ... ATTACH PARTITION`.
1781    /// Phase 2.2 stops at registry-only: queries against a partitioned
1782    /// parent don't auto-rewrite as UNION yet (Phase 4 adds pruning).
1783    pub partition_by: Option<PartitionSpec>,
1784    /// Table-scoped multi-tenancy declaration (Phase 2.5.4).
1785    ///
1786    /// Syntax: `CREATE TABLE t (...) WITH (tenant_by = 'col_name')` or
1787    /// the shorthand `CREATE TABLE t (...) TENANT BY (col_name)`. The
1788    /// runtime treats the named column as the tenant discriminator and
1789    /// automatically:
1790    ///
1791    /// 1. Registers the table → column mapping so INSERTs that omit the
1792    ///    column get `CURRENT_TENANT()` auto-filled.
1793    /// 2. Installs an implicit RLS policy equivalent to
1794    ///    `USING (col = CURRENT_TENANT())` for SELECT/UPDATE/DELETE/INSERT.
1795    /// 3. Flips `rls_enabled_tables` on so the policy actually applies.
1796    ///
1797    /// None leaves the table non-tenant-scoped — callers manage tenancy
1798    /// manually via explicit CREATE POLICY if they want it.
1799    pub tenant_by: Option<String>,
1800    /// When true, UPDATE and DELETE on this table are rejected at
1801    /// parse time. Corresponds to `CREATE TABLE ... APPEND ONLY` or
1802    /// `WITH (append_only = true)`. Default false (mutable).
1803    pub append_only: bool,
1804    /// Declarative event subscriptions for this table. #291 stores
1805    /// metadata only; event emission is intentionally out of scope.
1806    pub subscriptions: Vec<crate::catalog::SubscriptionDescriptor>,
1807    /// `CREATE VAULT ... WITH OWN MASTER KEY`: provision per-vault
1808    /// key material instead of using the cluster vault key.
1809    pub vault_own_master_key: bool,
1810}
1811
1812/// CREATE COLLECTION name KIND kind
1813#[derive(Debug, Clone)]
1814pub struct CreateCollectionQuery {
1815    pub name: String,
1816    pub kind: String,
1817    pub if_not_exists: bool,
1818}
1819
1820/// CREATE VECTOR name DIM n [METRIC metric]
1821#[derive(Debug, Clone)]
1822pub struct CreateVectorQuery {
1823    pub name: String,
1824    pub dimension: usize,
1825    pub metric: DistanceMetric,
1826    pub if_not_exists: bool,
1827}
1828
1829/// `PARTITION BY RANGE|LIST|HASH (column)` clause.
1830#[derive(Debug, Clone, PartialEq, Eq)]
1831pub struct PartitionSpec {
1832    pub kind: PartitionKind,
1833    /// Partition key column(s). Simple single-column for Phase 2.2.
1834    pub column: String,
1835}
1836
1837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1838pub enum PartitionKind {
1839    /// `PARTITION BY RANGE(col)` — children bind `FOR VALUES FROM (a) TO (b)`.
1840    Range,
1841    /// `PARTITION BY LIST(col)` — children bind `FOR VALUES IN (v1, v2, ...)`.
1842    List,
1843    /// `PARTITION BY HASH(col)` — children bind `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
1844    Hash,
1845}
1846
1847/// Column definition for CREATE TABLE
1848#[derive(Debug, Clone)]
1849pub struct CreateColumnDef {
1850    /// Column name
1851    pub name: String,
1852    /// Legacy declared type string preserved for the runtime/storage pipeline.
1853    pub data_type: String,
1854    /// Structured SQL type used by the semantic layer.
1855    pub sql_type: SqlTypeName,
1856    /// NOT NULL constraint
1857    pub not_null: bool,
1858    /// DEFAULT value expression
1859    pub default: Option<String>,
1860    /// Compression level (COMPRESS:N)
1861    pub compress: Option<u8>,
1862    /// UNIQUE constraint
1863    pub unique: bool,
1864    /// PRIMARY KEY constraint
1865    pub primary_key: bool,
1866    /// Enum variant names (for ENUM type)
1867    pub enum_variants: Vec<String>,
1868    /// Array element type (for ARRAY type)
1869    pub array_element: Option<String>,
1870    /// Decimal precision (for DECIMAL type)
1871    pub decimal_precision: Option<u8>,
1872}
1873
1874/// DROP TABLE name
1875#[derive(Debug, Clone)]
1876pub struct DropTableQuery {
1877    /// Table name
1878    pub name: String,
1879    /// IF EXISTS flag
1880    pub if_exists: bool,
1881}
1882
1883/// DROP GRAPH [IF EXISTS] name
1884#[derive(Debug, Clone)]
1885pub struct DropGraphQuery {
1886    pub name: String,
1887    pub if_exists: bool,
1888}
1889
1890/// DROP VECTOR [IF EXISTS] name
1891#[derive(Debug, Clone)]
1892pub struct DropVectorQuery {
1893    pub name: String,
1894    pub if_exists: bool,
1895}
1896
1897/// DROP DOCUMENT [IF EXISTS] name
1898#[derive(Debug, Clone)]
1899pub struct DropDocumentQuery {
1900    pub name: String,
1901    pub if_exists: bool,
1902}
1903
1904/// DROP {KV|CONFIG|VAULT} [IF EXISTS] name
1905#[derive(Debug, Clone)]
1906pub struct DropKvQuery {
1907    pub name: String,
1908    pub if_exists: bool,
1909    pub model: CollectionModel,
1910}
1911
1912/// DROP COLLECTION [IF EXISTS] name
1913#[derive(Debug, Clone)]
1914pub struct DropCollectionQuery {
1915    pub name: String,
1916    pub if_exists: bool,
1917    pub model: Option<CollectionModel>,
1918}
1919
1920/// TRUNCATE {TABLE|GRAPH|VECTOR|DOCUMENT|TIMESERIES|KV|QUEUE|COLLECTION} [IF EXISTS] name
1921#[derive(Debug, Clone)]
1922pub struct TruncateQuery {
1923    pub name: String,
1924    pub model: Option<CollectionModel>,
1925    pub if_exists: bool,
1926}
1927
1928/// ALTER TABLE name operations
1929#[derive(Debug, Clone)]
1930pub struct AlterTableQuery {
1931    /// Table name
1932    pub name: String,
1933    /// Alter operations
1934    pub operations: Vec<AlterOperation>,
1935}
1936
1937/// Single ALTER TABLE operation
1938#[derive(Debug, Clone)]
1939pub enum AlterOperation {
1940    /// ADD COLUMN definition
1941    AddColumn(CreateColumnDef),
1942    /// DROP COLUMN name
1943    DropColumn(String),
1944    /// RENAME COLUMN from TO to
1945    RenameColumn { from: String, to: String },
1946    /// `ATTACH PARTITION child FOR VALUES ...` (Phase 2.2 PG parity).
1947    ///
1948    /// Binds an existing child table to the parent partitioned table.
1949    /// The `bound` string captures the raw bound expression so the
1950    /// runtime can round-trip it back into `red_config` without a
1951    /// dedicated per-kind AST.
1952    AttachPartition {
1953        child: String,
1954        /// Human-readable bound string, e.g. `FROM (2024-01-01) TO (2025-01-01)`
1955        /// or `IN (1, 2, 3)` or `WITH (MODULUS 4, REMAINDER 0)`.
1956        bound: String,
1957    },
1958    /// `DETACH PARTITION child`
1959    DetachPartition { child: String },
1960    /// `ENABLE ROW LEVEL SECURITY` (Phase 2.5 PG parity).
1961    ///
1962    /// Flips the table into RLS-enforced mode. Reads against the table
1963    /// will be filtered by every matching `CREATE POLICY` (for the
1964    /// current role) combined with `AND`.
1965    EnableRowLevelSecurity,
1966    /// `DISABLE ROW LEVEL SECURITY` — disables enforcement; policies
1967    /// remain defined but are ignored until re-enabled.
1968    DisableRowLevelSecurity,
1969    /// `ENABLE TENANCY ON (col)` (Phase 2.5.4 PG parity-ish).
1970    ///
1971    /// Retrofit a tenant-scoped declaration onto an existing table —
1972    /// registers the column, installs the auto `__tenant_iso` RLS
1973    /// policy, and flips RLS on. Equivalent to re-running
1974    /// `CREATE TABLE ... TENANT BY (col)` minus the schema creation.
1975    EnableTenancy { column: String },
1976    /// `DISABLE TENANCY` — tears down the auto-policy and clears the
1977    /// tenancy registration. User-defined policies on the table are
1978    /// untouched; RLS stays enabled if any survive.
1979    DisableTenancy,
1980    /// `SET APPEND_ONLY = true|false` — flips the catalog flag.
1981    /// Setting `true` rejects all future UPDATE/DELETE at parse-time
1982    /// guard; setting `false` re-enables them. Existing rows are
1983    /// untouched either way — this is a purely declarative switch.
1984    SetAppendOnly(bool),
1985    /// `SET VERSIONED = true|false` — opt the table into (or out of)
1986    /// Git-for-Data. Enables merge / diff / AS OF semantics against
1987    /// this collection. Works retroactively: previously-created
1988    /// rows become part of the history accessible via AS OF as long
1989    /// as their xmin is still pinned by an existing commit.
1990    SetVersioned(bool),
1991    /// `ENABLE EVENTS ...` — install or re-enable table event subscription metadata.
1992    EnableEvents(crate::catalog::SubscriptionDescriptor),
1993    /// `DISABLE EVENTS` — mark all table event subscriptions disabled.
1994    DisableEvents,
1995    /// `ADD SUBSCRIPTION name TO queue [REDACT (...)] [WHERE ...]` — add a named subscription.
1996    AddSubscription {
1997        name: String,
1998        descriptor: crate::catalog::SubscriptionDescriptor,
1999    },
2000    /// `DROP SUBSCRIPTION name` — remove a named subscription by name.
2001    DropSubscription { name: String },
2002}
2003
2004// ============================================================================
2005// Shared Types
2006// ============================================================================
2007
2008/// Column/field projection
2009#[derive(Debug, Clone, PartialEq)]
2010pub enum Projection {
2011    /// Select all columns (*)
2012    All,
2013    /// Single column by name
2014    Column(String),
2015    /// Column with alias
2016    Alias(String, String),
2017    /// Function call (name, args)
2018    Function(String, Vec<Projection>),
2019    /// Expression with optional alias
2020    Expression(Box<Filter>, Option<String>),
2021    /// Field reference (for graph properties)
2022    Field(FieldRef, Option<String>),
2023}
2024
2025impl Projection {
2026    /// Create a projection from a field reference
2027    pub fn from_field(field: FieldRef) -> Self {
2028        Projection::Field(field, None)
2029    }
2030
2031    /// Create a column projection
2032    pub fn column(name: &str) -> Self {
2033        Projection::Column(name.to_string())
2034    }
2035
2036    /// Create an aliased projection
2037    pub fn with_alias(column: &str, alias: &str) -> Self {
2038        Projection::Alias(column.to_string(), alias.to_string())
2039    }
2040}
2041
2042/// Filter condition
2043#[derive(Debug, Clone, PartialEq)]
2044pub enum Filter {
2045    /// Comparison: field op value
2046    Compare {
2047        field: FieldRef,
2048        op: CompareOp,
2049        value: Value,
2050    },
2051    /// Field-to-field comparison: left.field op right.field. Used when
2052    /// WHERE / BETWEEN operands reference another column instead of a
2053    /// literal — the pre-Fase-2-parser-v2 shim for column-to-column
2054    /// predicates. Once the Expr-rewrite lands, this collapses into
2055    /// `Compare { left: Expr, op, right: Expr }`.
2056    CompareFields {
2057        left: FieldRef,
2058        op: CompareOp,
2059        right: FieldRef,
2060    },
2061    /// Expression-to-expression comparison: `lhs op rhs` where either
2062    /// side may be an arbitrary `Expr` tree (function call, CAST,
2063    /// arithmetic, nested CASE). This is the most general compare
2064    /// variant — `Compare` and `CompareFields` stay as fast-path
2065    /// specialisations because the planner / cost model / index
2066    /// selector all pattern-match on the simpler shapes. The parser
2067    /// only emits this variant when a simpler one cannot express the
2068    /// predicate.
2069    CompareExpr {
2070        lhs: super::Expr,
2071        op: CompareOp,
2072        rhs: super::Expr,
2073    },
2074    /// Logical AND
2075    And(Box<Filter>, Box<Filter>),
2076    /// Logical OR
2077    Or(Box<Filter>, Box<Filter>),
2078    /// Logical NOT
2079    Not(Box<Filter>),
2080    /// IS NULL
2081    IsNull(FieldRef),
2082    /// IS NOT NULL
2083    IsNotNull(FieldRef),
2084    /// IN (value1, value2, ...)
2085    In { field: FieldRef, values: Vec<Value> },
2086    /// BETWEEN low AND high
2087    Between {
2088        field: FieldRef,
2089        low: Value,
2090        high: Value,
2091    },
2092    /// LIKE pattern
2093    Like { field: FieldRef, pattern: String },
2094    /// STARTS WITH prefix
2095    StartsWith { field: FieldRef, prefix: String },
2096    /// ENDS WITH suffix
2097    EndsWith { field: FieldRef, suffix: String },
2098    /// CONTAINS substring
2099    Contains { field: FieldRef, substring: String },
2100}
2101
2102impl Filter {
2103    /// Create a comparison filter
2104    pub fn compare(field: FieldRef, op: CompareOp, value: Value) -> Self {
2105        Self::Compare { field, op, value }
2106    }
2107
2108    /// Combine with AND
2109    pub fn and(self, other: Filter) -> Self {
2110        Self::And(Box::new(self), Box::new(other))
2111    }
2112
2113    /// Combine with OR
2114    pub fn or(self, other: Filter) -> Self {
2115        Self::Or(Box::new(self), Box::new(other))
2116    }
2117
2118    /// Negate
2119    pub fn not(self) -> Self {
2120        Self::Not(Box::new(self))
2121    }
2122
2123    /// Bottom-up AST rewrites: OR-of-equalities → IN, AND/OR flatten.
2124    /// Inspired by MongoDB's `MatchExpression::optimize()`.
2125    /// Call on the result of `effective_table_filter()` before evaluation.
2126    pub fn optimize(self) -> Self {
2127        crate::storage::query::filter_optimizer::optimize(self)
2128    }
2129}
2130
2131/// Comparison operator
2132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2133pub enum CompareOp {
2134    /// Equal (=)
2135    Eq,
2136    /// Not equal (<> or !=)
2137    Ne,
2138    /// Less than (<)
2139    Lt,
2140    /// Less than or equal (<=)
2141    Le,
2142    /// Greater than (>)
2143    Gt,
2144    /// Greater than or equal (>=)
2145    Ge,
2146}
2147
2148impl fmt::Display for CompareOp {
2149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2150        match self {
2151            CompareOp::Eq => write!(f, "="),
2152            CompareOp::Ne => write!(f, "<>"),
2153            CompareOp::Lt => write!(f, "<"),
2154            CompareOp::Le => write!(f, "<="),
2155            CompareOp::Gt => write!(f, ">"),
2156            CompareOp::Ge => write!(f, ">="),
2157        }
2158    }
2159}
2160
2161/// Order by clause.
2162///
2163/// Fase 2 migration: `field` is the legacy bare column reference and
2164/// remains populated for back-compat with existing callers (SPARQL /
2165/// Gremlin / Cypher translators, the planner cost model, etc.). The
2166/// new `expr` slot carries an arbitrary `Expr` tree — when present,
2167/// runtime comparators prefer it over `field`, so the parser can
2168/// emit `ORDER BY CAST(a AS INT)`, `ORDER BY a + b * 2`, etc. without
2169/// breaking the rest of the codebase.
2170///
2171/// When `expr` is `None`, the clause behaves exactly like before.
2172/// When `expr` is `Some(Expr::Column(f))`, runtime code may still use
2173/// the legacy path — it's equivalent. Constructors default `expr` to
2174/// `None` so all existing call sites stay source-compatible.
2175#[derive(Debug, Clone)]
2176pub struct OrderByClause {
2177    /// Field to order by. Left populated even when `expr` is set so
2178    /// legacy consumers (planner cardinality estimate, cost model,
2179    /// mode translators) that still pattern-match on `field` keep
2180    /// working during the Fase 2 migration.
2181    pub field: FieldRef,
2182    /// Fase 2 expression-aware sort key. When `Some`, runtime order
2183    /// comparators evaluate this expression per row and sort on the
2184    /// resulting values — unlocks `ORDER BY expr` (Fase 1.6).
2185    pub expr: Option<super::Expr>,
2186    /// Ascending or descending
2187    pub ascending: bool,
2188    /// Nulls first or last
2189    pub nulls_first: bool,
2190}
2191
2192impl OrderByClause {
2193    /// Create ascending order
2194    pub fn asc(field: FieldRef) -> Self {
2195        Self {
2196            field,
2197            expr: None,
2198            ascending: true,
2199            nulls_first: false,
2200        }
2201    }
2202
2203    /// Create descending order
2204    pub fn desc(field: FieldRef) -> Self {
2205        Self {
2206            field,
2207            expr: None,
2208            ascending: false,
2209            nulls_first: true,
2210        }
2211    }
2212
2213    /// Attach an `Expr` sort key to an existing clause. Leaves `field`
2214    /// untouched so back-compat match sites keep their pattern.
2215    pub fn with_expr(mut self, expr: super::Expr) -> Self {
2216        self.expr = Some(expr);
2217        self
2218    }
2219}
2220
2221// ============================================================================
2222// Graph Commands
2223// ============================================================================
2224
2225/// Graph analytics command issued via SQL-like syntax
2226#[derive(Debug, Clone)]
2227pub struct GraphCommandOrderBy {
2228    pub metric: String,
2229    pub ascending: bool,
2230}
2231
2232#[derive(Debug, Clone)]
2233pub enum GraphCommand {
2234    /// GRAPH NEIGHBORHOOD 'source' [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2235    Neighborhood {
2236        source: String,
2237        depth: u32,
2238        direction: String,
2239        edge_labels: Option<Vec<String>>,
2240    },
2241    /// GRAPH SHORTEST_PATH 'source' TO 'target' [ALGORITHM alg] [DIRECTION dir] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2242    ShortestPath {
2243        source: String,
2244        target: String,
2245        algorithm: String,
2246        direction: String,
2247        limit: Option<u32>,
2248        order_by: Option<GraphCommandOrderBy>,
2249    },
2250    /// GRAPH TRAVERSE 'source' [STRATEGY bfs|dfs] [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2251    Traverse {
2252        source: String,
2253        strategy: String,
2254        depth: u32,
2255        direction: String,
2256        edge_labels: Option<Vec<String>>,
2257    },
2258    /// GRAPH CENTRALITY [ALGORITHM alg] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2259    ///
2260    /// `limit = None` keeps the historical implicit top-100 cap. `Some(n)`
2261    /// caps the returned rows at `n`.
2262    Centrality {
2263        algorithm: String,
2264        limit: Option<u32>,
2265        order_by: Option<GraphCommandOrderBy>,
2266    },
2267    /// GRAPH COMMUNITY [ALGORITHM alg] [MAX_ITERATIONS n] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2268    Community {
2269        algorithm: String,
2270        max_iterations: u32,
2271        limit: Option<u32>,
2272        order_by: Option<GraphCommandOrderBy>,
2273    },
2274    /// GRAPH COMPONENTS [MODE connected|weak|strong] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2275    Components {
2276        mode: String,
2277        limit: Option<u32>,
2278        order_by: Option<GraphCommandOrderBy>,
2279    },
2280    /// GRAPH CYCLES [MAX_LENGTH n]
2281    Cycles { max_length: u32 },
2282    /// GRAPH CLUSTERING
2283    Clustering,
2284    /// GRAPH TOPOLOGICAL_SORT
2285    TopologicalSort,
2286    /// GRAPH PROPERTIES ['<id-or-label>']
2287    ///
2288    /// `source = None` returns graph-wide stats. `source = Some("...")` returns
2289    /// the full property bag of a specific node, resolved via the same label
2290    /// index as `GRAPH NEIGHBORHOOD` / `GRAPH TRAVERSE` (issue #416).
2291    Properties { source: Option<String> },
2292}
2293
2294// ============================================================================
2295// Search Commands
2296// ============================================================================
2297
2298/// Search command issued via SQL-like syntax
2299#[derive(Debug, Clone)]
2300pub enum SearchCommand {
2301    /// SEARCH SIMILAR [v1, v2, ...] | $N | TEXT 'query' [COLLECTION col] [LIMIT n] [MIN_SCORE f] [USING provider]
2302    Similar {
2303        vector: Vec<f32>,
2304        text: Option<String>,
2305        provider: Option<String>,
2306        collection: String,
2307        limit: usize,
2308        min_score: f32,
2309        /// `$N` placeholder for the vector slot. `Some(idx)` when the SQL
2310        /// used `SEARCH SIMILAR $N ...`; the binder substitutes the
2311        /// user-supplied `Value::Vector` and clears this back to `None`.
2312        /// Runtime executors assert this is `None` post-bind.
2313        vector_param: Option<usize>,
2314        /// `$N` placeholder for the `LIMIT` slot (issue #361). The binder
2315        /// substitutes the user-supplied positive integer into `limit`
2316        /// and clears this back to `None`.
2317        limit_param: Option<usize>,
2318        /// `$N` placeholder for the `MIN_SCORE` slot (issue #361). The
2319        /// binder substitutes the user-supplied float into `min_score`
2320        /// and clears this back to `None`.
2321        min_score_param: Option<usize>,
2322        /// `$N` placeholder for `SEARCH SIMILAR TEXT $N` (issue #361).
2323        /// Binder substitutes the user-supplied text into `text` and
2324        /// clears this back to `None`.
2325        text_param: Option<usize>,
2326    },
2327    /// SEARCH TEXT 'query' [COLLECTION col] [LIMIT n] [FUZZY]
2328    Text {
2329        query: String,
2330        collection: Option<String>,
2331        limit: usize,
2332        fuzzy: bool,
2333        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2334        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2335        /// substitutes the user-supplied positive integer into `limit`
2336        /// and clears this back to `None`.
2337        limit_param: Option<usize>,
2338    },
2339    /// SEARCH HYBRID [vector] [TEXT 'query'] COLLECTION col [LIMIT n]
2340    Hybrid {
2341        vector: Option<Vec<f32>>,
2342        query: Option<String>,
2343        collection: String,
2344        limit: usize,
2345        /// `$N` placeholder for the `LIMIT` / `K` slot (issue #361).
2346        /// Same shape as `SearchCommand::Similar::limit_param`; the
2347        /// binder substitutes the user-supplied positive integer and
2348        /// clears this back to `None`.
2349        limit_param: Option<usize>,
2350    },
2351    /// SEARCH MULTIMODAL 'key_or_query' [COLLECTION col] [LIMIT n]
2352    Multimodal {
2353        query: String,
2354        collection: Option<String>,
2355        limit: usize,
2356        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2357        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2358        /// substitutes the user-supplied positive integer into `limit`
2359        /// and clears this back to `None`.
2360        limit_param: Option<usize>,
2361    },
2362    /// SEARCH INDEX index VALUE 'value' [COLLECTION col] [LIMIT n] [EXACT]
2363    Index {
2364        index: String,
2365        value: String,
2366        collection: Option<String>,
2367        limit: usize,
2368        exact: bool,
2369        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2370        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2371        /// substitutes the user-supplied positive integer into `limit`
2372        /// and clears this back to `None`.
2373        limit_param: Option<usize>,
2374    },
2375    /// SEARCH CONTEXT 'query' [FIELD field] [COLLECTION col] [LIMIT n] [DEPTH n]
2376    Context {
2377        query: String,
2378        field: Option<String>,
2379        collection: Option<String>,
2380        limit: usize,
2381        depth: usize,
2382        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2383        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2384        /// substitutes the user-supplied positive integer into `limit`
2385        /// and clears this back to `None`.
2386        limit_param: Option<usize>,
2387    },
2388    /// SEARCH SPATIAL RADIUS lat lon radius_km COLLECTION col COLUMN col [LIMIT n]
2389    SpatialRadius {
2390        center_lat: f64,
2391        center_lon: f64,
2392        radius_km: f64,
2393        collection: String,
2394        column: String,
2395        limit: usize,
2396        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2397        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2398        /// substitutes the user-supplied positive integer into `limit`
2399        /// and clears this back to `None`.
2400        limit_param: Option<usize>,
2401    },
2402    /// SEARCH SPATIAL BBOX min_lat min_lon max_lat max_lon COLLECTION col COLUMN col [LIMIT n]
2403    SpatialBbox {
2404        min_lat: f64,
2405        min_lon: f64,
2406        max_lat: f64,
2407        max_lon: f64,
2408        collection: String,
2409        column: String,
2410        limit: usize,
2411        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2412        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2413        /// substitutes the user-supplied positive integer into `limit`
2414        /// and clears this back to `None`.
2415        limit_param: Option<usize>,
2416    },
2417    /// SEARCH SPATIAL NEAREST lat lon K n COLLECTION col COLUMN col
2418    SpatialNearest {
2419        lat: f64,
2420        lon: f64,
2421        k: usize,
2422        collection: String,
2423        column: String,
2424        /// `$N` placeholder for the `K` slot (issue #361). Same shape
2425        /// as `SearchCommand::Hybrid::limit_param`; the binder
2426        /// substitutes the user-supplied positive integer into `k`
2427        /// and clears this back to `None`.
2428        k_param: Option<usize>,
2429    },
2430}
2431
2432// ============================================================================
2433// Time-Series DDL
2434// ============================================================================
2435
2436/// CREATE TIMESERIES name [RETENTION duration] [CHUNK_SIZE n] [DOWNSAMPLE spec[, spec...]]
2437///
2438/// `CREATE HYPERTABLE` lands on the same AST with `hypertable` populated.
2439/// The TimescaleDB-style syntax (time column + chunk_interval) gives the
2440/// runtime enough to register a `HypertableSpec` alongside the
2441/// underlying collection contract, so chunk routing and TTL sweeps can
2442/// address the table without a separate DDL.
2443#[derive(Debug, Clone)]
2444pub struct CreateTimeSeriesQuery {
2445    pub name: String,
2446    pub retention_ms: Option<u64>,
2447    pub chunk_size: Option<usize>,
2448    pub downsample_policies: Vec<String>,
2449    pub if_not_exists: bool,
2450    /// When `Some`, the DDL was spelled `CREATE HYPERTABLE` and the
2451    /// runtime must register the spec with the hypertable registry.
2452    pub hypertable: Option<HypertableDdl>,
2453}
2454
2455/// Hypertable-specific DDL fields — set only when the caller used
2456/// `CREATE HYPERTABLE`.
2457#[derive(Debug, Clone)]
2458pub struct HypertableDdl {
2459    /// Column that carries the nanosecond timestamp axis.
2460    pub time_column: String,
2461    /// Chunk width in nanoseconds.
2462    pub chunk_interval_ns: u64,
2463    /// Per-chunk default TTL in nanoseconds (`None` = no TTL).
2464    pub default_ttl_ns: Option<u64>,
2465}
2466
2467/// DROP TIMESERIES [IF EXISTS] name
2468#[derive(Debug, Clone)]
2469pub struct DropTimeSeriesQuery {
2470    pub name: String,
2471    pub if_exists: bool,
2472}
2473
2474// ============================================================================
2475// Queue DDL & Commands
2476// ============================================================================
2477
2478/// CREATE QUEUE name [MAX_SIZE n] [PRIORITY] [WITH TTL duration] [WITH DLQ name] [MAX_ATTEMPTS n]
2479#[derive(Debug, Clone)]
2480pub struct CreateQueueQuery {
2481    pub name: String,
2482    pub mode: QueueMode,
2483    pub priority: bool,
2484    pub max_size: Option<usize>,
2485    pub ttl_ms: Option<u64>,
2486    pub dlq: Option<String>,
2487    pub max_attempts: u32,
2488    pub if_not_exists: bool,
2489}
2490
2491/// ALTER QUEUE name SET MODE [FANOUT|WORK]
2492#[derive(Debug, Clone)]
2493pub struct AlterQueueQuery {
2494    pub name: String,
2495    pub mode: QueueMode,
2496}
2497
2498/// DROP QUEUE [IF EXISTS] name
2499#[derive(Debug, Clone)]
2500pub struct DropQueueQuery {
2501    pub name: String,
2502    pub if_exists: bool,
2503}
2504
2505/// SELECT <columns> FROM QUEUE name [WHERE filter] [LIMIT n]
2506#[derive(Debug, Clone)]
2507pub struct QueueSelectQuery {
2508    pub queue: String,
2509    pub columns: Vec<String>,
2510    pub filter: Option<Filter>,
2511    pub limit: Option<u64>,
2512}
2513
2514/// Which end of the queue
2515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2516pub enum QueueSide {
2517    Left,
2518    Right,
2519}
2520
2521/// Queue operation commands
2522#[derive(Debug, Clone)]
2523pub enum QueueCommand {
2524    Push {
2525        queue: String,
2526        value: Value,
2527        side: QueueSide,
2528        priority: Option<i32>,
2529    },
2530    Pop {
2531        queue: String,
2532        side: QueueSide,
2533        count: usize,
2534    },
2535    Peek {
2536        queue: String,
2537        count: usize,
2538    },
2539    Len {
2540        queue: String,
2541    },
2542    Purge {
2543        queue: String,
2544    },
2545    GroupCreate {
2546        queue: String,
2547        group: String,
2548    },
2549    GroupRead {
2550        queue: String,
2551        group: Option<String>,
2552        consumer: String,
2553        count: usize,
2554    },
2555    Pending {
2556        queue: String,
2557        group: String,
2558    },
2559    Claim {
2560        queue: String,
2561        group: String,
2562        consumer: String,
2563        min_idle_ms: u64,
2564    },
2565    Ack {
2566        queue: String,
2567        group: String,
2568        message_id: String,
2569    },
2570    Nack {
2571        queue: String,
2572        group: String,
2573        message_id: String,
2574    },
2575    Move {
2576        source: String,
2577        destination: String,
2578        filter: Option<Filter>,
2579        limit: usize,
2580    },
2581}
2582
2583// ============================================================================
2584// Tree DDL & Commands
2585// ============================================================================
2586
2587#[derive(Debug, Clone)]
2588pub struct TreeNodeSpec {
2589    pub label: String,
2590    pub node_type: Option<String>,
2591    pub properties: Vec<(String, Value)>,
2592    pub metadata: Vec<(String, Value)>,
2593    pub max_children: Option<usize>,
2594}
2595
2596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2597pub enum TreePosition {
2598    First,
2599    Last,
2600    Index(usize),
2601}
2602
2603#[derive(Debug, Clone)]
2604pub struct CreateTreeQuery {
2605    pub collection: String,
2606    pub name: String,
2607    pub root: TreeNodeSpec,
2608    pub default_max_children: usize,
2609    pub if_not_exists: bool,
2610}
2611
2612#[derive(Debug, Clone)]
2613pub struct DropTreeQuery {
2614    pub collection: String,
2615    pub name: String,
2616    pub if_exists: bool,
2617}
2618
2619#[derive(Debug, Clone)]
2620pub enum TreeCommand {
2621    Insert {
2622        collection: String,
2623        tree_name: String,
2624        parent_id: u64,
2625        node: TreeNodeSpec,
2626        position: TreePosition,
2627    },
2628    Move {
2629        collection: String,
2630        tree_name: String,
2631        node_id: u64,
2632        parent_id: u64,
2633        position: TreePosition,
2634    },
2635    Delete {
2636        collection: String,
2637        tree_name: String,
2638        node_id: u64,
2639    },
2640    Validate {
2641        collection: String,
2642        tree_name: String,
2643    },
2644    Rebalance {
2645        collection: String,
2646        tree_name: String,
2647        dry_run: bool,
2648    },
2649}
2650
2651// ============================================================================
2652// KV DSL Commands
2653// ============================================================================
2654
2655/// KV verb commands: `KV PUT key = value [EXPIRE n] [IF NOT EXISTS]`, `KV GET key`, `KV DELETE key`
2656#[derive(Debug, Clone)]
2657pub enum KvCommand {
2658    Put {
2659        model: CollectionModel,
2660        collection: String,
2661        key: String,
2662        value: Value,
2663        /// TTL in milliseconds (from EXPIRE clause)
2664        ttl_ms: Option<u64>,
2665        tags: Vec<String>,
2666        if_not_exists: bool,
2667    },
2668    InvalidateTags {
2669        collection: String,
2670        tags: Vec<String>,
2671    },
2672    Get {
2673        model: CollectionModel,
2674        collection: String,
2675        key: String,
2676    },
2677    Unseal {
2678        collection: String,
2679        key: String,
2680        version: Option<i64>,
2681    },
2682    Rotate {
2683        collection: String,
2684        key: String,
2685        value: Value,
2686        tags: Vec<String>,
2687    },
2688    History {
2689        collection: String,
2690        key: String,
2691    },
2692    List {
2693        model: CollectionModel,
2694        collection: String,
2695        prefix: Option<String>,
2696        limit: Option<usize>,
2697        offset: usize,
2698    },
2699    Purge {
2700        collection: String,
2701        key: String,
2702    },
2703    Watch {
2704        model: CollectionModel,
2705        collection: String,
2706        key: String,
2707        prefix: bool,
2708        from_lsn: Option<u64>,
2709    },
2710    Delete {
2711        model: CollectionModel,
2712        collection: String,
2713        key: String,
2714    },
2715    /// `KV INCR key [BY n] [EXPIRE dur]` — atomic increment; negative `by` = decrement.
2716    Incr {
2717        model: CollectionModel,
2718        collection: String,
2719        key: String,
2720        /// Step value; negative for DECR. Defaults to 1.
2721        by: i64,
2722        ttl_ms: Option<u64>,
2723    },
2724    /// `KV CAS key EXPECT <expected|NULL> SET <new> [EXPIRE dur]` — compare-and-set.
2725    ///
2726    /// `expected = None` means `EXPECT NULL` (key must be absent).
2727    Cas {
2728        model: CollectionModel,
2729        collection: String,
2730        key: String,
2731        /// The value the caller expects to be current; `None` = key must be absent.
2732        expected: Option<Value>,
2733        new_value: Value,
2734        ttl_ms: Option<u64>,
2735    },
2736}
2737
2738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2739pub enum ConfigValueType {
2740    Bool,
2741    Int,
2742    String,
2743    Url,
2744    Object,
2745    Array,
2746}
2747
2748impl ConfigValueType {
2749    pub fn as_str(self) -> &'static str {
2750        match self {
2751            Self::Bool => "bool",
2752            Self::Int => "int",
2753            Self::String => "string",
2754            Self::Url => "url",
2755            Self::Object => "object",
2756            Self::Array => "array",
2757        }
2758    }
2759
2760    pub fn parse(input: &str) -> Option<Self> {
2761        match input.to_ascii_lowercase().as_str() {
2762            "bool" | "boolean" => Some(Self::Bool),
2763            "int" | "integer" => Some(Self::Int),
2764            "string" | "str" | "text" => Some(Self::String),
2765            "url" => Some(Self::Url),
2766            "object" | "json_object" => Some(Self::Object),
2767            "array" | "list" => Some(Self::Array),
2768            _ => None,
2769        }
2770    }
2771}
2772
2773#[derive(Debug, Clone)]
2774pub enum ConfigCommand {
2775    Put {
2776        collection: String,
2777        key: String,
2778        value: Value,
2779        value_type: Option<ConfigValueType>,
2780        tags: Vec<String>,
2781    },
2782    Get {
2783        collection: String,
2784        key: String,
2785    },
2786    Resolve {
2787        collection: String,
2788        key: String,
2789    },
2790    Rotate {
2791        collection: String,
2792        key: String,
2793        value: Value,
2794        value_type: Option<ConfigValueType>,
2795        tags: Vec<String>,
2796    },
2797    Delete {
2798        collection: String,
2799        key: String,
2800    },
2801    History {
2802        collection: String,
2803        key: String,
2804    },
2805    List {
2806        collection: String,
2807        prefix: Option<String>,
2808        limit: Option<usize>,
2809        offset: usize,
2810    },
2811    Watch {
2812        collection: String,
2813        key: String,
2814        prefix: bool,
2815        from_lsn: Option<u64>,
2816    },
2817    InvalidVolatileOperation {
2818        operation: String,
2819        collection: String,
2820        key: Option<String>,
2821    },
2822}
2823
2824// ============================================================================
2825// Builders (Fluent API)
2826// ============================================================================