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    /// `REFRESH EVERY <duration>` clause — only valid on materialized
486    /// views. When set, a background scheduler ticks the view at this
487    /// cadence. `None` means refresh-on-demand only (slice 9 behaviour).
488    /// Issue #583 slice 10.
489    pub refresh_every_ms: Option<u64>,
490    /// `WITH RETENTION <duration>` clause — only valid on materialized
491    /// views (issue #584 slice 12). Opts the view's backing storage
492    /// into a retention policy independent of the source. Persisted on
493    /// the view definition; the physical sweep is applied to the
494    /// MV's backing rows once slice-9's row-storage follow-up lands.
495    /// `None` means the view is unaffected by source retention.
496    pub retention_duration_ms: Option<u64>,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct DropViewQuery {
501    pub name: String,
502    pub materialized: bool,
503    pub if_exists: bool,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
507pub struct RefreshMaterializedViewQuery {
508    pub name: String,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct CopyFromQuery {
513    pub table: String,
514    pub path: String,
515    pub format: CopyFormat,
516    pub delimiter: Option<char>,
517    pub has_header: bool,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub enum CopyFormat {
522    Csv,
523}
524
525#[derive(Debug, Clone, PartialEq, Eq)]
526pub struct CreateSchemaQuery {
527    pub name: String,
528    pub if_not_exists: bool,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub struct DropSchemaQuery {
533    pub name: String,
534    pub if_exists: bool,
535    pub cascade: bool,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub struct CreateSequenceQuery {
540    pub name: String,
541    pub if_not_exists: bool,
542    /// First value produced by `nextval`. Default 1.
543    pub start: i64,
544    /// Added to the current value on each `nextval`. Default 1.
545    pub increment: i64,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq)]
549pub struct DropSequenceQuery {
550    pub name: String,
551    pub if_exists: bool,
552}
553
554/// Transaction-control statement variants. See [`QueryExpr::TransactionControl`].
555#[derive(Debug, Clone, PartialEq, Eq)]
556pub enum TxnControl {
557    /// `BEGIN [WORK | TRANSACTION]`, `START TRANSACTION`
558    Begin,
559    /// `COMMIT [WORK | TRANSACTION]`, `END`
560    Commit,
561    /// `ROLLBACK [WORK | TRANSACTION]`
562    Rollback,
563    /// `SAVEPOINT name`
564    Savepoint(String),
565    /// `RELEASE [SAVEPOINT] name`
566    ReleaseSavepoint(String),
567    /// `ROLLBACK TO [SAVEPOINT] name`
568    RollbackToSavepoint(String),
569}
570
571/// Maintenance command variants. See [`QueryExpr::MaintenanceCommand`].
572#[derive(Debug, Clone, PartialEq, Eq)]
573pub enum MaintenanceCommand {
574    /// `VACUUM [FULL] [table]`
575    ///
576    /// Triggers segment compaction and planner stats refresh. `FULL` additionally
577    /// forces a full pager sync. Target `None` applies to every collection.
578    Vacuum { target: Option<String>, full: bool },
579    /// `ANALYZE [table]`
580    ///
581    /// Refreshes planner statistics (histogram, distinct estimates, null counts).
582    /// Target `None` re-analyzes every collection.
583    Analyze { target: Option<String> },
584}
585
586/// AST node for `EXPLAIN ALTER FOR <CreateTableStmt> [FORMAT JSON]`.
587///
588/// `target` carries the CREATE TABLE structure exactly as the
589/// parser produces it for a regular CREATE — full reuse of
590/// `parse_create_table_body`. `format` determines whether the
591/// executor emits a `ALTER TABLE …;`-flavored text payload
592/// (the default — copy-paste friendly into the REPL) or a
593/// structured JSON object (machine-friendly).
594#[derive(Debug, Clone)]
595pub struct ExplainAlterQuery {
596    pub target: CreateTableQuery,
597    pub format: ExplainFormat,
598}
599
600/// Output format requested for an `EXPLAIN ALTER` command.
601#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
602pub enum ExplainFormat {
603    /// Plain SQL text — `ALTER TABLE …;` lines plus header
604    /// comments and rename hints. Default; copy-paste friendly.
605    #[default]
606    Sql,
607    /// Structured JSON object with `operations`,
608    /// `rename_candidates`, `summary`. Machine-friendly for
609    /// driver code (Purple migration generator, dashboards,
610    /// CLI tools).
611    Json,
612}
613
614#[derive(Debug, Clone, PartialEq)]
615pub struct CreateMigrationQuery {
616    pub name: String,
617    pub body: String,
618    pub depends_on: Vec<String>,
619    pub batch_size: Option<u64>,
620    pub no_rollback: bool,
621}
622
623#[derive(Debug, Clone, PartialEq)]
624pub struct ApplyMigrationQuery {
625    pub target: ApplyMigrationTarget,
626    pub for_tenant: Option<String>,
627}
628
629#[derive(Debug, Clone, PartialEq)]
630pub enum ApplyMigrationTarget {
631    Named(String),
632    All,
633}
634
635#[derive(Debug, Clone, PartialEq)]
636pub struct RollbackMigrationQuery {
637    pub name: String,
638}
639
640#[derive(Debug, Clone, PartialEq)]
641pub struct ExplainMigrationQuery {
642    pub name: String,
643}
644
645/// Probabilistic data structure commands
646#[derive(Debug, Clone)]
647pub enum ProbabilisticCommand {
648    // HyperLogLog
649    CreateHll {
650        name: String,
651        precision: u8,
652        if_not_exists: bool,
653    },
654    HllAdd {
655        name: String,
656        elements: Vec<String>,
657    },
658    HllCount {
659        names: Vec<String>,
660    },
661    HllMerge {
662        dest: String,
663        sources: Vec<String>,
664    },
665    HllInfo {
666        name: String,
667    },
668    DropHll {
669        name: String,
670        if_exists: bool,
671    },
672
673    // Count-Min Sketch (Fase 7)
674    CreateSketch {
675        name: String,
676        width: usize,
677        depth: usize,
678        if_not_exists: bool,
679    },
680    SketchAdd {
681        name: String,
682        element: String,
683        count: u64,
684    },
685    SketchCount {
686        name: String,
687        element: String,
688    },
689    SketchMerge {
690        dest: String,
691        sources: Vec<String>,
692    },
693    SketchInfo {
694        name: String,
695    },
696    DropSketch {
697        name: String,
698        if_exists: bool,
699    },
700
701    // Cuckoo Filter (Fase 8)
702    CreateFilter {
703        name: String,
704        capacity: usize,
705        if_not_exists: bool,
706    },
707    FilterAdd {
708        name: String,
709        element: String,
710    },
711    FilterCheck {
712        name: String,
713        element: String,
714    },
715    FilterDelete {
716        name: String,
717        element: String,
718    },
719    FilterCount {
720        name: String,
721    },
722    FilterInfo {
723        name: String,
724    },
725    DropFilter {
726        name: String,
727        if_exists: bool,
728    },
729}
730
731/// Index type for CREATE INDEX ... USING <type>
732#[derive(Debug, Clone, PartialEq, Eq)]
733pub enum IndexMethod {
734    BTree,
735    Hash,
736    Bitmap,
737    RTree,
738}
739
740impl fmt::Display for IndexMethod {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        match self {
743            Self::BTree => write!(f, "BTREE"),
744            Self::Hash => write!(f, "HASH"),
745            Self::Bitmap => write!(f, "BITMAP"),
746            Self::RTree => write!(f, "RTREE"),
747        }
748    }
749}
750
751/// CREATE INDEX [UNIQUE] [IF NOT EXISTS] name ON table (col1, col2, ...) [USING method]
752#[derive(Debug, Clone)]
753pub struct CreateIndexQuery {
754    pub name: String,
755    pub table: String,
756    pub columns: Vec<String>,
757    pub method: IndexMethod,
758    pub unique: bool,
759    pub if_not_exists: bool,
760}
761
762/// DROP INDEX [IF EXISTS] name ON table
763#[derive(Debug, Clone)]
764pub struct DropIndexQuery {
765    pub name: String,
766    pub table: String,
767    pub if_exists: bool,
768}
769
770/// ASK 'question' [USING provider] [MODEL 'model'] [DEPTH n] [LIMIT n] [MIN_SCORE x]
771///                [COLLECTION col] [TEMPERATURE x] [SEED n] [STRICT ON|OFF] [STREAM]
772///                [CACHE TTL '5m' | NOCACHE]
773///
774/// `temperature` and `seed` are per-query overrides resolved by the
775/// `DeterminismDecider` (issue #400). The parser merely surfaces the
776/// requested values; capability-based dropping happens at decide time.
777#[derive(Debug, Clone)]
778pub struct AskQuery {
779    /// `EXPLAIN ASK '...'` returns the retrieval/provider/cost plan
780    /// without making the LLM call.
781    pub explain: bool,
782    pub question: String,
783    /// Optional `$N` / `?` parameter slot for the question text.
784    pub question_param: Option<usize>,
785    pub provider: Option<String>,
786    pub model: Option<String>,
787    pub depth: Option<usize>,
788    pub limit: Option<usize>,
789    pub min_score: Option<f32>,
790    pub collection: Option<String>,
791    /// Per-query temperature override (`ASK '...' TEMPERATURE 0.7`).
792    /// `None` means fall back to `ask.default_temperature`.
793    pub temperature: Option<f32>,
794    /// Per-query seed override (`ASK '...' SEED 42`). `None` means the
795    /// decider derives one from `hash(question + sources_fingerprint)`.
796    pub seed: Option<u64>,
797    /// Strict citation validation is on by default. `STRICT OFF` keeps
798    /// citation diagnostics as warnings and skips retry/error handling.
799    pub strict: bool,
800    /// HTTP-only SSE response requested via `ASK '...' STREAM`.
801    pub stream: bool,
802    /// Per-query answer-cache override.
803    pub cache: AskCacheClause,
804}
805
806#[derive(Debug, Clone, PartialEq, Eq, Default)]
807pub enum AskCacheClause {
808    #[default]
809    Default,
810    CacheTtl(String),
811    NoCache,
812}
813
814impl QueryExpr {
815    /// Create a table query
816    pub fn table(name: &str) -> TableQueryBuilder {
817        TableQueryBuilder::new(name)
818    }
819
820    /// Create a graph query
821    pub fn graph() -> GraphQueryBuilder {
822        GraphQueryBuilder::new()
823    }
824
825    /// Create a path query
826    pub fn path(from: NodeSelector, to: NodeSelector) -> PathQueryBuilder {
827        PathQueryBuilder::new(from, to)
828    }
829}
830
831// ============================================================================
832// Table Query
833// ============================================================================
834
835/// Table query: SELECT columns FROM table WHERE filter ORDER BY ... LIMIT ...
836#[derive(Debug, Clone)]
837pub struct TableQuery {
838    /// Table name. Legacy slot — still populated even when `source`
839    /// is set to a subquery so existing call sites that read
840    /// `query.table.as_str()` keep compiling. When `source` is
841    /// `Some(TableSource::Subquery(…))`, this field holds a synthetic
842    /// sentinel name (`"__subq_NNNN"`) that runtime code must never
843    /// resolve against the real schema registry.
844    pub table: String,
845    /// Fase 2 Week 3: structured table source. `None` means the
846    /// legacy `table` field is authoritative. `Some(Name)` is the
847    /// same information as `table` but in typed form. `Some(Subquery)`
848    /// wires a `(SELECT …) AS alias` in a FROM position — the Fase
849    /// 1.7 unlock.
850    pub source: Option<TableSource>,
851    /// Optional table alias
852    pub alias: Option<String>,
853    /// Canonical SQL select list.
854    pub select_items: Vec<SelectItem>,
855    /// Columns to select (empty = all)
856    pub columns: Vec<Projection>,
857    /// Canonical SQL WHERE clause.
858    pub where_expr: Option<super::Expr>,
859    /// Filter condition
860    pub filter: Option<Filter>,
861    /// Canonical SQL GROUP BY items.
862    pub group_by_exprs: Vec<super::Expr>,
863    /// GROUP BY fields
864    pub group_by: Vec<String>,
865    /// Canonical SQL HAVING clause.
866    pub having_expr: Option<super::Expr>,
867    /// HAVING filter (applied after grouping)
868    pub having: Option<Filter>,
869    /// Order by clauses
870    pub order_by: Vec<OrderByClause>,
871    /// Limit
872    pub limit: Option<u64>,
873    /// User-supplied-parameter slot for `LIMIT $N`. Set by the parser
874    /// when the LIMIT clause references `$N`/`?` instead of a literal;
875    /// cleared by the binder (`user_params::bind`) after substituting
876    /// the parameter into `limit`. Mirrors the `limit_param` slot on
877    /// `SearchCommand` variants — see #361 slice 11.
878    pub limit_param: Option<usize>,
879    /// Offset
880    pub offset: Option<u64>,
881    /// User-supplied-parameter slot for `OFFSET $N`. Same lifecycle as
882    /// `limit_param`. See #361 slice 11.
883    pub offset_param: Option<usize>,
884    /// WITH EXPAND options (graph traversal, cross-ref following)
885    pub expand: Option<ExpandOptions>,
886    /// Time-travel anchor. When present the executor resolves this
887    /// to an MVCC xid and evaluates the query against that snapshot
888    /// instead of the current one. Mirrors git's `AS OF` semantics.
889    pub as_of: Option<AsOfClause>,
890    /// `SESSIONIZE BY <actor> GAP <duration> [ORDER BY <ts>]` operator
891    /// (issue #585 slice 8). When present, the executor annotates each
892    /// result row with a `session_id` column. `actor_col` / `gap_ms`
893    /// may be `None` when the source collection's descriptor (slice 1
894    /// `SESSION_KEY` / `SESSION_GAP`) supplies the defaults; one
895    /// without the other resolved at execution time is the typed
896    /// `MissingSessionKey` error.
897    pub sessionize: Option<SessionizeClause>,
898}
899
900/// `SESSIONIZE BY <actor_col> GAP <duration> [ORDER BY <ts_col>]`.
901#[derive(Debug, Clone, Default)]
902pub struct SessionizeClause {
903    /// Explicit `BY <ident>`. `None` means "default from descriptor's
904    /// `SESSION_KEY`" — resolved at execution time.
905    pub actor_col: Option<String>,
906    /// Explicit `GAP <duration>` in milliseconds. `None` means
907    /// "default from descriptor's `SESSION_GAP`".
908    pub gap_ms: Option<u64>,
909    /// Explicit `ORDER BY <ident>` immediately after `GAP`. When
910    /// `None` the executor falls back to the collection's timestamp
911    /// column (the same resolution as `retention_filter`).
912    pub order_col: Option<String>,
913}
914
915/// Source spec for `AS OF` — parsed form sits in `TableQuery`, then
916/// `vcs_resolve_as_of` turns it into an MVCC xid at execute time.
917#[derive(Debug, Clone)]
918pub enum AsOfClause {
919    /// Explicit commit hash literal: `AS OF COMMIT '<hex>'`.
920    Commit(String),
921    /// Branch or ref: `AS OF BRANCH 'main'` or `AS OF 'refs/heads/main'`.
922    Branch(String),
923    /// Tag: `AS OF TAG 'v1.0'`.
924    Tag(String),
925    /// Unix epoch milliseconds: `AS OF TIMESTAMP 1710000000000`.
926    TimestampMs(i64),
927    /// Raw MVCC snapshot xid: `AS OF SNAPSHOT 12345`.
928    Snapshot(u64),
929}
930
931/// Structured FROM source for a `TableQuery`. Additive alongside the
932/// legacy `TableQuery.table: String` slot — callers that understand
933/// this type can branch on subqueries; callers that only read `table`
934/// fall back to the synthetic sentinel name and, for subqueries,
935/// produce an "unknown table" error until they migrate.
936#[derive(Debug, Clone)]
937pub enum TableSource {
938    /// Plain table reference — equivalent to the legacy `String` form.
939    Name(String),
940    /// A subquery in FROM position: `FROM (SELECT …) AS alias`.
941    Subquery(Box<QueryExpr>),
942}
943
944/// Options for WITH EXPAND clause on SELECT queries.
945#[derive(Debug, Clone, Default)]
946pub struct ExpandOptions {
947    /// Expand via graph edges (WITH EXPAND GRAPH)
948    pub graph: bool,
949    /// Graph expansion depth (DEPTH n)
950    pub graph_depth: usize,
951    /// Expand via cross-references (WITH EXPAND CROSS_REFS)
952    pub cross_refs: bool,
953    /// Index hint from the optimizer (which index to prefer for this query)
954    pub index_hint: Option<crate::storage::query::planner::optimizer::IndexHint>,
955}
956
957impl TableQuery {
958    /// Create a new table query
959    pub fn new(table: &str) -> Self {
960        Self {
961            table: table.to_string(),
962            source: None,
963            alias: None,
964            select_items: Vec::new(),
965            columns: Vec::new(),
966            where_expr: None,
967            filter: None,
968            group_by_exprs: Vec::new(),
969            group_by: Vec::new(),
970            having_expr: None,
971            having: None,
972            order_by: Vec::new(),
973            limit: None,
974            limit_param: None,
975            offset: None,
976            offset_param: None,
977            expand: None,
978            as_of: None,
979            sessionize: None,
980        }
981    }
982
983    /// Create a TableQuery that wraps a subquery in FROM position.
984    /// The legacy `table` slot holds a synthetic sentinel so code that
985    /// only reads `table.as_str()` errors loudly with a
986    /// recognisable marker instead of silently treating it as a
987    /// real collection.
988    pub fn from_subquery(subquery: QueryExpr, alias: Option<String>) -> Self {
989        let sentinel = match &alias {
990            Some(a) => format!("__subq_{a}"),
991            None => "__subq_anon".to_string(),
992        };
993        Self {
994            table: sentinel,
995            source: Some(TableSource::Subquery(Box::new(subquery))),
996            alias,
997            select_items: Vec::new(),
998            columns: Vec::new(),
999            where_expr: None,
1000            filter: None,
1001            group_by_exprs: Vec::new(),
1002            group_by: Vec::new(),
1003            having_expr: None,
1004            having: None,
1005            order_by: Vec::new(),
1006            limit: None,
1007            limit_param: None,
1008            offset: None,
1009            offset_param: None,
1010            expand: None,
1011            as_of: None,
1012            sessionize: None,
1013        }
1014    }
1015}
1016
1017/// Canonical SQL select item for table queries.
1018#[derive(Debug, Clone, PartialEq)]
1019pub enum SelectItem {
1020    Wildcard,
1021    Expr {
1022        expr: super::Expr,
1023        alias: Option<String>,
1024    },
1025}
1026
1027// ============================================================================
1028// Graph Query
1029// ============================================================================
1030
1031/// Graph query: MATCH pattern WHERE filter RETURN projection
1032#[derive(Debug, Clone)]
1033pub struct GraphQuery {
1034    /// Optional outer alias when used as a join source
1035    pub alias: Option<String>,
1036    /// Graph pattern to match
1037    pub pattern: GraphPattern,
1038    /// Filter condition
1039    pub filter: Option<Filter>,
1040    /// Return projections
1041    pub return_: Vec<Projection>,
1042    /// Optional row limit
1043    pub limit: Option<u64>,
1044}
1045
1046impl GraphQuery {
1047    /// Create a new graph query
1048    pub fn new(pattern: GraphPattern) -> Self {
1049        Self {
1050            alias: None,
1051            pattern,
1052            filter: None,
1053            return_: Vec::new(),
1054            limit: None,
1055        }
1056    }
1057
1058    /// Set outer alias
1059    pub fn alias(mut self, alias: &str) -> Self {
1060        self.alias = Some(alias.to_string());
1061        self
1062    }
1063}
1064
1065/// Graph pattern: collection of node and edge patterns
1066#[derive(Debug, Clone, Default)]
1067pub struct GraphPattern {
1068    /// Node patterns
1069    pub nodes: Vec<NodePattern>,
1070    /// Edge patterns connecting nodes
1071    pub edges: Vec<EdgePattern>,
1072}
1073
1074impl GraphPattern {
1075    /// Create an empty pattern
1076    pub fn new() -> Self {
1077        Self::default()
1078    }
1079
1080    /// Add a node pattern
1081    pub fn node(mut self, pattern: NodePattern) -> Self {
1082        self.nodes.push(pattern);
1083        self
1084    }
1085
1086    /// Add an edge pattern
1087    pub fn edge(mut self, pattern: EdgePattern) -> Self {
1088        self.edges.push(pattern);
1089        self
1090    }
1091}
1092
1093/// Node pattern: (alias:Type {properties})
1094#[derive(Debug, Clone)]
1095pub struct NodePattern {
1096    /// Variable alias for this node
1097    pub alias: String,
1098    /// Optional label filter. Stored as the user-supplied label string so
1099    /// the parser is registry-free; executors resolve it against the live
1100    /// [`crate::storage::engine::graph_store::LabelRegistry`].
1101    pub node_label: Option<String>,
1102    /// Property filters
1103    pub properties: Vec<PropertyFilter>,
1104}
1105
1106impl NodePattern {
1107    /// Create a new node pattern
1108    pub fn new(alias: &str) -> Self {
1109        Self {
1110            alias: alias.to_string(),
1111            node_label: None,
1112            properties: Vec::new(),
1113        }
1114    }
1115
1116    /// Set the label filter (string form — preferred).
1117    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1118        self.node_label = Some(label.into());
1119        self
1120    }
1121
1122    /// Add property filter
1123    pub fn with_property(mut self, name: &str, op: CompareOp, value: Value) -> Self {
1124        self.properties.push(PropertyFilter {
1125            name: name.to_string(),
1126            op,
1127            value,
1128        });
1129        self
1130    }
1131}
1132
1133/// Edge pattern: -[alias:Type*min..max]->
1134#[derive(Debug, Clone)]
1135pub struct EdgePattern {
1136    /// Optional alias for this edge
1137    pub alias: Option<String>,
1138    /// Source node alias
1139    pub from: String,
1140    /// Target node alias
1141    pub to: String,
1142    /// Optional label filter (user-supplied string).
1143    pub edge_label: Option<String>,
1144    /// Edge direction
1145    pub direction: EdgeDirection,
1146    /// Minimum hops (for variable-length patterns)
1147    pub min_hops: u32,
1148    /// Maximum hops (for variable-length patterns)
1149    pub max_hops: u32,
1150}
1151
1152impl EdgePattern {
1153    /// Create a new edge pattern
1154    pub fn new(from: &str, to: &str) -> Self {
1155        Self {
1156            alias: None,
1157            from: from.to_string(),
1158            to: to.to_string(),
1159            edge_label: None,
1160            direction: EdgeDirection::Outgoing,
1161            min_hops: 1,
1162            max_hops: 1,
1163        }
1164    }
1165
1166    /// Set label filter (string form — preferred).
1167    pub fn of_label(mut self, label: impl Into<String>) -> Self {
1168        self.edge_label = Some(label.into());
1169        self
1170    }
1171
1172    /// Set direction
1173    pub fn direction(mut self, dir: EdgeDirection) -> Self {
1174        self.direction = dir;
1175        self
1176    }
1177
1178    /// Set hop range for variable-length patterns
1179    pub fn hops(mut self, min: u32, max: u32) -> Self {
1180        self.min_hops = min;
1181        self.max_hops = max;
1182        self
1183    }
1184
1185    /// Set alias
1186    pub fn alias(mut self, alias: &str) -> Self {
1187        self.alias = Some(alias.to_string());
1188        self
1189    }
1190}
1191
1192/// Edge direction
1193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1194pub enum EdgeDirection {
1195    /// Outgoing: (a)-[r]->(b)
1196    Outgoing,
1197    /// Incoming: (a)<-[r]-(b)
1198    Incoming,
1199    /// Both: (a)-[r]-(b)
1200    Both,
1201}
1202
1203/// Property filter: name op value
1204#[derive(Debug, Clone)]
1205pub struct PropertyFilter {
1206    pub name: String,
1207    pub op: CompareOp,
1208    pub value: Value,
1209}
1210
1211// ============================================================================
1212// Join Query
1213// ============================================================================
1214
1215/// Join query: combines table and graph queries
1216#[derive(Debug, Clone)]
1217pub struct JoinQuery {
1218    /// Left side (typically table)
1219    pub left: Box<QueryExpr>,
1220    /// Right side (typically graph)
1221    pub right: Box<QueryExpr>,
1222    /// Join type
1223    pub join_type: JoinType,
1224    /// Join condition
1225    pub on: JoinCondition,
1226    /// Post-join filter condition
1227    pub filter: Option<Filter>,
1228    /// Post-join ordering
1229    pub order_by: Vec<OrderByClause>,
1230    /// Post-join limit
1231    pub limit: Option<u64>,
1232    /// Post-join offset
1233    pub offset: Option<u64>,
1234    /// Canonical SQL RETURN projection.
1235    pub return_items: Vec<SelectItem>,
1236    /// Post-join projection
1237    pub return_: Vec<Projection>,
1238}
1239
1240impl JoinQuery {
1241    /// Create a new join query
1242    pub fn new(left: QueryExpr, right: QueryExpr, on: JoinCondition) -> Self {
1243        Self {
1244            left: Box::new(left),
1245            right: Box::new(right),
1246            join_type: JoinType::Inner,
1247            on,
1248            filter: None,
1249            order_by: Vec::new(),
1250            limit: None,
1251            offset: None,
1252            return_items: Vec::new(),
1253            return_: Vec::new(),
1254        }
1255    }
1256
1257    /// Set join type
1258    pub fn join_type(mut self, jt: JoinType) -> Self {
1259        self.join_type = jt;
1260        self
1261    }
1262}
1263
1264/// Join type
1265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1266pub enum JoinType {
1267    /// Inner join — only matching pairs emitted
1268    Inner,
1269    /// Left outer join — every left row, matched or padded with nulls on the right
1270    LeftOuter,
1271    /// Right outer join — every right row, matched or padded with nulls on the left
1272    RightOuter,
1273    /// Full outer join — LeftOuter ∪ RightOuter, each unmatched side padded
1274    FullOuter,
1275    /// Cross join — Cartesian product, no predicate
1276    Cross,
1277}
1278
1279/// Join condition: how to match rows with nodes
1280#[derive(Debug, Clone)]
1281pub struct JoinCondition {
1282    /// Left field (table side)
1283    pub left_field: FieldRef,
1284    /// Right field (graph side)
1285    pub right_field: FieldRef,
1286}
1287
1288impl JoinCondition {
1289    /// Create a new join condition
1290    pub fn new(left: FieldRef, right: FieldRef) -> Self {
1291        Self {
1292            left_field: left,
1293            right_field: right,
1294        }
1295    }
1296}
1297
1298/// Reference to a field (table column, node property, or edge property)
1299#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1300pub enum FieldRef {
1301    /// Table column: table.column
1302    TableColumn { table: String, column: String },
1303    /// Node property: alias.property
1304    NodeProperty { alias: String, property: String },
1305    /// Edge property: alias.property
1306    EdgeProperty { alias: String, property: String },
1307    /// Node ID: alias.id
1308    NodeId { alias: String },
1309}
1310
1311impl FieldRef {
1312    /// Create a table column reference
1313    pub fn column(table: &str, column: &str) -> Self {
1314        Self::TableColumn {
1315            table: table.to_string(),
1316            column: column.to_string(),
1317        }
1318    }
1319
1320    /// Create a node property reference
1321    pub fn node_prop(alias: &str, property: &str) -> Self {
1322        Self::NodeProperty {
1323            alias: alias.to_string(),
1324            property: property.to_string(),
1325        }
1326    }
1327
1328    /// Create a node ID reference
1329    pub fn node_id(alias: &str) -> Self {
1330        Self::NodeId {
1331            alias: alias.to_string(),
1332        }
1333    }
1334
1335    /// Create an edge property reference
1336    pub fn edge_prop(alias: &str, property: &str) -> Self {
1337        Self::EdgeProperty {
1338            alias: alias.to_string(),
1339            property: property.to_string(),
1340        }
1341    }
1342}
1343
1344// ============================================================================
1345// Path Query
1346// ============================================================================
1347
1348/// Path query: find paths between nodes
1349#[derive(Debug, Clone)]
1350pub struct PathQuery {
1351    /// Optional outer alias when used as a join source
1352    pub alias: Option<String>,
1353    /// Source node selector
1354    pub from: NodeSelector,
1355    /// Target node selector
1356    pub to: NodeSelector,
1357    /// Edge labels to traverse (empty = any). Strings are resolved against
1358    /// the runtime registry by the executor.
1359    pub via: Vec<String>,
1360    /// Maximum path length
1361    pub max_length: u32,
1362    /// Filter on paths
1363    pub filter: Option<Filter>,
1364    /// Return projections
1365    pub return_: Vec<Projection>,
1366}
1367
1368impl PathQuery {
1369    /// Create a new path query
1370    pub fn new(from: NodeSelector, to: NodeSelector) -> Self {
1371        Self {
1372            alias: None,
1373            from,
1374            to,
1375            via: Vec::new(),
1376            max_length: 10,
1377            filter: None,
1378            return_: Vec::new(),
1379        }
1380    }
1381
1382    /// Set outer alias
1383    pub fn alias(mut self, alias: &str) -> Self {
1384        self.alias = Some(alias.to_string());
1385        self
1386    }
1387
1388    /// Add an edge label constraint to traverse (string form).
1389    pub fn via_label(mut self, label: impl Into<String>) -> Self {
1390        self.via.push(label.into());
1391        self
1392    }
1393}
1394
1395/// Node selector for path queries
1396#[derive(Debug, Clone)]
1397pub enum NodeSelector {
1398    /// By node ID
1399    ById(String),
1400    /// By node label and property
1401    ByType {
1402        node_label: String,
1403        filter: Option<PropertyFilter>,
1404    },
1405    /// By table row (linked node)
1406    ByRow { table: String, row_id: u64 },
1407}
1408
1409impl NodeSelector {
1410    /// Select by node ID
1411    pub fn by_id(id: &str) -> Self {
1412        Self::ById(id.to_string())
1413    }
1414
1415    /// Select by label string (preferred).
1416    pub fn by_label(label: impl Into<String>) -> Self {
1417        Self::ByType {
1418            node_label: label.into(),
1419            filter: None,
1420        }
1421    }
1422
1423    /// Select by table row
1424    pub fn by_row(table: &str, row_id: u64) -> Self {
1425        Self::ByRow {
1426            table: table.to_string(),
1427            row_id,
1428        }
1429    }
1430}
1431
1432// ============================================================================
1433// Vector Query
1434// ============================================================================
1435
1436/// Vector similarity search query
1437///
1438/// ```text
1439/// VECTOR SEARCH embeddings
1440/// SIMILAR TO [0.1, 0.2, ..., 0.5]
1441/// WHERE metadata.source = 'nmap'
1442/// LIMIT 10
1443/// ```
1444#[derive(Debug, Clone)]
1445pub struct VectorQuery {
1446    /// Optional outer alias when used as a join source
1447    pub alias: Option<String>,
1448    /// Collection name to search
1449    pub collection: String,
1450    /// Query vector (or reference to get vector from)
1451    pub query_vector: VectorSource,
1452    /// Number of results to return
1453    pub k: usize,
1454    /// Metadata filter
1455    pub filter: Option<MetadataFilter>,
1456    /// Distance metric to use (defaults to collection's metric)
1457    pub metric: Option<DistanceMetric>,
1458    /// Include vectors in results
1459    pub include_vectors: bool,
1460    /// Include metadata in results
1461    pub include_metadata: bool,
1462    /// Minimum similarity threshold (optional)
1463    pub threshold: Option<f32>,
1464}
1465
1466impl VectorQuery {
1467    /// Create a new vector query
1468    pub fn new(collection: &str, query: VectorSource) -> Self {
1469        Self {
1470            alias: None,
1471            collection: collection.to_string(),
1472            query_vector: query,
1473            k: 10,
1474            filter: None,
1475            metric: None,
1476            include_vectors: false,
1477            include_metadata: true,
1478            threshold: None,
1479        }
1480    }
1481
1482    /// Set the number of results
1483    pub fn limit(mut self, k: usize) -> Self {
1484        self.k = k;
1485        self
1486    }
1487
1488    /// Set metadata filter
1489    pub fn with_filter(mut self, filter: MetadataFilter) -> Self {
1490        self.filter = Some(filter);
1491        self
1492    }
1493
1494    /// Include vectors in results
1495    pub fn with_vectors(mut self) -> Self {
1496        self.include_vectors = true;
1497        self
1498    }
1499
1500    /// Set similarity threshold
1501    pub fn min_similarity(mut self, threshold: f32) -> Self {
1502        self.threshold = Some(threshold);
1503        self
1504    }
1505
1506    /// Set outer alias
1507    pub fn alias(mut self, alias: &str) -> Self {
1508        self.alias = Some(alias.to_string());
1509        self
1510    }
1511}
1512
1513/// Source of query vector
1514#[derive(Debug, Clone)]
1515pub enum VectorSource {
1516    /// Literal vector values
1517    Literal(Vec<f32>),
1518    /// Text to embed (requires embedding function)
1519    Text(String),
1520    /// Reference to another vector by ID
1521    Reference { collection: String, vector_id: u64 },
1522    /// From a subquery result
1523    Subquery(Box<QueryExpr>),
1524}
1525
1526impl VectorSource {
1527    /// Create from literal vector
1528    pub fn literal(values: Vec<f32>) -> Self {
1529        Self::Literal(values)
1530    }
1531
1532    /// Create from text (to be embedded)
1533    pub fn text(s: &str) -> Self {
1534        Self::Text(s.to_string())
1535    }
1536
1537    /// Reference another vector
1538    pub fn reference(collection: &str, vector_id: u64) -> Self {
1539        Self::Reference {
1540            collection: collection.to_string(),
1541            vector_id,
1542        }
1543    }
1544}
1545
1546// ============================================================================
1547// Hybrid Query
1548// ============================================================================
1549
1550/// Hybrid query combining structured (table/graph) and vector search
1551///
1552/// ```text
1553/// FROM hosts h
1554/// JOIN VECTOR embeddings e ON h.id = e.metadata.host_id
1555/// SIMILAR TO 'ssh vulnerability'
1556/// WHERE h.os = 'Linux'
1557/// RETURN h.*, e.distance
1558/// ```
1559#[derive(Debug, Clone)]
1560pub struct HybridQuery {
1561    /// Optional outer alias when used as a join source
1562    pub alias: Option<String>,
1563    /// Structured query part (table/graph)
1564    pub structured: Box<QueryExpr>,
1565    /// Vector search part
1566    pub vector: VectorQuery,
1567    /// How to combine results
1568    pub fusion: FusionStrategy,
1569    /// Final result limit
1570    pub limit: Option<usize>,
1571}
1572
1573impl HybridQuery {
1574    /// Create a new hybrid query
1575    pub fn new(structured: QueryExpr, vector: VectorQuery) -> Self {
1576        Self {
1577            alias: None,
1578            structured: Box::new(structured),
1579            vector,
1580            fusion: FusionStrategy::Rerank { weight: 0.5 },
1581            limit: None,
1582        }
1583    }
1584
1585    /// Set fusion strategy
1586    pub fn with_fusion(mut self, fusion: FusionStrategy) -> Self {
1587        self.fusion = fusion;
1588        self
1589    }
1590
1591    /// Set result limit
1592    pub fn limit(mut self, limit: usize) -> Self {
1593        self.limit = Some(limit);
1594        self
1595    }
1596
1597    /// Set outer alias
1598    pub fn alias(mut self, alias: &str) -> Self {
1599        self.alias = Some(alias.to_string());
1600        self
1601    }
1602}
1603
1604/// Strategy for combining structured and vector search results
1605#[derive(Debug, Clone)]
1606pub enum FusionStrategy {
1607    /// Vector similarity re-ranks structured results
1608    /// weight: 0.0 = pure structured, 1.0 = pure vector
1609    Rerank { weight: f32 },
1610    /// Filter with structured query, then search vectors among filtered
1611    FilterThenSearch,
1612    /// Search vectors first, then filter with structured query
1613    SearchThenFilter,
1614    /// Reciprocal Rank Fusion
1615    /// k: RRF constant (typically 60)
1616    RRF { k: u32 },
1617    /// Intersection: only return results that match both
1618    Intersection,
1619    /// Union: return results from either (with combined scores)
1620    Union {
1621        structured_weight: f32,
1622        vector_weight: f32,
1623    },
1624}
1625
1626impl Default for FusionStrategy {
1627    fn default() -> Self {
1628        Self::Rerank { weight: 0.5 }
1629    }
1630}
1631
1632// ============================================================================
1633// DML/DDL Query Types
1634// ============================================================================
1635
1636/// Entity type qualifier for INSERT statements
1637#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1638pub enum InsertEntityType {
1639    /// Default: plain row
1640    #[default]
1641    Row,
1642    /// INSERT INTO t NODE (...)
1643    Node,
1644    /// INSERT INTO t EDGE (...)
1645    Edge,
1646    /// INSERT INTO t VECTOR (...)
1647    Vector,
1648    /// INSERT INTO t DOCUMENT (...)
1649    Document,
1650    /// INSERT INTO t KV (...)
1651    Kv,
1652}
1653
1654/// Explicit item-kind qualifier for UPDATE statements.
1655#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1656pub enum UpdateTarget {
1657    /// Default: table/document/KV row-shaped items.
1658    #[default]
1659    Rows,
1660    /// UPDATE t DOCUMENTS SET ...
1661    Documents,
1662    /// UPDATE t KV SET ...
1663    Kv,
1664    /// UPDATE t NODES SET ...
1665    Nodes,
1666    /// UPDATE t EDGES SET ...
1667    Edges,
1668}
1669
1670/// An item in a RETURNING clause: either `*` (all columns) or a named column.
1671#[derive(Debug, Clone, PartialEq)]
1672pub enum ReturningItem {
1673    /// RETURNING *
1674    All,
1675    /// RETURNING col
1676    Column(String),
1677}
1678
1679/// INSERT INTO table (columns) VALUES (row1), (row2), ... [WITH TTL duration] [WITH METADATA (k=v)]
1680#[derive(Debug, Clone)]
1681pub struct InsertQuery {
1682    /// Target table name
1683    pub table: String,
1684    /// Entity type qualifier
1685    pub entity_type: InsertEntityType,
1686    /// Column names
1687    pub columns: Vec<String>,
1688    /// Canonical SQL rows of expressions.
1689    pub value_exprs: Vec<Vec<super::Expr>>,
1690    /// Rows of values (each inner Vec is one row)
1691    pub values: Vec<Vec<Value>>,
1692    /// Optional RETURNING clause items.
1693    pub returning: Option<Vec<ReturningItem>>,
1694    /// Optional TTL in milliseconds (from WITH TTL clause)
1695    pub ttl_ms: Option<u64>,
1696    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1697    pub expires_at_ms: Option<u64>,
1698    /// Optional metadata key-value pairs (from WITH METADATA clause)
1699    pub with_metadata: Vec<(String, Value)>,
1700    /// Auto-embed fields on insert (from WITH AUTO EMBED clause)
1701    pub auto_embed: Option<AutoEmbedConfig>,
1702    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1703    pub suppress_events: bool,
1704}
1705
1706/// Configuration for automatic embedding generation on INSERT.
1707#[derive(Debug, Clone)]
1708pub struct AutoEmbedConfig {
1709    /// Fields to extract text from for embedding
1710    pub fields: Vec<String>,
1711    /// AI provider (e.g. "openai")
1712    pub provider: String,
1713    /// Optional model override
1714    pub model: Option<String>,
1715}
1716
1717/// EVENTS BACKFILL collection [WHERE pred] TO queue [LIMIT n]
1718#[derive(Debug, Clone)]
1719pub struct EventsBackfillQuery {
1720    pub collection: String,
1721    pub where_filter: Option<String>,
1722    pub target_queue: String,
1723    pub limit: Option<u64>,
1724}
1725
1726/// UPDATE table SET col=val, ... WHERE filter [WITH TTL duration] [WITH METADATA (...)]
1727#[derive(Debug, Clone)]
1728pub struct UpdateQuery {
1729    /// Target table name
1730    pub table: String,
1731    /// Explicit item-kind target. Omitted targets default to rows.
1732    pub target: UpdateTarget,
1733    /// Canonical SQL assignments.
1734    pub assignment_exprs: Vec<(String, super::Expr)>,
1735    /// Per-assignment compound operator for `SET col += expr` forms.
1736    /// `None` means ordinary `SET col = expr`.
1737    pub compound_assignment_ops: Vec<Option<super::BinOp>>,
1738    /// Best-effort literal-only cache of assignments. Non-foldable expressions
1739    /// are preserved exclusively in `assignment_exprs` and evaluated later
1740    /// against the row pre-image by the runtime.
1741    pub assignments: Vec<(String, Value)>,
1742    /// Canonical SQL WHERE clause.
1743    pub where_expr: Option<super::Expr>,
1744    /// Optional WHERE filter
1745    pub filter: Option<Filter>,
1746    /// Optional TTL in milliseconds (from WITH TTL clause)
1747    pub ttl_ms: Option<u64>,
1748    /// Optional absolute expiration (from WITH EXPIRES AT clause)
1749    pub expires_at_ms: Option<u64>,
1750    /// Optional metadata key-value pairs (from WITH METADATA clause)
1751    pub with_metadata: Vec<(String, Value)>,
1752    /// Optional RETURNING clause items.
1753    pub returning: Option<Vec<ReturningItem>>,
1754    /// Optional deterministic target ordering for limited UPDATE batches.
1755    pub order_by: Vec<OrderByClause>,
1756    /// Optional `LIMIT N` cap. Caps the number of targets the executor
1757    /// will mutate in a single statement. Required by `BATCH N ROWS`
1758    /// data migrations (#37) which run the same UPDATE body in a
1759    /// loop, advancing a checkpoint between batches.
1760    pub limit: Option<u64>,
1761    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1762    pub suppress_events: bool,
1763}
1764
1765/// DELETE FROM table WHERE filter
1766#[derive(Debug, Clone)]
1767pub struct DeleteQuery {
1768    /// Target table name
1769    pub table: String,
1770    /// Canonical SQL WHERE clause.
1771    pub where_expr: Option<super::Expr>,
1772    /// Optional WHERE filter
1773    pub filter: Option<Filter>,
1774    /// Optional RETURNING clause items.
1775    pub returning: Option<Vec<ReturningItem>>,
1776    /// Skip event subscription emission for this statement (SUPPRESS EVENTS).
1777    pub suppress_events: bool,
1778}
1779
1780/// CREATE TABLE name (columns) or CREATE {KV|CONFIG|VAULT} name
1781#[derive(Debug, Clone)]
1782pub struct CreateTableQuery {
1783    /// Declared collection model. Defaults to Table for CREATE TABLE.
1784    pub collection_model: CollectionModel,
1785    /// Table name
1786    pub name: String,
1787    /// Column definitions
1788    pub columns: Vec<CreateColumnDef>,
1789    /// IF NOT EXISTS flag
1790    pub if_not_exists: bool,
1791    /// Optional default TTL applied to newly inserted items in this collection.
1792    pub default_ttl_ms: Option<u64>,
1793    /// Metrics rollup tiers declared by `CREATE METRICS ... DOWNSAMPLE`.
1794    /// Uses the existing time-series policy spelling: target:source:aggregation.
1795    pub metrics_rollup_policies: Vec<String>,
1796    /// Fields to prioritize in the context index (WITH CONTEXT INDEX ON (f1, f2))
1797    pub context_index_fields: Vec<String>,
1798    /// Enables the global context index for this table
1799    /// (`WITH context_index = true`). Default false — pure OLTP tables
1800    /// skip the tokenisation / 3-way RwLock write storm on every insert.
1801    /// Having `context_index_fields` non-empty also enables it implicitly.
1802    pub context_index_enabled: bool,
1803    /// When true, CREATE TABLE implicitly adds two user-visible columns
1804    /// `created_at` and `updated_at` (BIGINT unix-ms). The runtime
1805    /// populates them from `UnifiedEntity::created_at/updated_at` on
1806    /// every write; `created_at` is immutable after insert.
1807    /// Enabled via `WITH timestamps = true` in the DDL.
1808    pub timestamps: bool,
1809    /// Partitioning spec (Phase 2.2 PG parity).
1810    ///
1811    /// When present the table is the *parent* of a partition tree — every
1812    /// child partition is registered via `ALTER TABLE ... ATTACH PARTITION`.
1813    /// Phase 2.2 stops at registry-only: queries against a partitioned
1814    /// parent don't auto-rewrite as UNION yet (Phase 4 adds pruning).
1815    pub partition_by: Option<PartitionSpec>,
1816    /// Table-scoped multi-tenancy declaration (Phase 2.5.4).
1817    ///
1818    /// Syntax: `CREATE TABLE t (...) WITH (tenant_by = 'col_name')` or
1819    /// the shorthand `CREATE TABLE t (...) TENANT BY (col_name)`. The
1820    /// runtime treats the named column as the tenant discriminator and
1821    /// automatically:
1822    ///
1823    /// 1. Registers the table → column mapping so INSERTs that omit the
1824    ///    column get `CURRENT_TENANT()` auto-filled.
1825    /// 2. Installs an implicit RLS policy equivalent to
1826    ///    `USING (col = CURRENT_TENANT())` for SELECT/UPDATE/DELETE/INSERT.
1827    /// 3. Flips `rls_enabled_tables` on so the policy actually applies.
1828    ///
1829    /// None leaves the table non-tenant-scoped — callers manage tenancy
1830    /// manually via explicit CREATE POLICY if they want it.
1831    pub tenant_by: Option<String>,
1832    /// When true, UPDATE and DELETE on this table are rejected at
1833    /// parse time. Corresponds to `CREATE TABLE ... APPEND ONLY` or
1834    /// `WITH (append_only = true)`. Default false (mutable).
1835    pub append_only: bool,
1836    /// Declarative event subscriptions for this table. #291 stores
1837    /// metadata only; event emission is intentionally out of scope.
1838    pub subscriptions: Vec<crate::catalog::SubscriptionDescriptor>,
1839    /// `CREATE VAULT ... WITH OWN MASTER KEY`: provision per-vault
1840    /// key material instead of using the cluster vault key.
1841    pub vault_own_master_key: bool,
1842}
1843
1844/// CREATE COLLECTION name KIND kind [SIGNED_BY ('pubkey_hex', ...)]
1845#[derive(Debug, Clone)]
1846pub struct CreateCollectionQuery {
1847    pub name: String,
1848    pub kind: String,
1849    pub if_not_exists: bool,
1850    pub vector_dimension: Option<usize>,
1851    pub vector_metric: Option<DistanceMetric>,
1852    /// Initial Ed25519 allowed-signer registry. Empty = unsigned collection.
1853    /// Each entry is a 32-byte Ed25519 public key. Mutable post-create via
1854    /// `ALTER COLLECTION ... ADD|REVOKE SIGNER` (see issue #520).
1855    pub allowed_signers: Vec<[u8; 32]>,
1856}
1857
1858/// CREATE VECTOR name DIM n [METRIC metric]
1859#[derive(Debug, Clone)]
1860pub struct CreateVectorQuery {
1861    pub name: String,
1862    pub dimension: usize,
1863    pub metric: DistanceMetric,
1864    pub if_not_exists: bool,
1865}
1866
1867/// `PARTITION BY RANGE|LIST|HASH (column)` clause.
1868#[derive(Debug, Clone, PartialEq, Eq)]
1869pub struct PartitionSpec {
1870    pub kind: PartitionKind,
1871    /// Partition key column(s). Simple single-column for Phase 2.2.
1872    pub column: String,
1873}
1874
1875#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1876pub enum PartitionKind {
1877    /// `PARTITION BY RANGE(col)` — children bind `FOR VALUES FROM (a) TO (b)`.
1878    Range,
1879    /// `PARTITION BY LIST(col)` — children bind `FOR VALUES IN (v1, v2, ...)`.
1880    List,
1881    /// `PARTITION BY HASH(col)` — children bind `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
1882    Hash,
1883}
1884
1885/// Column definition for CREATE TABLE
1886#[derive(Debug, Clone)]
1887pub struct CreateColumnDef {
1888    /// Column name
1889    pub name: String,
1890    /// Legacy declared type string preserved for the runtime/storage pipeline.
1891    pub data_type: String,
1892    /// Structured SQL type used by the semantic layer.
1893    pub sql_type: SqlTypeName,
1894    /// NOT NULL constraint
1895    pub not_null: bool,
1896    /// DEFAULT value expression
1897    pub default: Option<String>,
1898    /// Compression level (COMPRESS:N)
1899    pub compress: Option<u8>,
1900    /// UNIQUE constraint
1901    pub unique: bool,
1902    /// PRIMARY KEY constraint
1903    pub primary_key: bool,
1904    /// Enum variant names (for ENUM type)
1905    pub enum_variants: Vec<String>,
1906    /// Array element type (for ARRAY type)
1907    pub array_element: Option<String>,
1908    /// Decimal precision (for DECIMAL type)
1909    pub decimal_precision: Option<u8>,
1910}
1911
1912/// DROP TABLE name
1913#[derive(Debug, Clone)]
1914pub struct DropTableQuery {
1915    /// Table name
1916    pub name: String,
1917    /// IF EXISTS flag
1918    pub if_exists: bool,
1919}
1920
1921/// DROP GRAPH [IF EXISTS] name
1922#[derive(Debug, Clone)]
1923pub struct DropGraphQuery {
1924    pub name: String,
1925    pub if_exists: bool,
1926}
1927
1928/// DROP VECTOR [IF EXISTS] name
1929#[derive(Debug, Clone)]
1930pub struct DropVectorQuery {
1931    pub name: String,
1932    pub if_exists: bool,
1933}
1934
1935/// DROP DOCUMENT [IF EXISTS] name
1936#[derive(Debug, Clone)]
1937pub struct DropDocumentQuery {
1938    pub name: String,
1939    pub if_exists: bool,
1940}
1941
1942/// DROP {KV|CONFIG|VAULT} [IF EXISTS] name
1943#[derive(Debug, Clone)]
1944pub struct DropKvQuery {
1945    pub name: String,
1946    pub if_exists: bool,
1947    pub model: CollectionModel,
1948}
1949
1950/// DROP COLLECTION [IF EXISTS] name
1951#[derive(Debug, Clone)]
1952pub struct DropCollectionQuery {
1953    pub name: String,
1954    pub if_exists: bool,
1955    pub model: Option<CollectionModel>,
1956}
1957
1958/// TRUNCATE {TABLE|GRAPH|VECTOR|DOCUMENT|TIMESERIES|KV|QUEUE|COLLECTION} [IF EXISTS] name
1959#[derive(Debug, Clone)]
1960pub struct TruncateQuery {
1961    pub name: String,
1962    pub model: Option<CollectionModel>,
1963    pub if_exists: bool,
1964}
1965
1966/// ALTER TABLE name operations
1967#[derive(Debug, Clone)]
1968pub struct AlterTableQuery {
1969    /// Table name
1970    pub name: String,
1971    /// Alter operations
1972    pub operations: Vec<AlterOperation>,
1973}
1974
1975/// Single ALTER TABLE operation
1976#[derive(Debug, Clone)]
1977pub enum AlterOperation {
1978    /// ADD COLUMN definition
1979    AddColumn(CreateColumnDef),
1980    /// DROP COLUMN name
1981    DropColumn(String),
1982    /// RENAME COLUMN from TO to
1983    RenameColumn { from: String, to: String },
1984    /// `ATTACH PARTITION child FOR VALUES ...` (Phase 2.2 PG parity).
1985    ///
1986    /// Binds an existing child table to the parent partitioned table.
1987    /// The `bound` string captures the raw bound expression so the
1988    /// runtime can round-trip it back into `red_config` without a
1989    /// dedicated per-kind AST.
1990    AttachPartition {
1991        child: String,
1992        /// Human-readable bound string, e.g. `FROM (2024-01-01) TO (2025-01-01)`
1993        /// or `IN (1, 2, 3)` or `WITH (MODULUS 4, REMAINDER 0)`.
1994        bound: String,
1995    },
1996    /// `DETACH PARTITION child`
1997    DetachPartition { child: String },
1998    /// `ENABLE ROW LEVEL SECURITY` (Phase 2.5 PG parity).
1999    ///
2000    /// Flips the table into RLS-enforced mode. Reads against the table
2001    /// will be filtered by every matching `CREATE POLICY` (for the
2002    /// current role) combined with `AND`.
2003    EnableRowLevelSecurity,
2004    /// `DISABLE ROW LEVEL SECURITY` — disables enforcement; policies
2005    /// remain defined but are ignored until re-enabled.
2006    DisableRowLevelSecurity,
2007    /// `ENABLE TENANCY ON (col)` (Phase 2.5.4 PG parity-ish).
2008    ///
2009    /// Retrofit a tenant-scoped declaration onto an existing table —
2010    /// registers the column, installs the auto `__tenant_iso` RLS
2011    /// policy, and flips RLS on. Equivalent to re-running
2012    /// `CREATE TABLE ... TENANT BY (col)` minus the schema creation.
2013    EnableTenancy { column: String },
2014    /// `DISABLE TENANCY` — tears down the auto-policy and clears the
2015    /// tenancy registration. User-defined policies on the table are
2016    /// untouched; RLS stays enabled if any survive.
2017    DisableTenancy,
2018    /// `SET APPEND_ONLY = true|false` — flips the catalog flag.
2019    /// Setting `true` rejects all future UPDATE/DELETE at parse-time
2020    /// guard; setting `false` re-enables them. Existing rows are
2021    /// untouched either way — this is a purely declarative switch.
2022    SetAppendOnly(bool),
2023    /// `SET VERSIONED = true|false` — opt the table into (or out of)
2024    /// Git-for-Data. Enables merge / diff / AS OF semantics against
2025    /// this collection. Works retroactively: previously-created
2026    /// rows become part of the history accessible via AS OF as long
2027    /// as their xmin is still pinned by an existing commit.
2028    SetVersioned(bool),
2029    /// `ENABLE EVENTS ...` — install or re-enable table event subscription metadata.
2030    EnableEvents(crate::catalog::SubscriptionDescriptor),
2031    /// `DISABLE EVENTS` — mark all table event subscriptions disabled.
2032    DisableEvents,
2033    /// `ADD SUBSCRIPTION name TO queue [REDACT (...)] [WHERE ...]` — add a named subscription.
2034    AddSubscription {
2035        name: String,
2036        descriptor: crate::catalog::SubscriptionDescriptor,
2037    },
2038    /// `DROP SUBSCRIPTION name` — remove a named subscription by name.
2039    DropSubscription { name: String },
2040    /// Issue #522 — `ALTER COLLECTION name ADD SIGNER 'hex_pubkey'`.
2041    /// Appends the key to the per-collection signer registry and
2042    /// records an `Add` entry on the `signer_history` audit log.
2043    AddSigner { pubkey: [u8; 32] },
2044    /// Issue #522 — `ALTER COLLECTION name REVOKE SIGNER 'hex_pubkey'`.
2045    /// Removes the key from the *currently allowed* set and records a
2046    /// `Revoke` entry. Past rows signed by `pubkey` remain readable
2047    /// and re-verifiable — only future inserts are rejected.
2048    RevokeSigner { pubkey: [u8; 32] },
2049    /// Issue #580 — `ALTER COLLECTION name SET RETENTION <duration>`.
2050    /// Stores a declarative retention policy on the collection contract.
2051    /// Enforcement is lazy-on-scan: reads silently filter out rows older
2052    /// than `now - duration_ms` by the collection's timestamp column.
2053    SetRetention { duration_ms: u64 },
2054    /// Issue #580 — `ALTER COLLECTION name UNSET RETENTION`.
2055    /// Removes the policy. Previously-hidden expired rows become
2056    /// readable again — the slice never physically dropped them.
2057    UnsetRetention,
2058}
2059
2060// ============================================================================
2061// Shared Types
2062// ============================================================================
2063
2064/// Column/field projection
2065#[derive(Debug, Clone, PartialEq)]
2066pub enum Projection {
2067    /// Select all columns (*)
2068    All,
2069    /// Single column by name
2070    Column(String),
2071    /// Column with alias
2072    Alias(String, String),
2073    /// Function call (name, args)
2074    Function(String, Vec<Projection>),
2075    /// Expression with optional alias
2076    Expression(Box<Filter>, Option<String>),
2077    /// Field reference (for graph properties)
2078    Field(FieldRef, Option<String>),
2079    /// Window function call: `fn(args) OVER (PARTITION BY ... ORDER BY ...
2080    /// [frame])`. Carries the window specification as a sibling so the
2081    /// planner can lower it without re-parsing. No runtime in slice 7a —
2082    /// the analytics executor lands in a subsequent slice (issue #589
2083    /// follow-ups). See `super::WindowSpec`.
2084    Window {
2085        name: String,
2086        args: Vec<Projection>,
2087        window: Box<super::WindowSpec>,
2088        alias: Option<String>,
2089    },
2090}
2091
2092impl Projection {
2093    /// Create a projection from a field reference
2094    pub fn from_field(field: FieldRef) -> Self {
2095        Projection::Field(field, None)
2096    }
2097
2098    /// Create a column projection
2099    pub fn column(name: &str) -> Self {
2100        Projection::Column(name.to_string())
2101    }
2102
2103    /// Create an aliased projection
2104    pub fn with_alias(column: &str, alias: &str) -> Self {
2105        Projection::Alias(column.to_string(), alias.to_string())
2106    }
2107}
2108
2109/// Filter condition
2110#[derive(Debug, Clone, PartialEq)]
2111pub enum Filter {
2112    /// Comparison: field op value
2113    Compare {
2114        field: FieldRef,
2115        op: CompareOp,
2116        value: Value,
2117    },
2118    /// Field-to-field comparison: left.field op right.field. Used when
2119    /// WHERE / BETWEEN operands reference another column instead of a
2120    /// literal — the pre-Fase-2-parser-v2 shim for column-to-column
2121    /// predicates. Once the Expr-rewrite lands, this collapses into
2122    /// `Compare { left: Expr, op, right: Expr }`.
2123    CompareFields {
2124        left: FieldRef,
2125        op: CompareOp,
2126        right: FieldRef,
2127    },
2128    /// Expression-to-expression comparison: `lhs op rhs` where either
2129    /// side may be an arbitrary `Expr` tree (function call, CAST,
2130    /// arithmetic, nested CASE). This is the most general compare
2131    /// variant — `Compare` and `CompareFields` stay as fast-path
2132    /// specialisations because the planner / cost model / index
2133    /// selector all pattern-match on the simpler shapes. The parser
2134    /// only emits this variant when a simpler one cannot express the
2135    /// predicate.
2136    CompareExpr {
2137        lhs: super::Expr,
2138        op: CompareOp,
2139        rhs: super::Expr,
2140    },
2141    /// Logical AND
2142    And(Box<Filter>, Box<Filter>),
2143    /// Logical OR
2144    Or(Box<Filter>, Box<Filter>),
2145    /// Logical NOT
2146    Not(Box<Filter>),
2147    /// IS NULL
2148    IsNull(FieldRef),
2149    /// IS NOT NULL
2150    IsNotNull(FieldRef),
2151    /// IN (value1, value2, ...)
2152    In { field: FieldRef, values: Vec<Value> },
2153    /// BETWEEN low AND high
2154    Between {
2155        field: FieldRef,
2156        low: Value,
2157        high: Value,
2158    },
2159    /// LIKE pattern
2160    Like { field: FieldRef, pattern: String },
2161    /// STARTS WITH prefix
2162    StartsWith { field: FieldRef, prefix: String },
2163    /// ENDS WITH suffix
2164    EndsWith { field: FieldRef, suffix: String },
2165    /// CONTAINS substring
2166    Contains { field: FieldRef, substring: String },
2167}
2168
2169impl Filter {
2170    /// Create a comparison filter
2171    pub fn compare(field: FieldRef, op: CompareOp, value: Value) -> Self {
2172        Self::Compare { field, op, value }
2173    }
2174
2175    /// Combine with AND
2176    pub fn and(self, other: Filter) -> Self {
2177        Self::And(Box::new(self), Box::new(other))
2178    }
2179
2180    /// Combine with OR
2181    pub fn or(self, other: Filter) -> Self {
2182        Self::Or(Box::new(self), Box::new(other))
2183    }
2184
2185    /// Negate
2186    pub fn not(self) -> Self {
2187        Self::Not(Box::new(self))
2188    }
2189
2190    /// Bottom-up AST rewrites: OR-of-equalities → IN, AND/OR flatten.
2191    /// Inspired by MongoDB's `MatchExpression::optimize()`.
2192    /// Call on the result of `effective_table_filter()` before evaluation.
2193    pub fn optimize(self) -> Self {
2194        crate::storage::query::filter_optimizer::optimize(self)
2195    }
2196}
2197
2198/// Comparison operator
2199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2200pub enum CompareOp {
2201    /// Equal (=)
2202    Eq,
2203    /// Not equal (<> or !=)
2204    Ne,
2205    /// Less than (<)
2206    Lt,
2207    /// Less than or equal (<=)
2208    Le,
2209    /// Greater than (>)
2210    Gt,
2211    /// Greater than or equal (>=)
2212    Ge,
2213}
2214
2215impl fmt::Display for CompareOp {
2216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2217        match self {
2218            CompareOp::Eq => write!(f, "="),
2219            CompareOp::Ne => write!(f, "<>"),
2220            CompareOp::Lt => write!(f, "<"),
2221            CompareOp::Le => write!(f, "<="),
2222            CompareOp::Gt => write!(f, ">"),
2223            CompareOp::Ge => write!(f, ">="),
2224        }
2225    }
2226}
2227
2228/// Order by clause.
2229///
2230/// Fase 2 migration: `field` is the legacy bare column reference and
2231/// remains populated for back-compat with existing callers (SPARQL /
2232/// Gremlin / Cypher translators, the planner cost model, etc.). The
2233/// new `expr` slot carries an arbitrary `Expr` tree — when present,
2234/// runtime comparators prefer it over `field`, so the parser can
2235/// emit `ORDER BY CAST(a AS INT)`, `ORDER BY a + b * 2`, etc. without
2236/// breaking the rest of the codebase.
2237///
2238/// When `expr` is `None`, the clause behaves exactly like before.
2239/// When `expr` is `Some(Expr::Column(f))`, runtime code may still use
2240/// the legacy path — it's equivalent. Constructors default `expr` to
2241/// `None` so all existing call sites stay source-compatible.
2242#[derive(Debug, Clone)]
2243pub struct OrderByClause {
2244    /// Field to order by. Left populated even when `expr` is set so
2245    /// legacy consumers (planner cardinality estimate, cost model,
2246    /// mode translators) that still pattern-match on `field` keep
2247    /// working during the Fase 2 migration.
2248    pub field: FieldRef,
2249    /// Fase 2 expression-aware sort key. When `Some`, runtime order
2250    /// comparators evaluate this expression per row and sort on the
2251    /// resulting values — unlocks `ORDER BY expr` (Fase 1.6).
2252    pub expr: Option<super::Expr>,
2253    /// Ascending or descending
2254    pub ascending: bool,
2255    /// Nulls first or last
2256    pub nulls_first: bool,
2257}
2258
2259impl OrderByClause {
2260    /// Create ascending order
2261    pub fn asc(field: FieldRef) -> Self {
2262        Self {
2263            field,
2264            expr: None,
2265            ascending: true,
2266            nulls_first: false,
2267        }
2268    }
2269
2270    /// Create descending order
2271    pub fn desc(field: FieldRef) -> Self {
2272        Self {
2273            field,
2274            expr: None,
2275            ascending: false,
2276            nulls_first: true,
2277        }
2278    }
2279
2280    /// Attach an `Expr` sort key to an existing clause. Leaves `field`
2281    /// untouched so back-compat match sites keep their pattern.
2282    pub fn with_expr(mut self, expr: super::Expr) -> Self {
2283        self.expr = Some(expr);
2284        self
2285    }
2286}
2287
2288// ============================================================================
2289// Window OVER clause (issue #589 slice 7a)
2290// ============================================================================
2291//
2292// Syntactic representation of `OVER (PARTITION BY ... ORDER BY ... [frame])`.
2293// Slice 7a: AST + parser only. No runtime / executor wiring.
2294
2295/// Frame unit: `ROWS` (physical row offset) or `RANGE` (logical value
2296/// offset). Slice 7a stores the choice but does not yet differentiate
2297/// at runtime — semantics arrive with the analytics executor.
2298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2299pub enum WindowFrameUnit {
2300    Rows,
2301    Range,
2302}
2303
2304/// One endpoint of a frame: UNBOUNDED PRECEDING / CURRENT ROW /
2305/// PRECEDING(expr) / FOLLOWING(expr) / UNBOUNDED FOLLOWING.
2306#[derive(Debug, Clone, PartialEq)]
2307pub enum WindowFrameBound {
2308    UnboundedPreceding,
2309    UnboundedFollowing,
2310    CurrentRow,
2311    Preceding(Box<super::Expr>),
2312    Following(Box<super::Expr>),
2313}
2314
2315/// `ROWS|RANGE BETWEEN start AND end` — or the single-bound shorthand
2316/// `ROWS start` (end implied as CURRENT ROW per SQL standard). Slice 7a
2317/// represents both shapes uniformly with `end: Option<...>` so downstream
2318/// code can normalise.
2319#[derive(Debug, Clone, PartialEq)]
2320pub struct WindowFrame {
2321    pub unit: WindowFrameUnit,
2322    pub start: WindowFrameBound,
2323    pub end: Option<WindowFrameBound>,
2324}
2325
2326/// One ORDER BY item inside a window spec. Window order keys are
2327/// expression-based by SQL standard, so we carry an `Expr` directly
2328/// rather than reusing the top-level `OrderByClause` (which still has
2329/// a legacy `FieldRef` slot for the Fase 2 migration).
2330#[derive(Debug, Clone, PartialEq)]
2331pub struct WindowOrderItem {
2332    pub expr: super::Expr,
2333    pub ascending: bool,
2334    pub nulls_first: bool,
2335}
2336
2337/// Full window specification — the AST node behind `OVER (...)`.
2338/// `frame` is `None` when the user did not specify a frame clause; the
2339/// analytics executor will materialise the SQL default (RANGE UNBOUNDED
2340/// PRECEDING AND CURRENT ROW when ORDER BY is present, the full
2341/// partition otherwise) once it lands.
2342#[derive(Debug, Clone, PartialEq, Default)]
2343pub struct WindowSpec {
2344    pub partition_by: Vec<super::Expr>,
2345    pub order_by: Vec<WindowOrderItem>,
2346    pub frame: Option<WindowFrame>,
2347}
2348
2349// ============================================================================
2350// Graph Commands
2351// ============================================================================
2352
2353/// Graph analytics command issued via SQL-like syntax
2354#[derive(Debug, Clone)]
2355pub struct GraphCommandOrderBy {
2356    pub metric: String,
2357    pub ascending: bool,
2358}
2359
2360#[derive(Debug, Clone)]
2361pub enum GraphCommand {
2362    /// GRAPH NEIGHBORHOOD 'source' [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2363    Neighborhood {
2364        source: String,
2365        depth: u32,
2366        direction: String,
2367        edge_labels: Option<Vec<String>>,
2368    },
2369    /// GRAPH SHORTEST_PATH 'source' TO 'target' [ALGORITHM alg] [DIRECTION dir] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2370    ShortestPath {
2371        source: String,
2372        target: String,
2373        algorithm: String,
2374        direction: String,
2375        limit: Option<u32>,
2376        order_by: Option<GraphCommandOrderBy>,
2377    },
2378    /// GRAPH TRAVERSE 'source' [STRATEGY bfs|dfs] [DEPTH n] [DIRECTION dir] [EDGES IN ('label', ...)]
2379    Traverse {
2380        source: String,
2381        strategy: String,
2382        depth: u32,
2383        direction: String,
2384        edge_labels: Option<Vec<String>>,
2385    },
2386    /// GRAPH CENTRALITY [ALGORITHM alg] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2387    ///
2388    /// `limit = None` keeps the historical implicit top-100 cap. `Some(n)`
2389    /// caps the returned rows at `n`.
2390    Centrality {
2391        algorithm: String,
2392        limit: Option<u32>,
2393        order_by: Option<GraphCommandOrderBy>,
2394    },
2395    /// GRAPH COMMUNITY [ALGORITHM alg] [MAX_ITERATIONS n] [ORDER BY metric [ASC|DESC]] [LIMIT n] [RETURN ASSIGNMENTS]
2396    ///
2397    /// `return_assignments = false` (default) keeps the historical per-community
2398    /// aggregate shape (`community_id`, `size`). `true` emits one row per node
2399    /// (`node_id`, `community_id`) — the node→community map (#660).
2400    Community {
2401        algorithm: String,
2402        max_iterations: u32,
2403        limit: Option<u32>,
2404        order_by: Option<GraphCommandOrderBy>,
2405        return_assignments: bool,
2406    },
2407    /// GRAPH COMPONENTS [MODE connected|weak|strong] [ORDER BY metric [ASC|DESC]] [LIMIT n]
2408    Components {
2409        mode: String,
2410        limit: Option<u32>,
2411        order_by: Option<GraphCommandOrderBy>,
2412    },
2413    /// GRAPH CYCLES [MAX_LENGTH n]
2414    Cycles { max_length: u32 },
2415    /// GRAPH CLUSTERING
2416    Clustering,
2417    /// GRAPH TOPOLOGICAL_SORT
2418    TopologicalSort,
2419    /// GRAPH PROPERTIES ['<id-or-label>']
2420    ///
2421    /// `source = None` returns graph-wide stats. `source = Some("...")` returns
2422    /// the full property bag of a specific node, resolved via the same label
2423    /// index as `GRAPH NEIGHBORHOOD` / `GRAPH TRAVERSE` (issue #416).
2424    Properties { source: Option<String> },
2425}
2426
2427// ============================================================================
2428// Search Commands
2429// ============================================================================
2430
2431/// Search command issued via SQL-like syntax
2432#[derive(Debug, Clone)]
2433pub enum SearchCommand {
2434    /// SEARCH SIMILAR [v1, v2, ...] | $N | TEXT 'query' [COLLECTION col] [LIMIT n] [MIN_SCORE f] [USING provider]
2435    Similar {
2436        vector: Vec<f32>,
2437        text: Option<String>,
2438        provider: Option<String>,
2439        collection: String,
2440        limit: usize,
2441        min_score: f32,
2442        /// `$N` placeholder for the vector slot. `Some(idx)` when the SQL
2443        /// used `SEARCH SIMILAR $N ...`; the binder substitutes the
2444        /// user-supplied `Value::Vector` and clears this back to `None`.
2445        /// Runtime executors assert this is `None` post-bind.
2446        vector_param: Option<usize>,
2447        /// `$N` placeholder for the `LIMIT` slot (issue #361). The binder
2448        /// substitutes the user-supplied positive integer into `limit`
2449        /// and clears this back to `None`.
2450        limit_param: Option<usize>,
2451        /// `$N` placeholder for the `MIN_SCORE` slot (issue #361). The
2452        /// binder substitutes the user-supplied float into `min_score`
2453        /// and clears this back to `None`.
2454        min_score_param: Option<usize>,
2455        /// `$N` placeholder for `SEARCH SIMILAR TEXT $N` (issue #361).
2456        /// Binder substitutes the user-supplied text into `text` and
2457        /// clears this back to `None`.
2458        text_param: Option<usize>,
2459    },
2460    /// SEARCH TEXT 'query' [COLLECTION col] [LIMIT n] [FUZZY]
2461    Text {
2462        query: String,
2463        collection: Option<String>,
2464        limit: usize,
2465        fuzzy: bool,
2466        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2467        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2468        /// substitutes the user-supplied positive integer into `limit`
2469        /// and clears this back to `None`.
2470        limit_param: Option<usize>,
2471    },
2472    /// SEARCH HYBRID [vector] [TEXT 'query'] COLLECTION col [LIMIT n]
2473    Hybrid {
2474        vector: Option<Vec<f32>>,
2475        query: Option<String>,
2476        collection: String,
2477        limit: usize,
2478        /// `$N` placeholder for the `LIMIT` / `K` slot (issue #361).
2479        /// Same shape as `SearchCommand::Similar::limit_param`; the
2480        /// binder substitutes the user-supplied positive integer and
2481        /// clears this back to `None`.
2482        limit_param: Option<usize>,
2483    },
2484    /// SEARCH MULTIMODAL 'key_or_query' [COLLECTION col] [LIMIT n]
2485    Multimodal {
2486        query: String,
2487        collection: Option<String>,
2488        limit: usize,
2489        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2490        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2491        /// substitutes the user-supplied positive integer into `limit`
2492        /// and clears this back to `None`.
2493        limit_param: Option<usize>,
2494    },
2495    /// SEARCH INDEX index VALUE 'value' [COLLECTION col] [LIMIT n] [EXACT]
2496    Index {
2497        index: String,
2498        value: String,
2499        collection: Option<String>,
2500        limit: usize,
2501        exact: bool,
2502        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2503        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2504        /// substitutes the user-supplied positive integer into `limit`
2505        /// and clears this back to `None`.
2506        limit_param: Option<usize>,
2507    },
2508    /// SEARCH CONTEXT 'query' [FIELD field] [COLLECTION col] [LIMIT n] [DEPTH n]
2509    Context {
2510        query: String,
2511        field: Option<String>,
2512        collection: Option<String>,
2513        limit: usize,
2514        depth: usize,
2515        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2516        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2517        /// substitutes the user-supplied positive integer into `limit`
2518        /// and clears this back to `None`.
2519        limit_param: Option<usize>,
2520    },
2521    /// SEARCH SPATIAL RADIUS lat lon radius_km COLLECTION col COLUMN col [LIMIT n]
2522    SpatialRadius {
2523        center_lat: f64,
2524        center_lon: f64,
2525        radius_km: f64,
2526        collection: String,
2527        column: String,
2528        limit: usize,
2529        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2530        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2531        /// substitutes the user-supplied positive integer into `limit`
2532        /// and clears this back to `None`.
2533        limit_param: Option<usize>,
2534    },
2535    /// SEARCH SPATIAL BBOX min_lat min_lon max_lat max_lon COLLECTION col COLUMN col [LIMIT n]
2536    SpatialBbox {
2537        min_lat: f64,
2538        min_lon: f64,
2539        max_lat: f64,
2540        max_lon: f64,
2541        collection: String,
2542        column: String,
2543        limit: usize,
2544        /// `$N` placeholder for the `LIMIT` slot (issue #361). Same
2545        /// shape as `SearchCommand::Hybrid::limit_param`; the binder
2546        /// substitutes the user-supplied positive integer into `limit`
2547        /// and clears this back to `None`.
2548        limit_param: Option<usize>,
2549    },
2550    /// SEARCH SPATIAL NEAREST lat lon K n COLLECTION col COLUMN col
2551    SpatialNearest {
2552        lat: f64,
2553        lon: f64,
2554        k: usize,
2555        collection: String,
2556        column: String,
2557        /// `$N` placeholder for the `K` slot (issue #361). Same shape
2558        /// as `SearchCommand::Hybrid::limit_param`; the binder
2559        /// substitutes the user-supplied positive integer into `k`
2560        /// and clears this back to `None`.
2561        k_param: Option<usize>,
2562    },
2563}
2564
2565// ============================================================================
2566// Time-Series DDL
2567// ============================================================================
2568
2569/// CREATE TIMESERIES name [RETENTION duration] [CHUNK_SIZE n] [DOWNSAMPLE spec[, spec...]]
2570///
2571/// `CREATE HYPERTABLE` lands on the same AST with `hypertable` populated.
2572/// The TimescaleDB-style syntax (time column + chunk_interval) gives the
2573/// runtime enough to register a `HypertableSpec` alongside the
2574/// underlying collection contract, so chunk routing and TTL sweeps can
2575/// address the table without a separate DDL.
2576#[derive(Debug, Clone)]
2577pub struct CreateTimeSeriesQuery {
2578    pub name: String,
2579    pub retention_ms: Option<u64>,
2580    pub chunk_size: Option<usize>,
2581    pub downsample_policies: Vec<String>,
2582    pub if_not_exists: bool,
2583    /// When `Some`, the DDL was spelled `CREATE HYPERTABLE` and the
2584    /// runtime must register the spec with the hypertable registry.
2585    pub hypertable: Option<HypertableDdl>,
2586    /// `WITH SESSION_KEY <col>` — default partition column for the
2587    /// `SESSIONIZE` operator. Persisted on the collection contract so
2588    /// queries that omit `BY <col>` pick it up. Issue #576 slice 1.
2589    pub session_key: Option<String>,
2590    /// `SESSION_GAP <duration>` — default inactivity gap (ms) for the
2591    /// `SESSIONIZE` operator. Issue #576 slice 1.
2592    pub session_gap_ms: Option<u64>,
2593}
2594
2595/// Hypertable-specific DDL fields — set only when the caller used
2596/// `CREATE HYPERTABLE`.
2597#[derive(Debug, Clone)]
2598pub struct HypertableDdl {
2599    /// Column that carries the nanosecond timestamp axis.
2600    pub time_column: String,
2601    /// Chunk width in nanoseconds.
2602    pub chunk_interval_ns: u64,
2603    /// Per-chunk default TTL in nanoseconds (`None` = no TTL).
2604    pub default_ttl_ns: Option<u64>,
2605}
2606
2607/// DROP TIMESERIES [IF EXISTS] name
2608#[derive(Debug, Clone)]
2609pub struct DropTimeSeriesQuery {
2610    pub name: String,
2611    pub if_exists: bool,
2612}
2613
2614// ============================================================================
2615// Queue DDL & Commands
2616// ============================================================================
2617
2618/// Default `MAX_ATTEMPTS` for `CREATE QUEUE` when omitted.
2619pub const DEFAULT_QUEUE_MAX_ATTEMPTS: u32 = 3;
2620/// Default `LOCK_DEADLINE_MS` for `CREATE QUEUE` when omitted.
2621pub const DEFAULT_QUEUE_LOCK_DEADLINE_MS: u64 = 30_000;
2622/// Default `IN_FLIGHT_CAP_PER_GROUP` for `CREATE QUEUE` when omitted.
2623pub const DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP: u32 = 10_000;
2624
2625/// CREATE QUEUE name [MAX_SIZE n] [PRIORITY] [WITH TTL duration] [WITH DLQ name]
2626/// [MAX_ATTEMPTS n] [LOCK_DEADLINE_MS n] [IN_FLIGHT_CAP_PER_GROUP n]
2627#[derive(Debug, Clone)]
2628pub struct CreateQueueQuery {
2629    pub name: String,
2630    pub mode: QueueMode,
2631    pub priority: bool,
2632    pub max_size: Option<usize>,
2633    pub ttl_ms: Option<u64>,
2634    pub dlq: Option<String>,
2635    pub max_attempts: u32,
2636    pub lock_deadline_ms: u64,
2637    pub in_flight_cap_per_group: u32,
2638    pub if_not_exists: bool,
2639}
2640
2641/// ALTER QUEUE name SET <clause>
2642///   MODE [FANOUT|WORK]
2643///   MAX_ATTEMPTS n
2644///   LOCK_DEADLINE_MS n
2645///   IN_FLIGHT_CAP_PER_GROUP n
2646///   DLQ name
2647#[derive(Debug, Clone, Default)]
2648pub struct AlterQueueQuery {
2649    pub name: String,
2650    pub mode: Option<QueueMode>,
2651    pub max_attempts: Option<u32>,
2652    pub lock_deadline_ms: Option<u64>,
2653    pub in_flight_cap_per_group: Option<u32>,
2654    pub dlq: Option<String>,
2655}
2656
2657/// DROP QUEUE [IF EXISTS] name
2658#[derive(Debug, Clone)]
2659pub struct DropQueueQuery {
2660    pub name: String,
2661    pub if_exists: bool,
2662}
2663
2664/// SELECT <columns> FROM QUEUE name [WHERE filter] [LIMIT n]
2665#[derive(Debug, Clone)]
2666pub struct QueueSelectQuery {
2667    pub queue: String,
2668    pub columns: Vec<String>,
2669    pub filter: Option<Filter>,
2670    pub limit: Option<u64>,
2671}
2672
2673/// Which end of the queue
2674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2675pub enum QueueSide {
2676    Left,
2677    Right,
2678}
2679
2680/// Queue operation commands
2681// The largest variant carries an inline `Filter`; boxing it would ripple
2682// to every construction and match site for a marginal stack-size win, so
2683// the size difference is accepted.
2684#[allow(clippy::large_enum_variant)]
2685#[derive(Debug, Clone)]
2686pub enum QueueCommand {
2687    Push {
2688        queue: String,
2689        value: Value,
2690        side: QueueSide,
2691        priority: Option<i32>,
2692    },
2693    Pop {
2694        queue: String,
2695        side: QueueSide,
2696        count: usize,
2697    },
2698    Peek {
2699        queue: String,
2700        count: usize,
2701    },
2702    Len {
2703        queue: String,
2704    },
2705    Purge {
2706        queue: String,
2707    },
2708    GroupCreate {
2709        queue: String,
2710        group: String,
2711    },
2712    GroupRead {
2713        queue: String,
2714        group: Option<String>,
2715        consumer: String,
2716        count: usize,
2717    },
2718    Pending {
2719        queue: String,
2720        group: String,
2721    },
2722    Claim {
2723        queue: String,
2724        group: String,
2725        consumer: String,
2726        min_idle_ms: u64,
2727    },
2728    Ack {
2729        queue: String,
2730        // Legacy tuple handle. Empty `group` / `message_id` strings mean
2731        // the request relies solely on `delivery_id`. ADR 0026: when both
2732        // `delivery_id` and the tuple are supplied, `delivery_id` wins.
2733        group: String,
2734        message_id: String,
2735        /// Server-issued opaque base32 delivery handle (ADR 0026). When
2736        /// present, takes precedence over the legacy tuple; the tuple is
2737        /// kept for one minor release as a wire-compat bridge.
2738        delivery_id: Option<String>,
2739    },
2740    Nack {
2741        queue: String,
2742        group: String,
2743        message_id: String,
2744        delivery_id: Option<String>,
2745    },
2746    Move {
2747        source: String,
2748        destination: String,
2749        filter: Option<Filter>,
2750        limit: usize,
2751    },
2752}
2753
2754// ============================================================================
2755// Tree DDL & Commands
2756// ============================================================================
2757
2758#[derive(Debug, Clone)]
2759pub struct TreeNodeSpec {
2760    pub label: String,
2761    pub node_type: Option<String>,
2762    pub properties: Vec<(String, Value)>,
2763    pub metadata: Vec<(String, Value)>,
2764    pub max_children: Option<usize>,
2765}
2766
2767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2768pub enum TreePosition {
2769    First,
2770    Last,
2771    Index(usize),
2772}
2773
2774#[derive(Debug, Clone)]
2775pub struct CreateTreeQuery {
2776    pub collection: String,
2777    pub name: String,
2778    pub root: TreeNodeSpec,
2779    pub default_max_children: usize,
2780    pub if_not_exists: bool,
2781}
2782
2783#[derive(Debug, Clone)]
2784pub struct DropTreeQuery {
2785    pub collection: String,
2786    pub name: String,
2787    pub if_exists: bool,
2788}
2789
2790#[derive(Debug, Clone)]
2791pub enum TreeCommand {
2792    Insert {
2793        collection: String,
2794        tree_name: String,
2795        parent_id: u64,
2796        node: TreeNodeSpec,
2797        position: TreePosition,
2798    },
2799    Move {
2800        collection: String,
2801        tree_name: String,
2802        node_id: u64,
2803        parent_id: u64,
2804        position: TreePosition,
2805    },
2806    Delete {
2807        collection: String,
2808        tree_name: String,
2809        node_id: u64,
2810    },
2811    Validate {
2812        collection: String,
2813        tree_name: String,
2814    },
2815    Rebalance {
2816        collection: String,
2817        tree_name: String,
2818        dry_run: bool,
2819    },
2820}
2821
2822// ============================================================================
2823// KV DSL Commands
2824// ============================================================================
2825
2826/// KV verb commands: `KV PUT key = value [EXPIRE n] [IF NOT EXISTS]`, `KV GET key`, `KV DELETE key`
2827#[derive(Debug, Clone)]
2828pub enum KvCommand {
2829    Put {
2830        model: CollectionModel,
2831        collection: String,
2832        key: String,
2833        value: Value,
2834        /// TTL in milliseconds (from EXPIRE clause)
2835        ttl_ms: Option<u64>,
2836        tags: Vec<String>,
2837        if_not_exists: bool,
2838    },
2839    InvalidateTags {
2840        collection: String,
2841        tags: Vec<String>,
2842    },
2843    Get {
2844        model: CollectionModel,
2845        collection: String,
2846        key: String,
2847    },
2848    Unseal {
2849        collection: String,
2850        key: String,
2851        version: Option<i64>,
2852    },
2853    Rotate {
2854        collection: String,
2855        key: String,
2856        value: Value,
2857        tags: Vec<String>,
2858    },
2859    History {
2860        collection: String,
2861        key: String,
2862    },
2863    List {
2864        model: CollectionModel,
2865        collection: String,
2866        prefix: Option<String>,
2867        limit: Option<usize>,
2868        offset: usize,
2869    },
2870    Purge {
2871        collection: String,
2872        key: String,
2873    },
2874    Watch {
2875        model: CollectionModel,
2876        collection: String,
2877        key: String,
2878        prefix: bool,
2879        from_lsn: Option<u64>,
2880    },
2881    Delete {
2882        model: CollectionModel,
2883        collection: String,
2884        key: String,
2885    },
2886    /// `KV INCR key [BY n] [EXPIRE dur]` — atomic increment; negative `by` = decrement.
2887    Incr {
2888        model: CollectionModel,
2889        collection: String,
2890        key: String,
2891        /// Step value; negative for DECR. Defaults to 1.
2892        by: i64,
2893        ttl_ms: Option<u64>,
2894    },
2895    /// `KV CAS key EXPECT <expected|NULL> SET <new> [EXPIRE dur]` — compare-and-set.
2896    ///
2897    /// `expected = None` means `EXPECT NULL` (key must be absent).
2898    Cas {
2899        model: CollectionModel,
2900        collection: String,
2901        key: String,
2902        /// The value the caller expects to be current; `None` = key must be absent.
2903        expected: Option<Value>,
2904        new_value: Value,
2905        ttl_ms: Option<u64>,
2906    },
2907}
2908
2909#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2910pub enum ConfigValueType {
2911    Bool,
2912    Int,
2913    String,
2914    Url,
2915    Object,
2916    Array,
2917}
2918
2919impl ConfigValueType {
2920    pub fn as_str(self) -> &'static str {
2921        match self {
2922            Self::Bool => "bool",
2923            Self::Int => "int",
2924            Self::String => "string",
2925            Self::Url => "url",
2926            Self::Object => "object",
2927            Self::Array => "array",
2928        }
2929    }
2930
2931    pub fn parse(input: &str) -> Option<Self> {
2932        match input.to_ascii_lowercase().as_str() {
2933            "bool" | "boolean" => Some(Self::Bool),
2934            "int" | "integer" => Some(Self::Int),
2935            "string" | "str" | "text" => Some(Self::String),
2936            "url" => Some(Self::Url),
2937            "object" | "json_object" => Some(Self::Object),
2938            "array" | "list" => Some(Self::Array),
2939            _ => None,
2940        }
2941    }
2942}
2943
2944#[derive(Debug, Clone)]
2945pub enum ConfigCommand {
2946    Put {
2947        collection: String,
2948        key: String,
2949        value: Value,
2950        value_type: Option<ConfigValueType>,
2951        tags: Vec<String>,
2952    },
2953    Get {
2954        collection: String,
2955        key: String,
2956    },
2957    Resolve {
2958        collection: String,
2959        key: String,
2960    },
2961    Rotate {
2962        collection: String,
2963        key: String,
2964        value: Value,
2965        value_type: Option<ConfigValueType>,
2966        tags: Vec<String>,
2967    },
2968    Delete {
2969        collection: String,
2970        key: String,
2971    },
2972    History {
2973        collection: String,
2974        key: String,
2975    },
2976    List {
2977        collection: String,
2978        prefix: Option<String>,
2979        limit: Option<usize>,
2980        offset: usize,
2981    },
2982    Watch {
2983        collection: String,
2984        key: String,
2985        prefix: bool,
2986        from_lsn: Option<u64>,
2987    },
2988    InvalidVolatileOperation {
2989        operation: String,
2990        collection: String,
2991        key: Option<String>,
2992    },
2993}
2994
2995// ============================================================================
2996// Builders (Fluent API)
2997// ============================================================================