Skip to main content

reddb_rql/
core.rs

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