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