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