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