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