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