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