@dbsp/types 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/chunk-5L5N5N5J.js +194 -0
- package/dist/chunk-5L5N5N5J.js.map +1 -0
- package/dist/index.d.ts +3058 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.js +85 -0
- package/dist/internal.js.map +1 -0
- package/package.json +56 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3058 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module model-ir
|
|
3
|
+
* ModelIR (Model Intermediate Representation) type definitions.
|
|
4
|
+
* Represents database tables, columns, and relations with planning metadata.
|
|
5
|
+
*
|
|
6
|
+
* Runtime functions (createPseudoColumnMetadata, createRecursiveMetadata, etc.)
|
|
7
|
+
* remain in @dbsp/core.
|
|
8
|
+
*/
|
|
9
|
+
/** Column data types supported by the planner */
|
|
10
|
+
type ColumnType = 'string' | 'text' | 'number' | 'integer' | 'bigint' | 'decimal' | 'boolean' | 'date' | 'time' | 'datetime' | 'timestamp' | 'json' | 'jsonb' | 'uuid' | 'daterange' | 'tsrange' | 'tstzrange' | 'int4range' | 'int8range' | 'numrange';
|
|
11
|
+
/** Foreign key delete behavior */
|
|
12
|
+
type OnDeleteAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
|
13
|
+
/** Relation types */
|
|
14
|
+
type RelationType = 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany';
|
|
15
|
+
/**
|
|
16
|
+
* CLI-NQL: Relation kind for natural query language.
|
|
17
|
+
* Maps to SQL/database perspective rather than ORM perspective.
|
|
18
|
+
* - 'many-to-one': Child → Parent (e.g., post.author)
|
|
19
|
+
* - 'one-to-many': Parent → Children (e.g., author.posts)
|
|
20
|
+
* - 'many-to-many': M:N via junction (e.g., post.tags)
|
|
21
|
+
* - 'recursive-up': Ancestors (e.g., category.ancestors)
|
|
22
|
+
* - 'recursive-down': Descendants (e.g., category.descendants)
|
|
23
|
+
*/
|
|
24
|
+
type RelationKind = 'many-to-one' | 'one-to-many' | 'many-to-many' | 'recursive-up' | 'recursive-down';
|
|
25
|
+
/**
|
|
26
|
+
* CLI-NQL: Recursive relation metadata.
|
|
27
|
+
* For self-referential tables like categories with parentId.
|
|
28
|
+
*/
|
|
29
|
+
interface RecursiveMetadata {
|
|
30
|
+
/**
|
|
31
|
+
* Direction of traversal.
|
|
32
|
+
* - 'up': Traverse to ancestors (parent → grandparent → ...)
|
|
33
|
+
* - 'down': Traverse to descendants (children → grandchildren → ...)
|
|
34
|
+
*/
|
|
35
|
+
readonly direction: 'up' | 'down';
|
|
36
|
+
/**
|
|
37
|
+
* Maximum recursion depth to prevent infinite loops.
|
|
38
|
+
* @default 10
|
|
39
|
+
*/
|
|
40
|
+
readonly maxDepth: number;
|
|
41
|
+
/**
|
|
42
|
+
* The relation name to follow for recursion.
|
|
43
|
+
* For 'up': typically 'parent' relation
|
|
44
|
+
* For 'down': typically 'children' relation
|
|
45
|
+
*/
|
|
46
|
+
readonly through: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Metadata for auto-generated pseudo-columns from self-referential FKs.
|
|
50
|
+
* These enable intuitive traversal in NQL: parent.name, ascendant.title, etc.
|
|
51
|
+
*/
|
|
52
|
+
interface PseudoColumnMetadata {
|
|
53
|
+
/** The table this pseudo-column belongs to */
|
|
54
|
+
readonly table: string;
|
|
55
|
+
/** The FK column that creates this self-reference */
|
|
56
|
+
readonly foreignKeyColumn: string;
|
|
57
|
+
/** Target column in the same table (usually 'id') */
|
|
58
|
+
readonly targetColumn: string;
|
|
59
|
+
/**
|
|
60
|
+
* Role names for traversal directions.
|
|
61
|
+
* parentRole: singular upward (e.g., 'parent', 'manager')
|
|
62
|
+
* childRole: plural downward (e.g., 'children', 'subordinates')
|
|
63
|
+
*/
|
|
64
|
+
readonly parentRole: string;
|
|
65
|
+
readonly childRole: string;
|
|
66
|
+
/**
|
|
67
|
+
* Keywords for recursive traversal.
|
|
68
|
+
* ascendantKeyword: recursive upward (e.g., 'ascendant', 'manager.ascendant')
|
|
69
|
+
* descendantKeyword: recursive downward (e.g., 'descendant', 'manager.descendant')
|
|
70
|
+
*/
|
|
71
|
+
readonly ascendantKeyword: string;
|
|
72
|
+
readonly descendantKeyword: string;
|
|
73
|
+
}
|
|
74
|
+
/** Cardinality for planning */
|
|
75
|
+
type Cardinality = 'one' | 'many';
|
|
76
|
+
/** Optionality for join type inference */
|
|
77
|
+
type Optionality = 'required' | 'optional';
|
|
78
|
+
/**
|
|
79
|
+
* Include strategy for fetching related data.
|
|
80
|
+
* - 'join': Use JOIN (efficient for to-one, risk of row explosion for to-many)
|
|
81
|
+
* - 'subquery': Use subquery query (safe for to-many, N+1 if not batched)
|
|
82
|
+
* - 'cte': Use CTE-based include (good for recursive/hierarchical)
|
|
83
|
+
* - 'lateral': Use LATERAL JOIN (PostgreSQL/MSSQL CROSS APPLY, handles LIMIT)
|
|
84
|
+
* - 'json_agg': Use JSON aggregation (PostgreSQL/MySQL/DuckDB, single row per parent)
|
|
85
|
+
* - 'auto': Planner decides based on relation type + dialect capabilities
|
|
86
|
+
*/
|
|
87
|
+
type IncludeStrategy = 'join' | 'subquery' | 'cte' | 'lateral' | 'json_agg' | 'auto';
|
|
88
|
+
/** Strategy for filtering by relation */
|
|
89
|
+
type FilterStrategy = 'exists' | 'join' | 'auto';
|
|
90
|
+
/** Default join type when joining */
|
|
91
|
+
type JoinDefault = 'left' | 'inner' | 'auto';
|
|
92
|
+
/**
|
|
93
|
+
* Column definition
|
|
94
|
+
*/
|
|
95
|
+
interface ColumnIR {
|
|
96
|
+
/** Column name in database */
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** Data type for TypeScript inference */
|
|
99
|
+
readonly type: ColumnType;
|
|
100
|
+
/** Whether NULL is allowed */
|
|
101
|
+
readonly nullable: boolean;
|
|
102
|
+
/** Default value (optional) */
|
|
103
|
+
readonly default?: unknown;
|
|
104
|
+
/**
|
|
105
|
+
* Original database type string from introspection.
|
|
106
|
+
* Preserves precision/scale/length info that may be lost in `type`.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* - 'varchar(255)' when type is 'string'
|
|
110
|
+
* - 'numeric(10,2)' when type is 'number'
|
|
111
|
+
* - 'timestamptz' when type is 'datetime'
|
|
112
|
+
*
|
|
113
|
+
* This is optional and only populated by introspection.
|
|
114
|
+
* Manually defined schemas may not have this field.
|
|
115
|
+
*/
|
|
116
|
+
readonly originalDbType?: string;
|
|
117
|
+
/** Whether column has a UNIQUE constraint */
|
|
118
|
+
readonly unique?: boolean;
|
|
119
|
+
/** Whether column auto-increments (SERIAL, IDENTITY, AUTOINCREMENT) */
|
|
120
|
+
readonly autoIncrement?: boolean;
|
|
121
|
+
/** Collation name for string columns */
|
|
122
|
+
readonly collation?: string;
|
|
123
|
+
/** Column comment (COMMENT ON COLUMN) */
|
|
124
|
+
readonly comment?: string;
|
|
125
|
+
/** Identity column generation strategy (GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY) */
|
|
126
|
+
readonly identity?: 'always' | 'byDefault';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Foreign key constraint
|
|
130
|
+
*/
|
|
131
|
+
interface ForeignKeyIR {
|
|
132
|
+
/** Local columns that form the FK */
|
|
133
|
+
readonly columns: readonly string[];
|
|
134
|
+
/** Referenced table and columns */
|
|
135
|
+
readonly references: {
|
|
136
|
+
readonly table: string;
|
|
137
|
+
readonly columns: readonly string[];
|
|
138
|
+
};
|
|
139
|
+
/** Delete behavior */
|
|
140
|
+
readonly onDelete?: OnDeleteAction;
|
|
141
|
+
/** Update behavior */
|
|
142
|
+
readonly onUpdate?: OnDeleteAction;
|
|
143
|
+
/** Whether this FK constraint is deferrable (DEFERRABLE INITIALLY DEFERRED) */
|
|
144
|
+
readonly deferred?: boolean;
|
|
145
|
+
/** If true, add the constraint WITHOUT scanning existing rows (NOT VALID). Use validate_constraint to validate later. */
|
|
146
|
+
readonly notValid?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* CHECK constraint definition
|
|
150
|
+
*/
|
|
151
|
+
/**
|
|
152
|
+
* PostgreSQL ENUM type definition
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* PostgreSQL sequence definition
|
|
156
|
+
*/
|
|
157
|
+
interface SequenceIR {
|
|
158
|
+
/** Sequence name */
|
|
159
|
+
readonly name: string;
|
|
160
|
+
/** Start value */
|
|
161
|
+
readonly startWith?: number;
|
|
162
|
+
/** Increment step */
|
|
163
|
+
readonly incrementBy?: number;
|
|
164
|
+
/** Minimum value */
|
|
165
|
+
readonly minValue?: number;
|
|
166
|
+
/** Maximum value */
|
|
167
|
+
readonly maxValue?: number;
|
|
168
|
+
/** Whether to cycle */
|
|
169
|
+
readonly cycle?: boolean;
|
|
170
|
+
/** Schema name (if not default) */
|
|
171
|
+
readonly schema?: string;
|
|
172
|
+
}
|
|
173
|
+
interface EnumIR {
|
|
174
|
+
/** Enum type name */
|
|
175
|
+
readonly name: string;
|
|
176
|
+
/** Ordered list of enum values */
|
|
177
|
+
readonly values: readonly string[];
|
|
178
|
+
/** Schema name (if not in default schema) */
|
|
179
|
+
readonly schema?: string;
|
|
180
|
+
}
|
|
181
|
+
interface CheckConstraintIR {
|
|
182
|
+
/** Constraint name in database */
|
|
183
|
+
readonly name: string;
|
|
184
|
+
/** CHECK expression in canonical form (from pg_get_constraintdef) */
|
|
185
|
+
readonly expression: string;
|
|
186
|
+
/** If true, add the constraint WITHOUT scanning existing rows (NOT VALID). Use validate_constraint to validate later. */
|
|
187
|
+
readonly notValid?: boolean;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Row-Level Security policy definition.
|
|
191
|
+
*
|
|
192
|
+
* Models PostgreSQL's CREATE POLICY. Other dialects (Oracle VPD, MSSQL security predicates)
|
|
193
|
+
* use fundamentally different mechanisms — this interface captures the PG-style model which
|
|
194
|
+
* is the most common. Adapters without RLS support skip policies via capability negotiation.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* {
|
|
199
|
+
* name: 'tenant_isolation',
|
|
200
|
+
* command: 'ALL',
|
|
201
|
+
* roles: ['app_user'],
|
|
202
|
+
* using: "tenant_id = current_setting('app.current_tenant')::uuid",
|
|
203
|
+
* withCheck: "tenant_id = current_setting('app.current_tenant')::uuid",
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
interface PolicyIR {
|
|
208
|
+
/** Policy name (must be unique per table) */
|
|
209
|
+
readonly name: string;
|
|
210
|
+
/** SQL command the policy applies to (default: ALL) */
|
|
211
|
+
readonly command?: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';
|
|
212
|
+
/** Database roles the policy applies to (default: PUBLIC) */
|
|
213
|
+
readonly roles?: readonly string[];
|
|
214
|
+
/** Whether the policy is permissive or restrictive (default: PERMISSIVE) */
|
|
215
|
+
readonly permissive?: boolean;
|
|
216
|
+
/** USING expression — SQL predicate for row visibility (SELECT, UPDATE, DELETE) */
|
|
217
|
+
readonly using?: string;
|
|
218
|
+
/** WITH CHECK expression — SQL predicate for new/modified rows (INSERT, UPDATE) */
|
|
219
|
+
readonly withCheck?: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Table partition configuration (parent table only).
|
|
223
|
+
* Child partition management is out of scope (DDL-PARTITION-MGMT).
|
|
224
|
+
*/
|
|
225
|
+
interface PartitionIR {
|
|
226
|
+
/** Partition strategy */
|
|
227
|
+
readonly strategy: 'RANGE' | 'LIST' | 'HASH';
|
|
228
|
+
/** Partition key columns */
|
|
229
|
+
readonly columns: readonly string[];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Index definition (single or composite)
|
|
233
|
+
*/
|
|
234
|
+
interface IndexIR {
|
|
235
|
+
/** Index name (auto-generated if not provided: idx_{table}_{columns}) */
|
|
236
|
+
readonly name?: string;
|
|
237
|
+
/** Columns included in the index (1+ columns) */
|
|
238
|
+
readonly columns: readonly string[];
|
|
239
|
+
/** Whether this is a unique index */
|
|
240
|
+
readonly unique?: boolean;
|
|
241
|
+
/** Index access method (default: btree) */
|
|
242
|
+
readonly method?: string;
|
|
243
|
+
/** Partial index predicate (WHERE clause) */
|
|
244
|
+
readonly where?: string;
|
|
245
|
+
/** Expression-based index entries (used instead of/alongside columns) */
|
|
246
|
+
readonly expressions?: readonly string[];
|
|
247
|
+
/** Non-key columns to include (INCLUDE clause, PG11+) */
|
|
248
|
+
readonly include?: readonly string[];
|
|
249
|
+
/** Per-column operator class overrides (non-default only). Key = column name, value = opclass name */
|
|
250
|
+
readonly opclass?: Readonly<Record<string, string>>;
|
|
251
|
+
/** Index storage parameters (WITH clause). Key = param name, value = param value */
|
|
252
|
+
readonly with?: Readonly<Record<string, string>>;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Table definition
|
|
256
|
+
*/
|
|
257
|
+
interface TableIR {
|
|
258
|
+
/** Table name in database */
|
|
259
|
+
readonly name: string;
|
|
260
|
+
/** Column definitions */
|
|
261
|
+
readonly columns: readonly ColumnIR[];
|
|
262
|
+
/** Primary key (single column or composite); omitted for junction tables without explicit PK */
|
|
263
|
+
readonly primaryKey?: string | readonly string[];
|
|
264
|
+
/** Foreign key constraints */
|
|
265
|
+
readonly foreignKeys: readonly ForeignKeyIR[];
|
|
266
|
+
/** Index definitions */
|
|
267
|
+
readonly indexes: readonly IndexIR[];
|
|
268
|
+
/** CHECK constraints */
|
|
269
|
+
readonly checkConstraints?: readonly CheckConstraintIR[];
|
|
270
|
+
/**
|
|
271
|
+
* Auto-generated pseudo-columns from self-referential FKs.
|
|
272
|
+
* Each self-ref FK generates: parent/child roles + ascendant/descendant keywords.
|
|
273
|
+
* For multi-FK tables, roles are scoped (e.g., manager.ascendant).
|
|
274
|
+
*/
|
|
275
|
+
readonly pseudoColumns?: readonly PseudoColumnMetadata[];
|
|
276
|
+
/** Table-level comment (COMMENT ON TABLE) */
|
|
277
|
+
readonly comment?: string;
|
|
278
|
+
/** Partition configuration (parent table). Child management deferred to DDL-PARTITION-MGMT. */
|
|
279
|
+
readonly partition?: PartitionIR;
|
|
280
|
+
/** Whether Row-Level Security is enabled on this table */
|
|
281
|
+
readonly rlsEnabled?: boolean;
|
|
282
|
+
/** Row-Level Security policies */
|
|
283
|
+
readonly policies?: readonly PolicyIR[];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Relation definition with planning metadata
|
|
287
|
+
*/
|
|
288
|
+
interface RelationIR {
|
|
289
|
+
/** Relation name (used in queries) */
|
|
290
|
+
readonly name: string;
|
|
291
|
+
/** Relation type */
|
|
292
|
+
readonly type: RelationType;
|
|
293
|
+
/** Source table name */
|
|
294
|
+
readonly source: string;
|
|
295
|
+
/** Target table name */
|
|
296
|
+
readonly target: string;
|
|
297
|
+
/** Junction table for M:N relations */
|
|
298
|
+
readonly through?: string | undefined;
|
|
299
|
+
/** Foreign key column(s) on the "many" side */
|
|
300
|
+
readonly foreignKey?: string | readonly string[] | undefined;
|
|
301
|
+
/**
|
|
302
|
+
* For M:N relations: foreign key column on junction table pointing to target.
|
|
303
|
+
* Example: In posts-tags via postTags, otherKey = 'tagId'
|
|
304
|
+
*/
|
|
305
|
+
readonly otherKey?: string | undefined;
|
|
306
|
+
/** Cardinality affects strategy selection */
|
|
307
|
+
readonly cardinality: Cardinality;
|
|
308
|
+
/** Optionality affects LEFT vs INNER join */
|
|
309
|
+
readonly optionality: Optionality;
|
|
310
|
+
/**
|
|
311
|
+
* How to fetch related data when included.
|
|
312
|
+
* - 'join': Use JOIN (efficient for to-one)
|
|
313
|
+
* - 'subquery': Use subquery query (avoids row explosion for to-many)
|
|
314
|
+
* - 'auto': Planner decides based on cardinality
|
|
315
|
+
* @default 'auto'
|
|
316
|
+
*/
|
|
317
|
+
readonly includeStrategy: IncludeStrategy;
|
|
318
|
+
/**
|
|
319
|
+
* How to filter by this relation.
|
|
320
|
+
* - 'exists': Use EXISTS subquery (no row multiplication)
|
|
321
|
+
* - 'join': Use JOIN (may cause row explosion on to-many)
|
|
322
|
+
* - 'auto': Planner decides (defaults to EXISTS for to-many)
|
|
323
|
+
* @default 'auto'
|
|
324
|
+
*/
|
|
325
|
+
readonly filterStrategy: FilterStrategy;
|
|
326
|
+
/**
|
|
327
|
+
* Default join type when joining.
|
|
328
|
+
* - 'left': LEFT JOIN (keep parent even if no child)
|
|
329
|
+
* - 'inner': INNER JOIN (parent must have child)
|
|
330
|
+
* - 'auto': Inferred from optionality + filters
|
|
331
|
+
* @default 'auto'
|
|
332
|
+
*/
|
|
333
|
+
readonly joinDefault: JoinDefault;
|
|
334
|
+
/** Column on source table that FK on target references; set when generated relation overrides the default PK. */
|
|
335
|
+
readonly sourceKey?: string | undefined;
|
|
336
|
+
/** Column on target table that the FK points to; set when generated relation overrides the default PK. */
|
|
337
|
+
readonly targetKey?: string | undefined;
|
|
338
|
+
/**
|
|
339
|
+
* CLI-NQL: Recursive relation metadata for ancestors/descendants.
|
|
340
|
+
* Only present for self-referential relations (source === target).
|
|
341
|
+
*/
|
|
342
|
+
readonly recursive?: RecursiveMetadata | undefined;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Result of checking for ambiguous relations
|
|
346
|
+
*/
|
|
347
|
+
interface AmbiguityCheckResult {
|
|
348
|
+
/** Whether the relation path is ambiguous */
|
|
349
|
+
readonly ambiguous: boolean;
|
|
350
|
+
/** Available relation names if ambiguous */
|
|
351
|
+
readonly options: readonly string[];
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Complete model intermediate representation
|
|
355
|
+
*/
|
|
356
|
+
interface ModelIR {
|
|
357
|
+
/** Table definitions indexed by name */
|
|
358
|
+
readonly tables: ReadonlyMap<string, TableIR>;
|
|
359
|
+
/** Relation definitions indexed by "source.name" */
|
|
360
|
+
readonly relations: ReadonlyMap<string, RelationIR>;
|
|
361
|
+
/** ENUM type definitions indexed by name */
|
|
362
|
+
readonly enums?: ReadonlyMap<string, EnumIR>;
|
|
363
|
+
/** Extension names to ensure (CREATE EXTENSION IF NOT EXISTS) */
|
|
364
|
+
readonly extensions?: readonly string[];
|
|
365
|
+
/** Sequence definitions */
|
|
366
|
+
readonly sequences?: ReadonlyMap<string, SequenceIR>;
|
|
367
|
+
/**
|
|
368
|
+
* Get table by logical name.
|
|
369
|
+
*
|
|
370
|
+
* @param name - Logical table name (camelCase, e.g. "postComments").
|
|
371
|
+
* This is the model-level name, NOT the database name (e.g. "post_comments").
|
|
372
|
+
* Use adapter-side naming utilities to convert DB names to logical names.
|
|
373
|
+
*/
|
|
374
|
+
getTable(name: string): TableIR | undefined;
|
|
375
|
+
/** Get relation by qualified name "source.relationName" */
|
|
376
|
+
getRelation(qualifiedName: string): RelationIR | undefined;
|
|
377
|
+
/** Get all relations from a source table */
|
|
378
|
+
getRelationsFrom(sourceTable: string): readonly RelationIR[];
|
|
379
|
+
/** Get all relations to a target table */
|
|
380
|
+
getRelationsTo(targetTable: string): readonly RelationIR[];
|
|
381
|
+
/** Check if relation path is ambiguous (multiple relations to same target) */
|
|
382
|
+
isAmbiguous(sourceTable: string, targetTable: string): AmbiguityCheckResult;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Hierarchy pattern detected during database introspection.
|
|
386
|
+
* Describes adjacency-list or edge-table patterns on self-referencing tables.
|
|
387
|
+
*/
|
|
388
|
+
interface HierarchyIR {
|
|
389
|
+
/** How the hierarchy is represented in the schema */
|
|
390
|
+
readonly type: 'adjacency' | 'edge-table';
|
|
391
|
+
/** Table that contains the nodes */
|
|
392
|
+
readonly nodeTable: string;
|
|
393
|
+
/** Edge table (for edge-table type only) */
|
|
394
|
+
readonly edgeTable?: string;
|
|
395
|
+
/** Column pointing to the parent node (adjacency) or source node (edge-table) */
|
|
396
|
+
readonly parentColumn: string;
|
|
397
|
+
/** Column pointing to the child node (edge-table type only) */
|
|
398
|
+
readonly childColumn?: string;
|
|
399
|
+
/** Column holding the node's own identifier */
|
|
400
|
+
readonly nodeIdColumn: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* @module dialects
|
|
405
|
+
* Dialect type definitions - DialectCapabilities, DialectName, column type unions.
|
|
406
|
+
*
|
|
407
|
+
* Runtime constants (POSTGRESQL_CAPABILITIES, etc.) and functions
|
|
408
|
+
* (registerDialect, getDialectCapabilities, etc.) remain in @dbsp/core.
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
/** Known dialect identifiers */
|
|
412
|
+
type DialectName = 'postgresql' | 'mysql' | 'sqlite' | 'duckdb' | 'mssql';
|
|
413
|
+
/** PostgreSQL-only column types (range types + jsonb) */
|
|
414
|
+
type PostgresOnlyColumnType = 'daterange' | 'tsrange' | 'tstzrange' | 'int4range' | 'int8range' | 'numrange' | 'jsonb';
|
|
415
|
+
/** Column types common to all dialects */
|
|
416
|
+
type CommonColumnType = Exclude<ColumnType, PostgresOnlyColumnType>;
|
|
417
|
+
/** PostgreSQL supports all column types */
|
|
418
|
+
type PostgresColumnType = ColumnType;
|
|
419
|
+
/** MySQL column types (common + json) */
|
|
420
|
+
type MySQLColumnType = CommonColumnType | 'json';
|
|
421
|
+
/** SQLite column types (common + json) */
|
|
422
|
+
type SQLiteColumnType = CommonColumnType | 'json';
|
|
423
|
+
/** DuckDB column types (common + json) */
|
|
424
|
+
type DuckDBColumnType = CommonColumnType | 'json';
|
|
425
|
+
/** MSSQL column types (common + json) */
|
|
426
|
+
type MSSQLColumnType = CommonColumnType | 'json';
|
|
427
|
+
/** Maps a dialect name to its supported column types */
|
|
428
|
+
type SupportedColumnTypes<D extends DialectName> = D extends 'postgresql' ? PostgresColumnType : D extends 'mysql' ? MySQLColumnType : D extends 'sqlite' ? SQLiteColumnType : D extends 'duckdb' ? DuckDBColumnType : D extends 'mssql' ? MSSQLColumnType : CommonColumnType;
|
|
429
|
+
/** Whether a column type is supported in a given dialect */
|
|
430
|
+
type IsTypeSupported<T extends ColumnType, D extends DialectName> = T extends SupportedColumnTypes<D> ? true : false;
|
|
431
|
+
interface DialectCapabilities {
|
|
432
|
+
/** Dialect identifier (e.g., 'postgresql', 'mysql', 'sqlite') */
|
|
433
|
+
readonly name: string;
|
|
434
|
+
/** Supports RETURNING clause for INSERT/UPDATE/DELETE */
|
|
435
|
+
readonly supportsReturning: boolean;
|
|
436
|
+
/** Supports recursive CTEs (WITH RECURSIVE) */
|
|
437
|
+
readonly supportsRecursiveCTE: boolean;
|
|
438
|
+
/** Supports window functions (ROW_NUMBER, RANK, etc.) */
|
|
439
|
+
readonly supportsWindowFunctions: boolean;
|
|
440
|
+
/** Supports native array types (e.g., PostgreSQL ARRAY) */
|
|
441
|
+
readonly supportsArrayType: boolean;
|
|
442
|
+
/** Supports native range types (PostgreSQL only: daterange, int4range, etc.) */
|
|
443
|
+
readonly supportsRangeTypes: boolean;
|
|
444
|
+
/** Supports native JSON/JSONB types */
|
|
445
|
+
readonly supportsJsonType: boolean;
|
|
446
|
+
/** Supports JSON path/extract/contains operators (PG: ->, ->>, @>, <@, ?, #>, #>>) */
|
|
447
|
+
readonly supportsJsonOperators: boolean;
|
|
448
|
+
/** Supports schema prefixes (e.g., schema.table) */
|
|
449
|
+
readonly supportsSchemas: boolean;
|
|
450
|
+
/**
|
|
451
|
+
* How to build paths in recursive CTEs.
|
|
452
|
+
* - 'array': PostgreSQL ARRAY[] || ARRAY[item]
|
|
453
|
+
* - 'string': MySQL/SQLite CONCAT(path, '/', item)
|
|
454
|
+
* - 'json': JSON array append
|
|
455
|
+
*/
|
|
456
|
+
readonly recursivePathStyle: 'array' | 'string' | 'json';
|
|
457
|
+
/**
|
|
458
|
+
* String concatenation style.
|
|
459
|
+
* - 'operator': Uses || operator (PostgreSQL, SQLite)
|
|
460
|
+
* - 'function': Uses CONCAT() function (MySQL)
|
|
461
|
+
*/
|
|
462
|
+
readonly stringConcatStyle: 'operator' | 'function';
|
|
463
|
+
/**
|
|
464
|
+
* Identifier quoting character.
|
|
465
|
+
* - '"': PostgreSQL, SQLite (standard SQL)
|
|
466
|
+
* - '`': MySQL
|
|
467
|
+
* - '[': MSSQL
|
|
468
|
+
*/
|
|
469
|
+
readonly identifierQuote: '"' | '`' | '[';
|
|
470
|
+
/**
|
|
471
|
+
* Parameter placeholder style.
|
|
472
|
+
* - 'dollar': $1, $2, $3 (PostgreSQL)
|
|
473
|
+
* - 'question': ? (MySQL, SQLite)
|
|
474
|
+
* - 'named': :param (some drivers)
|
|
475
|
+
*/
|
|
476
|
+
readonly parameterStyle: 'dollar' | 'question' | 'named';
|
|
477
|
+
/**
|
|
478
|
+
* Limit/offset syntax.
|
|
479
|
+
* - 'limit-offset': LIMIT x OFFSET y (most databases)
|
|
480
|
+
* - 'top': TOP x (MSSQL)
|
|
481
|
+
* - 'fetch': FETCH FIRST x ROWS ONLY (standard SQL:2008)
|
|
482
|
+
*/
|
|
483
|
+
readonly limitStyle: 'limit-offset' | 'top' | 'fetch';
|
|
484
|
+
/**
|
|
485
|
+
* Boolean literal style.
|
|
486
|
+
* - 'native': TRUE/FALSE keywords
|
|
487
|
+
* - 'numeric': 1/0 values
|
|
488
|
+
*/
|
|
489
|
+
readonly booleanStyle: 'native' | 'numeric';
|
|
490
|
+
/**
|
|
491
|
+
* Supports LATERAL JOIN (PostgreSQL) or CROSS APPLY (MSSQL).
|
|
492
|
+
* Enables per-row subqueries with LIMIT for hasMany relations.
|
|
493
|
+
*/
|
|
494
|
+
readonly supportsLateralJoin: boolean;
|
|
495
|
+
/**
|
|
496
|
+
* Supports JSON aggregation functions.
|
|
497
|
+
* - PostgreSQL: json_agg(), jsonb_agg()
|
|
498
|
+
* - MySQL: JSON_ARRAYAGG()
|
|
499
|
+
* - DuckDB: json_group_array()
|
|
500
|
+
*/
|
|
501
|
+
readonly supportsJsonAgg: boolean;
|
|
502
|
+
/** ENUM types (PG: CREATE TYPE, MySQL: inline ENUM(), SQLite: CHECK) */
|
|
503
|
+
readonly supportsDDLEnumTypes?: boolean;
|
|
504
|
+
/** Sequences (PG: CREATE SEQUENCE, MySQL: N/A uses AUTO_INCREMENT) */
|
|
505
|
+
readonly supportsDDLSequences?: boolean;
|
|
506
|
+
/** Extensions (PG: CREATE EXTENSION, others: N/A or load_extension) */
|
|
507
|
+
readonly supportsDDLExtensions?: boolean;
|
|
508
|
+
/** Table partitioning (PG/MySQL: PARTITION BY, SQLite: N/A) */
|
|
509
|
+
readonly supportsDDLPartitioning?: boolean;
|
|
510
|
+
/** CHECK constraints (PG/MySQL 8.0.16+/SQLite: CHECK) */
|
|
511
|
+
readonly supportsDDLCheckConstraints?: boolean;
|
|
512
|
+
/** ON UPDATE actions on FK (CASCADE, SET NULL, etc.) */
|
|
513
|
+
readonly supportsDDLOnUpdateFK?: boolean;
|
|
514
|
+
/** DEFERRABLE INITIALLY DEFERRED on FK constraints */
|
|
515
|
+
readonly supportsDDLDeferredFK?: boolean;
|
|
516
|
+
/** Identity columns — GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY */
|
|
517
|
+
readonly supportsDDLIdentityColumns?: boolean;
|
|
518
|
+
/** Column collation (COLLATE) */
|
|
519
|
+
readonly supportsDDLCollation?: boolean;
|
|
520
|
+
/** COMMENT ON TABLE/COLUMN (PG: COMMENT ON, MySQL: inline COMMENT) */
|
|
521
|
+
readonly supportsDDLComments?: boolean;
|
|
522
|
+
/** Non-btree index methods (GIN, GiST, HASH, BRIN, HNSW) */
|
|
523
|
+
readonly supportsDDLIndexMethods?: boolean;
|
|
524
|
+
/** Per-column operator class (gin_trgm_ops, vector_cosine_ops) */
|
|
525
|
+
readonly supportsDDLIndexOpclass?: boolean;
|
|
526
|
+
/** INCLUDE non-key columns (PG11+, Oracle 18c+, MSSQL) */
|
|
527
|
+
readonly supportsDDLIndexInclude?: boolean;
|
|
528
|
+
/** Partial indexes (WHERE clause) */
|
|
529
|
+
readonly supportsDDLPartialIndexes?: boolean;
|
|
530
|
+
/** Expression/functional indexes */
|
|
531
|
+
readonly supportsDDLExpressionIndexes?: boolean;
|
|
532
|
+
/** Row-Level Security policies (PG: CREATE POLICY, ALTER TABLE ENABLE ROW LEVEL SECURITY) */
|
|
533
|
+
readonly supportsDDLRowLevelSecurity?: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Shared utility types used across all @dbsp packages
|
|
538
|
+
* @module @dbsp/types/shared/utils
|
|
539
|
+
*/
|
|
540
|
+
/**
|
|
541
|
+
* Sort direction for ORDER BY clauses
|
|
542
|
+
* @public
|
|
543
|
+
*/
|
|
544
|
+
type SortDirection = 'asc' | 'desc';
|
|
545
|
+
/**
|
|
546
|
+
* Range value representation for PostgreSQL range types.
|
|
547
|
+
* Supports: daterange, tsrange, tstzrange, int4range, int8range, numrange
|
|
548
|
+
* @public
|
|
549
|
+
*/
|
|
550
|
+
interface RangeValue {
|
|
551
|
+
readonly lower: unknown;
|
|
552
|
+
readonly upper: unknown;
|
|
553
|
+
/** Bounds specification: '[)' (default), '[]', '()', '(]' */
|
|
554
|
+
readonly bounds?: '[)' | '[]' | '()' | '(]';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @module intent/operators
|
|
559
|
+
* Comparison and logical operator types for intent AST.
|
|
560
|
+
*/
|
|
561
|
+
/** Comparison operators for scalar values */
|
|
562
|
+
type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'isDistinctFrom';
|
|
563
|
+
/** String operators */
|
|
564
|
+
type StringOperator = 'like';
|
|
565
|
+
/** Array operators */
|
|
566
|
+
type ArrayOperator = 'in';
|
|
567
|
+
/** Null operators */
|
|
568
|
+
type NullOperator = 'isNull' | 'isNotNull';
|
|
569
|
+
/** Logical operators */
|
|
570
|
+
type LogicalOperator = 'and' | 'or' | 'not';
|
|
571
|
+
/** Relation filter operators */
|
|
572
|
+
type RelationOperator = 'exists' | 'notExists';
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* @module intent/recursive-types
|
|
576
|
+
* Recursive relation types for hierarchical queries.
|
|
577
|
+
*/
|
|
578
|
+
/**
|
|
579
|
+
* Direction of recursion for ancestors/descendants traversal.
|
|
580
|
+
* - 'up': Traverse to ancestors (parent → grandparent → ...)
|
|
581
|
+
* - 'down': Traverse to descendants (children → grandchildren → ...)
|
|
582
|
+
*/
|
|
583
|
+
type RecursiveDirection = 'up' | 'down';
|
|
584
|
+
/**
|
|
585
|
+
* Options for recursive EXISTS checks.
|
|
586
|
+
* Used when checking existence through a recursive path (ancestors/descendants).
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* // Check if any ancestor has name = 'Electronics'
|
|
590
|
+
* { direction: 'up', through: 'parent', maxDepth: 10 }
|
|
591
|
+
*/
|
|
592
|
+
interface RecursiveExistsOptions {
|
|
593
|
+
/** Direction of recursion: up (ancestors) or down (descendants) */
|
|
594
|
+
readonly direction: RecursiveDirection;
|
|
595
|
+
/** The relation name to follow for recursion (e.g., 'parent' for ancestors) */
|
|
596
|
+
readonly through: string;
|
|
597
|
+
/** Maximum recursion depth (default: 10) */
|
|
598
|
+
readonly maxDepth?: number;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* @module intent/where-intent
|
|
603
|
+
* Where intent types for filter conditions.
|
|
604
|
+
*/
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Typed field reference for cross-table column comparisons in relation filters.
|
|
608
|
+
*
|
|
609
|
+
* When using aliased relation filters like `some(orders as o, o.total > minOrder)`,
|
|
610
|
+
* the RHS `minOrder` is a reference to the parent table's column, not a literal value.
|
|
611
|
+
* FieldRef captures this distinction so the adapter can compile it as a column reference
|
|
612
|
+
* instead of a parameterized value.
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* // some(rel as r, r.col > bareCol) → value: { kind: 'fieldRef', column: 'bareCol', scope: 'outer' }
|
|
616
|
+
* // some(rel as r, r.col > r.otherCol) → value: { kind: 'fieldRef', column: 'otherCol', scope: 'inner' }
|
|
617
|
+
* // some(a as x, some(b as y, y.f > x.g)) → value: { kind: 'fieldRef', column: 'g', scope: 'outer', alias: 'x' }
|
|
618
|
+
*/
|
|
619
|
+
interface FieldRef {
|
|
620
|
+
readonly kind: 'fieldRef';
|
|
621
|
+
readonly column: string;
|
|
622
|
+
readonly scope: 'inner' | 'outer';
|
|
623
|
+
/** Named alias for outer scope (when referencing a specific outer alias in nested filters) */
|
|
624
|
+
readonly alias?: string;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Type guard for FieldRef values
|
|
628
|
+
*/
|
|
629
|
+
declare function isFieldRef(value: unknown): value is FieldRef;
|
|
630
|
+
interface WhereComparisonIntent {
|
|
631
|
+
readonly kind: 'comparison';
|
|
632
|
+
readonly field: string;
|
|
633
|
+
readonly operator: ComparisonOperator;
|
|
634
|
+
readonly value: unknown;
|
|
635
|
+
/** JSON path extraction before comparison (e.g., data->'key' = 'val') */
|
|
636
|
+
readonly jsonPath?: readonly string[];
|
|
637
|
+
/** JSON extraction mode: 'json' = ->, 'text' = ->> */
|
|
638
|
+
readonly jsonMode?: 'json' | 'text';
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* String filter: field like pattern
|
|
642
|
+
*/
|
|
643
|
+
interface WhereLikeIntent {
|
|
644
|
+
readonly kind: 'like';
|
|
645
|
+
readonly field: string;
|
|
646
|
+
readonly pattern: string;
|
|
647
|
+
/** Case-insensitive matching */
|
|
648
|
+
readonly caseInsensitive?: boolean;
|
|
649
|
+
/** Escape character for LIKE pattern (e.g. '\\' to escape _ and %) */
|
|
650
|
+
readonly escape?: string;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Array filter: field in [values]
|
|
654
|
+
*/
|
|
655
|
+
interface WhereInIntent {
|
|
656
|
+
readonly kind: 'in';
|
|
657
|
+
readonly field: string;
|
|
658
|
+
readonly values: readonly unknown[];
|
|
659
|
+
/** Optional subquery producing the value set (when present, values is empty) */
|
|
660
|
+
readonly subquery?: QueryIntent;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Array membership filter using PostgreSQL ANY() operator.
|
|
664
|
+
* Compiles to: "col" = ANY($N::type[])
|
|
665
|
+
*/
|
|
666
|
+
interface WhereAnyIntent {
|
|
667
|
+
readonly kind: 'any';
|
|
668
|
+
readonly field: string;
|
|
669
|
+
readonly values: readonly unknown[];
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Range operator for PostgreSQL range types.
|
|
673
|
+
* - overlaps: && (ranges have common points)
|
|
674
|
+
* - contains: @> (range contains value or range)
|
|
675
|
+
* - containedBy: <@ (range is contained by another range)
|
|
676
|
+
*/
|
|
677
|
+
type RangeOperator = 'overlaps' | 'contains' | 'containedBy' | 'between';
|
|
678
|
+
/**
|
|
679
|
+
* Range filter: field overlaps/contains/containedBy range value
|
|
680
|
+
* PostgreSQL range types: daterange, tsrange, tstzrange, int4range, int8range, numrange
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* // Check if booking dates overlap a period
|
|
684
|
+
* { kind: 'range', field: 'dates', operator: 'overlaps', value: { lower: '2025-01-15', upper: '2025-01-20' } }
|
|
685
|
+
*
|
|
686
|
+
* // Check if salary range contains a value
|
|
687
|
+
* { kind: 'range', field: 'salary_range', operator: 'contains', value: 50000 }
|
|
688
|
+
*/
|
|
689
|
+
interface WhereRangeIntent {
|
|
690
|
+
readonly kind: 'range';
|
|
691
|
+
readonly field: string;
|
|
692
|
+
readonly operator: RangeOperator;
|
|
693
|
+
/** Can be a RangeValue (for range-to-range ops) or scalar (for contains/containedBy with point) */
|
|
694
|
+
readonly value: RangeValue | unknown;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Null filter: field is null / is not null
|
|
698
|
+
*/
|
|
699
|
+
interface WhereNullIntent {
|
|
700
|
+
readonly kind: 'null';
|
|
701
|
+
readonly field: string;
|
|
702
|
+
readonly operator: NullOperator;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Logical AND: all conditions must match
|
|
706
|
+
*/
|
|
707
|
+
interface WhereAndIntent {
|
|
708
|
+
readonly kind: 'and';
|
|
709
|
+
readonly conditions: readonly WhereIntent[];
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Logical OR: at least one condition must match
|
|
713
|
+
*/
|
|
714
|
+
interface WhereOrIntent {
|
|
715
|
+
readonly kind: 'or';
|
|
716
|
+
readonly conditions: readonly WhereIntent[];
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Logical NOT: condition must not match
|
|
720
|
+
*/
|
|
721
|
+
interface WhereNotIntent {
|
|
722
|
+
readonly kind: 'not';
|
|
723
|
+
readonly condition: WhereIntent;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Relation exists filter: filter by existence of related records
|
|
727
|
+
* Critical for Q1 golden test - enables EXISTS subquery strategy
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* // Find users who have at least one published post
|
|
731
|
+
* { kind: 'exists', relation: 'posts', where: { kind: 'comparison', field: 'status', operator: 'eq', value: 'published' } }
|
|
732
|
+
*/
|
|
733
|
+
interface WhereExistsIntent {
|
|
734
|
+
readonly kind: 'exists';
|
|
735
|
+
/** Relation name to check existence */
|
|
736
|
+
readonly relation: string;
|
|
737
|
+
/** Optional filter on related records */
|
|
738
|
+
readonly where?: WhereIntent;
|
|
739
|
+
/**
|
|
740
|
+
* Recursive options for ancestor/descendant existence checks.
|
|
741
|
+
* When present, generates a recursive CTE instead of simple EXISTS.
|
|
742
|
+
*/
|
|
743
|
+
readonly recursive?: RecursiveExistsOptions;
|
|
744
|
+
/**
|
|
745
|
+
* Optional JOIN declarations inside the EXISTS subquery.
|
|
746
|
+
* Keys are relation names (used as aliases), values specify join type.
|
|
747
|
+
* Enables filtering on joined tables inside the subquery.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* exists('callers', {
|
|
751
|
+
* include: { callerFile: { join: 'inner' } },
|
|
752
|
+
* where: eq('callerFile.project_id', projectId)
|
|
753
|
+
* })
|
|
754
|
+
*/
|
|
755
|
+
readonly include?: Readonly<Record<string, {
|
|
756
|
+
join?: 'inner' | 'left';
|
|
757
|
+
}>>;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Relation not exists filter: filter by absence of related records
|
|
761
|
+
*
|
|
762
|
+
* @example
|
|
763
|
+
* // Find users who have no posts
|
|
764
|
+
* { kind: 'notExists', relation: 'posts' }
|
|
765
|
+
*/
|
|
766
|
+
interface WhereNotExistsIntent {
|
|
767
|
+
readonly kind: 'notExists';
|
|
768
|
+
/** Relation name to check absence */
|
|
769
|
+
readonly relation: string;
|
|
770
|
+
/** Optional filter on related records */
|
|
771
|
+
readonly where?: WhereIntent;
|
|
772
|
+
/**
|
|
773
|
+
* Recursive options for ancestor/descendant absence checks.
|
|
774
|
+
* When present, generates a recursive CTE instead of simple NOT EXISTS.
|
|
775
|
+
*/
|
|
776
|
+
readonly recursive?: RecursiveExistsOptions;
|
|
777
|
+
/**
|
|
778
|
+
* Optional JOIN declarations inside the NOT EXISTS subquery.
|
|
779
|
+
* Keys are relation names (used as aliases), values specify join type.
|
|
780
|
+
* Enables filtering on joined tables inside the subquery.
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* notExists('callers', {
|
|
784
|
+
* include: { callerFile: { join: 'inner' } },
|
|
785
|
+
* where: eq('callerFile.project_id', projectId)
|
|
786
|
+
* })
|
|
787
|
+
*/
|
|
788
|
+
readonly include?: Readonly<Record<string, {
|
|
789
|
+
join?: 'inner' | 'left';
|
|
790
|
+
}>>;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Raw EXISTS subquery filter using an arbitrary QueryIntent.
|
|
794
|
+
* Unlike WhereExistsIntent (which uses FK-resolved relation names),
|
|
795
|
+
* this wraps a fully-specified subquery for correlated EXISTS checks.
|
|
796
|
+
*
|
|
797
|
+
* @example
|
|
798
|
+
* // EXISTS (SELECT 1 FROM symbols WHERE symbols.id = calls.symbol_id AND ...)
|
|
799
|
+
* exists(subquery('symbols').where(eq('id', ref('calls.symbol_id'))))
|
|
800
|
+
*/
|
|
801
|
+
interface WhereRawExistsIntent {
|
|
802
|
+
readonly kind: 'rawExists';
|
|
803
|
+
/** The subquery producing rows for the EXISTS check */
|
|
804
|
+
readonly subquery: QueryIntent;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Raw NOT EXISTS subquery filter using an arbitrary QueryIntent.
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* // NOT EXISTS (SELECT 1 FROM symbols WHERE ...)
|
|
811
|
+
* notExists(subquery('symbols').where(...))
|
|
812
|
+
*/
|
|
813
|
+
interface WhereRawNotExistsIntent {
|
|
814
|
+
readonly kind: 'rawNotExists';
|
|
815
|
+
/** The subquery producing rows for the NOT EXISTS check */
|
|
816
|
+
readonly subquery: QueryIntent;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Relation filter: filter parent by conditions on related records
|
|
820
|
+
* More flexible than exists - allows filtering by related record attributes
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* // Find users whose latest post was created in 2024
|
|
824
|
+
* { kind: 'relationFilter', relation: 'posts', where: {...}, mode: 'some' }
|
|
825
|
+
*/
|
|
826
|
+
interface WhereRelationFilterIntent {
|
|
827
|
+
readonly kind: 'relationFilter';
|
|
828
|
+
/**
|
|
829
|
+
* Relation path for filtering.
|
|
830
|
+
* - Single relation: 'posts' or ['posts']
|
|
831
|
+
* - Multi-hop (SPEC-002): ['author', 'company'] for author.company traversal
|
|
832
|
+
*/
|
|
833
|
+
readonly relation: string | readonly string[];
|
|
834
|
+
/** Filter conditions on related records */
|
|
835
|
+
readonly where: WhereIntent;
|
|
836
|
+
/**
|
|
837
|
+
* Match mode:
|
|
838
|
+
* - 'some': At least one related record matches (default)
|
|
839
|
+
* - 'every': All related records match
|
|
840
|
+
* - 'none': No related records match
|
|
841
|
+
*/
|
|
842
|
+
readonly mode: 'some' | 'every' | 'none';
|
|
843
|
+
/** Optional alias for complex conditions (SPEC-002) */
|
|
844
|
+
readonly alias?: string | undefined;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Reference to a parent query column in a subquery.
|
|
848
|
+
* Used to create correlated subqueries.
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* // Reference parent 'id' column in subquery WHERE
|
|
852
|
+
* { kind: 'ref', column: 'id' }
|
|
853
|
+
* { kind: 'ref', column: 't0.id' } // with alias
|
|
854
|
+
*/
|
|
855
|
+
interface SubqueryRefIntent {
|
|
856
|
+
readonly kind: 'ref';
|
|
857
|
+
/** Column name or aliased column (e.g., 'id' or 't0.id') */
|
|
858
|
+
readonly column: string;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Subquery intent for scalar subquery comparisons.
|
|
862
|
+
* Produces correlated subqueries in SQL.
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* // Find products where price equals max price of category
|
|
866
|
+
* {
|
|
867
|
+
* kind: 'subquery',
|
|
868
|
+
* field: 'price',
|
|
869
|
+
* operator: 'eq',
|
|
870
|
+
* subquery: { from: 'products', select: { kind: 'aggregate', fn: 'max', field: 'price' } }
|
|
871
|
+
* }
|
|
872
|
+
*/
|
|
873
|
+
interface WhereSubqueryIntent {
|
|
874
|
+
readonly kind: 'subquery';
|
|
875
|
+
/** Field to compare on the parent query */
|
|
876
|
+
readonly field: string;
|
|
877
|
+
/** Comparison operator */
|
|
878
|
+
readonly operator: ComparisonOperator;
|
|
879
|
+
/** Subquery producing scalar value */
|
|
880
|
+
readonly subquery: QueryIntent;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Scalar subquery intent - produces a single value.
|
|
884
|
+
* Simplified QueryIntent for subquery context.
|
|
885
|
+
*/
|
|
886
|
+
/** @deprecated Use QueryIntent instead — subqueries are full queries with contextual validation */
|
|
887
|
+
type ScalarSubqueryIntent = QueryIntent;
|
|
888
|
+
/**
|
|
889
|
+
* JSON containment filter: col @> value or col <@ value.
|
|
890
|
+
* @example { kind: 'jsonContains', field: 'data', value: '{"active":true}', reversed: false }
|
|
891
|
+
* → WHERE "data" @> $1
|
|
892
|
+
*/
|
|
893
|
+
interface WhereJsonContainsIntent {
|
|
894
|
+
readonly kind: 'jsonContains';
|
|
895
|
+
readonly field: string;
|
|
896
|
+
readonly value: unknown;
|
|
897
|
+
/** false = @> (field contains value), true = <@ (field contained by value) */
|
|
898
|
+
readonly reversed: boolean;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* JSON key existence filter: col ? 'key'.
|
|
902
|
+
* @example { kind: 'jsonExists', field: 'data', key: 'email' }
|
|
903
|
+
* → WHERE "data" ? $1
|
|
904
|
+
*/
|
|
905
|
+
interface WhereJsonExistsIntent {
|
|
906
|
+
readonly kind: 'jsonExists';
|
|
907
|
+
readonly field: string;
|
|
908
|
+
readonly key: string;
|
|
909
|
+
}
|
|
910
|
+
/** WHERE clause using a custom expression with comparison */
|
|
911
|
+
interface WhereExpressionIntent {
|
|
912
|
+
readonly kind: 'expression';
|
|
913
|
+
readonly expr: ExpressionIntent;
|
|
914
|
+
readonly operator: ComparisonOperator;
|
|
915
|
+
readonly value: unknown;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Where intent - filter conditions union type
|
|
919
|
+
* Discriminated union using 'kind' field
|
|
920
|
+
*/
|
|
921
|
+
type WhereIntent = WhereComparisonIntent | WhereLikeIntent | WhereInIntent | WhereAnyIntent | WhereNullIntent | WhereRangeIntent | WhereAndIntent | WhereOrIntent | WhereNotIntent | WhereExistsIntent | WhereNotExistsIntent | WhereRawExistsIntent | WhereRawNotExistsIntent | WhereRelationFilterIntent | WhereSubqueryIntent | WhereJsonContainsIntent | WhereJsonExistsIntent | WhereExpressionIntent;
|
|
922
|
+
|
|
923
|
+
/** Null handling in sort */
|
|
924
|
+
type NullsPosition = 'first' | 'last';
|
|
925
|
+
/**
|
|
926
|
+
* Select all columns
|
|
927
|
+
*/
|
|
928
|
+
interface SelectAllIntent {
|
|
929
|
+
readonly type: 'all';
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Select specific fields
|
|
933
|
+
*/
|
|
934
|
+
interface SelectFieldsIntent {
|
|
935
|
+
readonly type: 'fields';
|
|
936
|
+
/** Field names to select */
|
|
937
|
+
readonly fields: readonly string[];
|
|
938
|
+
}
|
|
939
|
+
/** Aggregate function types */
|
|
940
|
+
type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'array_agg' | 'string_agg';
|
|
941
|
+
/**
|
|
942
|
+
* Aggregate operation intent
|
|
943
|
+
* @example { function: 'count' } → COUNT(*)
|
|
944
|
+
* @example { function: 'sum', field: 'price' } → SUM(price)
|
|
945
|
+
*/
|
|
946
|
+
interface AggregateIntent {
|
|
947
|
+
/** Aggregate function */
|
|
948
|
+
readonly function: AggregateFunction;
|
|
949
|
+
/** Field to aggregate (optional for count without field) */
|
|
950
|
+
readonly field?: string;
|
|
951
|
+
/** Alias for result column */
|
|
952
|
+
readonly as?: string;
|
|
953
|
+
/** Whether to apply DISTINCT to the aggregate (e.g., COUNT(DISTINCT field)) */
|
|
954
|
+
readonly distinct?: boolean;
|
|
955
|
+
/** FILTER (WHERE ...) clause for conditional aggregation */
|
|
956
|
+
readonly filter?: WhereIntent;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Select with aggregate functions
|
|
960
|
+
*/
|
|
961
|
+
interface SelectAggregateIntent {
|
|
962
|
+
readonly type: 'aggregate';
|
|
963
|
+
/** Aggregate operations */
|
|
964
|
+
readonly aggregates: readonly AggregateIntent[];
|
|
965
|
+
/** Non-aggregate fields (for GROUP BY) */
|
|
966
|
+
readonly fields?: readonly string[];
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Select with expressions (computed columns).
|
|
970
|
+
* Columns are ExpressionIntent directly - matches NQL compiler output.
|
|
971
|
+
*/
|
|
972
|
+
interface SelectWithExpressionsIntent {
|
|
973
|
+
readonly type: 'expressions';
|
|
974
|
+
/** Columns as ExpressionIntent (NQL format) */
|
|
975
|
+
readonly columns: readonly ExpressionIntent[];
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Select intent - what columns to retrieve
|
|
979
|
+
*/
|
|
980
|
+
type SelectIntent = SelectAllIntent | SelectFieldsIntent | SelectAggregateIntent | SelectWithExpressionsIntent;
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* @module intent/expression-intent
|
|
984
|
+
* Expression intent types for computed/derived values in SELECT.
|
|
985
|
+
*/
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* COALESCE expression: returns first non-null value from a list of fields
|
|
989
|
+
* @example { kind: 'coalesce', fields: ['name_fr', 'name_en'], as: 'display_name' }
|
|
990
|
+
* → COALESCE(name_fr, name_en) AS display_name
|
|
991
|
+
*/
|
|
992
|
+
interface CoalesceExpressionIntent {
|
|
993
|
+
readonly kind: 'coalesce';
|
|
994
|
+
/** Fields to check in order (first non-null wins) */
|
|
995
|
+
readonly fields: readonly string[];
|
|
996
|
+
/** Required alias for the result column */
|
|
997
|
+
readonly as: string;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Raw SQL expression (escape hatch for advanced use cases)
|
|
1001
|
+
* @example { kind: 'raw', sql: 'NOW()', as: 'current_time' }
|
|
1002
|
+
* → NOW() AS current_time
|
|
1003
|
+
* @warning Use sparingly - bypasses type safety and SQL injection protection
|
|
1004
|
+
*/
|
|
1005
|
+
interface RawExpressionIntent {
|
|
1006
|
+
readonly kind: 'raw';
|
|
1007
|
+
/** Raw SQL fragment (must be safe, no user input!) */
|
|
1008
|
+
readonly sql: string;
|
|
1009
|
+
/** Required alias for the result column */
|
|
1010
|
+
readonly as: string;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Simple column expression: just a column reference, optionally aliased
|
|
1014
|
+
* Used by NQL when a column is selected without modification
|
|
1015
|
+
* @example { kind: 'column', column: 'name' }
|
|
1016
|
+
* → SELECT "name"
|
|
1017
|
+
* @example { kind: 'column', column: 'name', as: 'userName' }
|
|
1018
|
+
* → SELECT "name" AS "userName"
|
|
1019
|
+
*/
|
|
1020
|
+
interface ColumnExpressionIntent {
|
|
1021
|
+
readonly kind: 'column';
|
|
1022
|
+
/** Column name to select */
|
|
1023
|
+
readonly column: string;
|
|
1024
|
+
/** Optional alias for the result column */
|
|
1025
|
+
readonly as?: string;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Column alias expression: simple column reference with alias
|
|
1029
|
+
* Uses native Kysely eb.ref().as() - type-safe and dialect-portable
|
|
1030
|
+
* @example { kind: 'columnAlias', column: 'name', alias: 'userName' }
|
|
1031
|
+
* → SELECT "name" AS "userName"
|
|
1032
|
+
*/
|
|
1033
|
+
interface ColumnAliasIntent {
|
|
1034
|
+
readonly kind: 'columnAlias';
|
|
1035
|
+
/** Column name to select */
|
|
1036
|
+
readonly column: string;
|
|
1037
|
+
/** Alias for the result column */
|
|
1038
|
+
readonly alias: string;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Relation column expression: select a column from a related table
|
|
1042
|
+
* Auto-creates JOIN via include mechanism and selects with custom alias
|
|
1043
|
+
* @example { kind: 'relationColumn', relation: 'category', column: 'name', as: 'categoryName' }
|
|
1044
|
+
* → SELECT t1."name" AS "categoryName" (where t1 is the joined category table)
|
|
1045
|
+
* @example { kind: 'relationColumn', relation: 'category.parent', column: 'name', as: 'parentCategoryName' }
|
|
1046
|
+
* → Multi-level join: products → category → parent, select parent.name
|
|
1047
|
+
*/
|
|
1048
|
+
interface RelationColumnIntent {
|
|
1049
|
+
readonly kind: 'relationColumn';
|
|
1050
|
+
/** Relation path to traverse (dot-separated for multi-level) */
|
|
1051
|
+
readonly relation: string;
|
|
1052
|
+
/** Column name to select from the target relation */
|
|
1053
|
+
readonly column: string;
|
|
1054
|
+
/** Alias for the result column */
|
|
1055
|
+
readonly as: string;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Aggregate expression intent for SELECT expressions
|
|
1059
|
+
* @example { kind: 'aggregate', function: 'count', as: 'total' } → COUNT(*) AS total
|
|
1060
|
+
* @example { kind: 'aggregate', function: 'sum', field: 'price', as: 'total_price' } → SUM(price) AS total_price
|
|
1061
|
+
* @example { kind: 'aggregate', function: 'count', field: 'id', distinct: true, as: 'unique_count' }
|
|
1062
|
+
* → COUNT(DISTINCT id) AS unique_count
|
|
1063
|
+
*/
|
|
1064
|
+
interface AggregateExpressionIntent {
|
|
1065
|
+
readonly kind: 'aggregate';
|
|
1066
|
+
/** Aggregate function */
|
|
1067
|
+
readonly function: AggregateFunction;
|
|
1068
|
+
/** Field to aggregate (or '*' for count) */
|
|
1069
|
+
readonly field: string | '*';
|
|
1070
|
+
/** Alias for result column */
|
|
1071
|
+
readonly as?: string | undefined;
|
|
1072
|
+
/** Whether to apply DISTINCT to the aggregate */
|
|
1073
|
+
readonly distinct?: boolean | undefined;
|
|
1074
|
+
/** Extra arguments for multi-arg aggregates like string_agg(field, separator) */
|
|
1075
|
+
readonly extraArgs?: readonly unknown[] | undefined;
|
|
1076
|
+
/** FILTER (WHERE ...) clause for conditional aggregation */
|
|
1077
|
+
readonly filter?: WhereIntent | undefined;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Pseudo-column traversal keyword for self-referential relations.
|
|
1081
|
+
* Used by NQL to traverse hierarchical/tree structures.
|
|
1082
|
+
*
|
|
1083
|
+
* Default keywords: 'parent', 'child', 'ascendant', 'descendant'.
|
|
1084
|
+
* Custom keywords are supported via schema configuration
|
|
1085
|
+
* (e.g., 'manager', 'managee' via parentRole/childRole).
|
|
1086
|
+
*/
|
|
1087
|
+
type PseudoColumnTraversal = string;
|
|
1088
|
+
/**
|
|
1089
|
+
* Pseudo-column expression intent for self-referential traversal.
|
|
1090
|
+
* Enables access to columns on related rows in hierarchical structures.
|
|
1091
|
+
*
|
|
1092
|
+
* @example { kind: 'pseudoColumn', traversal: 'parent', targetColumn: 'name', as: 'parent.name' }
|
|
1093
|
+
* → SELECT parent_row.name AS "parent.name" via CTE join
|
|
1094
|
+
* @example { kind: 'pseudoColumn', traversal: 'ascendant', targetColumn: 'title', as: 'ancestor_title' }
|
|
1095
|
+
* → Recursive CTE to find all ancestors, return their title column
|
|
1096
|
+
*/
|
|
1097
|
+
interface PseudoColumnExpressionIntent {
|
|
1098
|
+
readonly kind: 'pseudoColumn';
|
|
1099
|
+
/** Traversal type: single-hop (parent/child) or recursive (ascendant/descendant) */
|
|
1100
|
+
readonly traversal: PseudoColumnTraversal;
|
|
1101
|
+
/** The column to access on the target row(s) */
|
|
1102
|
+
readonly targetColumn: string;
|
|
1103
|
+
/** Alias for result column (required) */
|
|
1104
|
+
readonly as: string;
|
|
1105
|
+
/** Optional bounded depth for ascendant[N] / descendant[N] syntax */
|
|
1106
|
+
readonly depth?: number;
|
|
1107
|
+
/** Custom role name for multi-FK tables (e.g., 'manager' in manager.ascendant) */
|
|
1108
|
+
readonly role?: string;
|
|
1109
|
+
/**
|
|
1110
|
+
* Chained traversals for multi-hop navigation (e.g., parent.parent.name → ['parent', 'parent']).
|
|
1111
|
+
* When present, overrides single `traversal` field. Each element generates a successive self-join.
|
|
1112
|
+
*/
|
|
1113
|
+
readonly traversals?: readonly PseudoColumnTraversal[];
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Generic function expression (e.g., now(), upper(name), coalesce(a, b)).
|
|
1117
|
+
* Used for SQL functions that are not aggregates.
|
|
1118
|
+
*/
|
|
1119
|
+
interface FunctionExpressionIntent {
|
|
1120
|
+
readonly kind: 'function';
|
|
1121
|
+
/** Function name (e.g., 'upper', 'now', 'coalesce') */
|
|
1122
|
+
readonly name: string;
|
|
1123
|
+
/** Function arguments */
|
|
1124
|
+
readonly args: readonly unknown[];
|
|
1125
|
+
/** Alias for result column */
|
|
1126
|
+
readonly as?: string | undefined;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Scalar subquery in SELECT clause.
|
|
1130
|
+
* Produces a single value from a nested query.
|
|
1131
|
+
*/
|
|
1132
|
+
interface SubqueryExpressionIntent {
|
|
1133
|
+
readonly kind: 'subquery';
|
|
1134
|
+
/** The nested query */
|
|
1135
|
+
readonly query: QueryIntent;
|
|
1136
|
+
/** Alias for result column */
|
|
1137
|
+
readonly as?: string | undefined;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Arithmetic expression (e.g., price * quantity).
|
|
1141
|
+
* Binary operation with left operand, operator, and right operand.
|
|
1142
|
+
*/
|
|
1143
|
+
interface ArithmeticExpressionIntent {
|
|
1144
|
+
readonly kind: 'arithmetic';
|
|
1145
|
+
/** Left operand (column name or value) */
|
|
1146
|
+
readonly left: string | number | unknown;
|
|
1147
|
+
/** Arithmetic operator */
|
|
1148
|
+
readonly operator: '+' | '-' | '*' | '/' | '%';
|
|
1149
|
+
/** Right operand (column name or value) */
|
|
1150
|
+
readonly right: string | number | unknown;
|
|
1151
|
+
/** Alias for result column */
|
|
1152
|
+
readonly as?: string | undefined;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Literal value expression: string, number, boolean, or null.
|
|
1156
|
+
* Used in CASE THEN/ELSE clauses for constant values.
|
|
1157
|
+
*/
|
|
1158
|
+
interface LiteralExpressionIntent {
|
|
1159
|
+
readonly kind: 'literal';
|
|
1160
|
+
/** The literal value */
|
|
1161
|
+
readonly value: string | number | boolean | null;
|
|
1162
|
+
/** Optional alias */
|
|
1163
|
+
readonly as?: string | undefined;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Comparison expression: left operator right.
|
|
1167
|
+
* Used in CASE WHEN conditions.
|
|
1168
|
+
*/
|
|
1169
|
+
interface ComparisonExpressionIntent {
|
|
1170
|
+
readonly kind: 'comparison';
|
|
1171
|
+
/** Left side column reference */
|
|
1172
|
+
readonly column: string;
|
|
1173
|
+
/** Comparison operator */
|
|
1174
|
+
readonly operator: '=' | '!=' | '<' | '>' | '<=' | '>=' | 'like';
|
|
1175
|
+
/** Right side value */
|
|
1176
|
+
readonly value: unknown;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* CASE expression: conditional logic in SELECT.
|
|
1180
|
+
* CASE WHEN condition THEN result [WHEN ...] [ELSE default] END
|
|
1181
|
+
*/
|
|
1182
|
+
interface CaseExpressionIntent {
|
|
1183
|
+
readonly kind: 'case';
|
|
1184
|
+
/** Array of WHEN-THEN pairs */
|
|
1185
|
+
readonly when: ReadonlyArray<{
|
|
1186
|
+
readonly condition: WhereIntent;
|
|
1187
|
+
readonly result: ExpressionIntent;
|
|
1188
|
+
}>;
|
|
1189
|
+
/** Optional ELSE clause */
|
|
1190
|
+
readonly else?: ExpressionIntent | undefined;
|
|
1191
|
+
/** Alias for result column */
|
|
1192
|
+
readonly as?: string | undefined;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* JSON path extraction: col->'key' or col->>'key' (chained paths supported).
|
|
1196
|
+
* Also used for function notation: json_extract(col, 'key'), json_extract_text(col, 'key').
|
|
1197
|
+
*/
|
|
1198
|
+
interface JsonExtractIntent {
|
|
1199
|
+
readonly kind: 'jsonExtract';
|
|
1200
|
+
readonly field: string;
|
|
1201
|
+
readonly path: readonly string[];
|
|
1202
|
+
/** 'json' = returns JSON value (->), 'text' = returns text (->>) */
|
|
1203
|
+
readonly mode: 'json' | 'text';
|
|
1204
|
+
readonly as?: string | undefined;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* JSON containment: col @> value (contains) or col <@ value (contained by).
|
|
1208
|
+
*/
|
|
1209
|
+
interface JsonContainsIntent {
|
|
1210
|
+
readonly kind: 'jsonContains';
|
|
1211
|
+
readonly field: string;
|
|
1212
|
+
readonly value: unknown;
|
|
1213
|
+
/** true = <@ (contained by), false = @> (contains) */
|
|
1214
|
+
readonly reversed: boolean;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* JSON key existence: col ? 'key'.
|
|
1218
|
+
*/
|
|
1219
|
+
interface JsonExistsIntent {
|
|
1220
|
+
readonly kind: 'jsonExists';
|
|
1221
|
+
readonly field: string;
|
|
1222
|
+
readonly key: string;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* JSON path extraction with array path: col #> '{a,b}' or col #>> '{a,b}'.
|
|
1226
|
+
*/
|
|
1227
|
+
interface JsonPathExtractIntent {
|
|
1228
|
+
readonly kind: 'jsonPathExtract';
|
|
1229
|
+
readonly field: string;
|
|
1230
|
+
/** PostgreSQL array literal path, e.g. '{a,b,c}' */
|
|
1231
|
+
readonly path: string;
|
|
1232
|
+
/** 'json' = returns JSON (#>), 'text' = returns text (#>>) */
|
|
1233
|
+
readonly mode: 'json' | 'text';
|
|
1234
|
+
readonly as?: string | undefined;
|
|
1235
|
+
}
|
|
1236
|
+
/** Custom binary operator expression (e.g., <=> for pgvector) */
|
|
1237
|
+
interface CustomOpExpressionIntent {
|
|
1238
|
+
readonly kind: 'customOp';
|
|
1239
|
+
readonly operator: string;
|
|
1240
|
+
readonly left: ExpressionIntent;
|
|
1241
|
+
readonly right: ExpressionIntent;
|
|
1242
|
+
readonly as?: string;
|
|
1243
|
+
}
|
|
1244
|
+
/** Custom function call (e.g., paradedb.score) */
|
|
1245
|
+
/** Represents a single ORDER BY entry inside an aggregate function. */
|
|
1246
|
+
interface AggOrderByArg {
|
|
1247
|
+
/** Discriminator to distinguish from regular expression args. */
|
|
1248
|
+
readonly __aggOrderBy: true;
|
|
1249
|
+
readonly field: string;
|
|
1250
|
+
readonly direction: 'asc' | 'desc';
|
|
1251
|
+
}
|
|
1252
|
+
interface CustomFnExpressionIntent {
|
|
1253
|
+
readonly kind: 'customFn';
|
|
1254
|
+
readonly name: string;
|
|
1255
|
+
readonly args: readonly ExpressionIntent[];
|
|
1256
|
+
readonly as?: string;
|
|
1257
|
+
/** FILTER (WHERE ...) clause for conditional aggregation */
|
|
1258
|
+
readonly filter?: WhereIntent | undefined;
|
|
1259
|
+
/** ORDER BY clause for ordered aggregates (e.g. array_agg(x ORDER BY y)) */
|
|
1260
|
+
readonly aggOrderBy?: readonly AggOrderByArg[] | undefined;
|
|
1261
|
+
}
|
|
1262
|
+
/** Column reference in custom expressions */
|
|
1263
|
+
interface RefExpressionIntent {
|
|
1264
|
+
readonly kind: 'ref';
|
|
1265
|
+
readonly column: string;
|
|
1266
|
+
}
|
|
1267
|
+
/** Parameterized value with automatic $N binding */
|
|
1268
|
+
interface ParamExpressionIntent {
|
|
1269
|
+
readonly kind: 'param';
|
|
1270
|
+
readonly value: unknown;
|
|
1271
|
+
}
|
|
1272
|
+
/** Type cast expression */
|
|
1273
|
+
interface CastExpressionIntent {
|
|
1274
|
+
readonly kind: 'cast';
|
|
1275
|
+
readonly expr: ExpressionIntent;
|
|
1276
|
+
readonly typeName: string;
|
|
1277
|
+
}
|
|
1278
|
+
/** Named argument in a function call: name => value */
|
|
1279
|
+
interface NamedArgExpressionIntent {
|
|
1280
|
+
readonly kind: 'namedArg';
|
|
1281
|
+
readonly name: string;
|
|
1282
|
+
readonly value: ExpressionIntent;
|
|
1283
|
+
}
|
|
1284
|
+
/** Star/wildcard expression (*) — used in COUNT(*), SELECT *, etc. */
|
|
1285
|
+
interface StarExpressionIntent {
|
|
1286
|
+
readonly kind: 'star';
|
|
1287
|
+
}
|
|
1288
|
+
/** PostgreSQL ARRAY constructor: ARRAY[item1, item2, ...] */
|
|
1289
|
+
interface ArrayExpressionIntent {
|
|
1290
|
+
readonly kind: 'array';
|
|
1291
|
+
readonly elements: readonly ExpressionIntent[];
|
|
1292
|
+
readonly as?: string;
|
|
1293
|
+
}
|
|
1294
|
+
/** Unary operator expression (e.g., NOT, -, ~) */
|
|
1295
|
+
interface UnaryExpressionIntent {
|
|
1296
|
+
readonly kind: 'unary';
|
|
1297
|
+
readonly operator: string;
|
|
1298
|
+
readonly operand: ExpressionIntent;
|
|
1299
|
+
readonly as?: string;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Expression intent union type - computed/derived values in SELECT
|
|
1303
|
+
* Extensible for future expression types
|
|
1304
|
+
*/
|
|
1305
|
+
type ExpressionIntent = ColumnExpressionIntent | CoalesceExpressionIntent | RawExpressionIntent | ColumnAliasIntent | RelationColumnIntent | WindowIntent | AggregateExpressionIntent | PseudoColumnExpressionIntent | FunctionExpressionIntent | SubqueryExpressionIntent | ArithmeticExpressionIntent | LiteralExpressionIntent | ComparisonExpressionIntent | CaseExpressionIntent | JsonExtractIntent | JsonContainsIntent | JsonExistsIntent | JsonPathExtractIntent | CustomOpExpressionIntent | CustomFnExpressionIntent | RefExpressionIntent | ParamExpressionIntent | CastExpressionIntent | UnaryExpressionIntent | NamedArgExpressionIntent | StarExpressionIntent | ArrayExpressionIntent;
|
|
1306
|
+
/**
|
|
1307
|
+
* Window function types for OVER clause analytics.
|
|
1308
|
+
* - Ranking: row_number, rank, dense_rank (no field required)
|
|
1309
|
+
* - Aggregate: sum, avg, count, min, max (field required)
|
|
1310
|
+
* - Offset: lag, lead (field required, offset/default deferred to P3+)
|
|
1311
|
+
*/
|
|
1312
|
+
type WindowFunction = 'row_number' | 'rank' | 'dense_rank' | 'sum' | 'avg' | 'count' | 'min' | 'max' | 'lag' | 'lead';
|
|
1313
|
+
/**
|
|
1314
|
+
* Order specification for window OVER clause.
|
|
1315
|
+
*/
|
|
1316
|
+
interface WindowOrderBy {
|
|
1317
|
+
readonly field: string;
|
|
1318
|
+
readonly direction?: 'asc' | 'desc';
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Window function intent for analytics over partitions.
|
|
1322
|
+
* Produces SQL like: ROW_NUMBER() OVER (PARTITION BY x ORDER BY y) AS alias
|
|
1323
|
+
*
|
|
1324
|
+
* @example Row numbering
|
|
1325
|
+
* {
|
|
1326
|
+
* kind: 'window',
|
|
1327
|
+
* function: 'row_number',
|
|
1328
|
+
* alias: 'rn',
|
|
1329
|
+
* over: { orderBy: [{ field: 'created_at', direction: 'desc' }] }
|
|
1330
|
+
* }
|
|
1331
|
+
*
|
|
1332
|
+
* @example Running total
|
|
1333
|
+
* {
|
|
1334
|
+
* kind: 'window',
|
|
1335
|
+
* function: 'sum',
|
|
1336
|
+
* field: 'amount',
|
|
1337
|
+
* alias: 'running_total',
|
|
1338
|
+
* over: { partitionBy: ['account_id'], orderBy: [{ field: 'date' }] }
|
|
1339
|
+
* }
|
|
1340
|
+
*/
|
|
1341
|
+
interface WindowIntent {
|
|
1342
|
+
readonly kind: 'window';
|
|
1343
|
+
/** Window function to apply */
|
|
1344
|
+
readonly function: WindowFunction;
|
|
1345
|
+
/** Field for aggregate/offset functions (required for sum/avg/count/min/max/lag/lead) */
|
|
1346
|
+
readonly field?: string | undefined;
|
|
1347
|
+
/** Result column alias (required) */
|
|
1348
|
+
readonly alias: string;
|
|
1349
|
+
/** Offset for lag/lead (default: 1 in PostgreSQL) */
|
|
1350
|
+
readonly offset?: number | undefined;
|
|
1351
|
+
/** Default value for lag/lead when row doesn't exist */
|
|
1352
|
+
readonly defaultValue?: unknown;
|
|
1353
|
+
/** OVER clause specification */
|
|
1354
|
+
readonly over: {
|
|
1355
|
+
/** PARTITION BY columns (optional) */
|
|
1356
|
+
readonly partitionBy?: readonly string[] | undefined;
|
|
1357
|
+
/** ORDER BY specification (optional but recommended for ranking) */
|
|
1358
|
+
readonly orderBy?: readonly WindowOrderBy[] | undefined;
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Ranking window functions (no field required)
|
|
1363
|
+
*/
|
|
1364
|
+
type RankingWindowFunction = 'row_number' | 'rank' | 'dense_rank';
|
|
1365
|
+
/**
|
|
1366
|
+
* Aggregate window functions (field required)
|
|
1367
|
+
*/
|
|
1368
|
+
type AggregateWindowFunction = 'sum' | 'avg' | 'count' | 'min' | 'max';
|
|
1369
|
+
/**
|
|
1370
|
+
* Offset window functions (field required, offset/default deferred)
|
|
1371
|
+
*/
|
|
1372
|
+
type OffsetWindowFunction = 'lag' | 'lead';
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* @module intent/include-intent
|
|
1376
|
+
* Include intent types for relation loading and ordering.
|
|
1377
|
+
*/
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* CLI-012c: Options for recursive include (self-referential relations only).
|
|
1381
|
+
*
|
|
1382
|
+
* Enables WITH RECURSIVE CTE generation for hierarchical data traversal
|
|
1383
|
+
* (org charts, category trees, bill of materials).
|
|
1384
|
+
*
|
|
1385
|
+
* For complex recursive queries (bidirectional, custom traversal expressions),
|
|
1386
|
+
* use `RecursiveIntent` directly.
|
|
1387
|
+
*
|
|
1388
|
+
* Note: Named `IncludeRecursiveOptions` to avoid conflict with DX-layer
|
|
1389
|
+
* `RecursiveIncludeOptions` in dx/types.ts.
|
|
1390
|
+
*/
|
|
1391
|
+
interface IncludeRecursiveOptions {
|
|
1392
|
+
/**
|
|
1393
|
+
* Maximum recursion depth (default: 100).
|
|
1394
|
+
* Safety limit to prevent infinite recursion.
|
|
1395
|
+
* @example maxDepth: 10 - fetch up to 10 levels deep
|
|
1396
|
+
*/
|
|
1397
|
+
readonly maxDepth?: number;
|
|
1398
|
+
/**
|
|
1399
|
+
* Track additional metadata during recursion.
|
|
1400
|
+
*/
|
|
1401
|
+
readonly track?: {
|
|
1402
|
+
/**
|
|
1403
|
+
* Include depth counter (starts at 0 for root nodes).
|
|
1404
|
+
* Set to true for default column name 'depth', or object for custom alias.
|
|
1405
|
+
*/
|
|
1406
|
+
readonly depth?: boolean | {
|
|
1407
|
+
readonly as?: string;
|
|
1408
|
+
};
|
|
1409
|
+
/**
|
|
1410
|
+
* Include path array for cycle detection/debugging.
|
|
1411
|
+
* Set to true for default column name 'path', or object for custom alias.
|
|
1412
|
+
*/
|
|
1413
|
+
readonly path?: boolean | {
|
|
1414
|
+
readonly as?: string;
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
/**
|
|
1418
|
+
* Foreign key column for recursion.
|
|
1419
|
+
* If not specified, will be inferred from relation definition.
|
|
1420
|
+
* @example 'parentId' for self-referential category tree
|
|
1421
|
+
*/
|
|
1422
|
+
readonly foreignKey?: string;
|
|
1423
|
+
}
|
|
1424
|
+
interface IncludeIntent {
|
|
1425
|
+
/** Relation name to include */
|
|
1426
|
+
readonly relation: string;
|
|
1427
|
+
/** What columns to select from related records */
|
|
1428
|
+
readonly select?: SelectIntent | undefined;
|
|
1429
|
+
/** Filter conditions on related records */
|
|
1430
|
+
readonly where?: WhereIntent | undefined;
|
|
1431
|
+
/** Nested includes for deep loading */
|
|
1432
|
+
readonly include?: readonly IncludeIntent[] | undefined;
|
|
1433
|
+
/**
|
|
1434
|
+
* Explicit relation path for disambiguation.
|
|
1435
|
+
* Use when multiple relations exist between same tables.
|
|
1436
|
+
* @example 'author' or 'editor' when User has both relations to Post
|
|
1437
|
+
*/
|
|
1438
|
+
readonly via?: string | undefined;
|
|
1439
|
+
/**
|
|
1440
|
+
* Maximum number of related records to include per parent.
|
|
1441
|
+
* Only effective with LATERAL JOIN strategy (PostgreSQL/DuckDB/MSSQL).
|
|
1442
|
+
* @example limit: 5 - fetch at most 5 related records per parent
|
|
1443
|
+
*/
|
|
1444
|
+
readonly limit?: number | undefined;
|
|
1445
|
+
/**
|
|
1446
|
+
* Order by for related records (used with limit).
|
|
1447
|
+
* @example orderBy: [{ field: 'createdAt', direction: 'desc' }]
|
|
1448
|
+
*/
|
|
1449
|
+
readonly orderBy?: readonly OrderByIntent[] | undefined;
|
|
1450
|
+
/**
|
|
1451
|
+
* CLI-012c: Enable recursive CTE for self-referential relations.
|
|
1452
|
+
* Only valid when relation.source === relation.target (e.g., categories → parent).
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* include: [{
|
|
1456
|
+
* relation: 'children',
|
|
1457
|
+
* recursive: { maxDepth: 10, track: { depth: true } }
|
|
1458
|
+
* }]
|
|
1459
|
+
*/
|
|
1460
|
+
readonly recursive?: IncludeRecursiveOptions | undefined;
|
|
1461
|
+
/**
|
|
1462
|
+
* NQL v2.1: Override include output strategy for this relation.
|
|
1463
|
+
*
|
|
1464
|
+
* This is an **output strategy** (flat vs nested), not an implementation strategy.
|
|
1465
|
+
* The planner decides the best implementation (join, subquery, lateral, cte)
|
|
1466
|
+
* based on relation type and dialect capabilities.
|
|
1467
|
+
*
|
|
1468
|
+
* - 'auto': Let planner decide freely, including json_agg (default)
|
|
1469
|
+
* - 'flat': Exclude json_agg from candidates; planner picks best flat strategy
|
|
1470
|
+
*
|
|
1471
|
+
* @example
|
|
1472
|
+
* // NQL: orders | select *, customer.* | flat
|
|
1473
|
+
* // Results in: include: [{ relation: 'customer', strategy: 'flat' }]
|
|
1474
|
+
* // Planner then picks lateral, join, subquery, or cte (never json_agg)
|
|
1475
|
+
*/
|
|
1476
|
+
readonly strategy?: 'auto' | 'flat' | undefined;
|
|
1477
|
+
/**
|
|
1478
|
+
* Join type for the include when using the 'join' strategy.
|
|
1479
|
+
* - 'left' (default): LEFT JOIN — all root rows returned, NULL for unmatched relations
|
|
1480
|
+
* - 'inner': INNER JOIN — only root rows WITH a matching related record are returned
|
|
1481
|
+
*
|
|
1482
|
+
* Forces the 'join' include strategy (overrides auto-selection).
|
|
1483
|
+
*
|
|
1484
|
+
* @example
|
|
1485
|
+
* // Only return symbols that have a matching file
|
|
1486
|
+
* include('file', { join: 'inner' })
|
|
1487
|
+
* // → INNER JOIN files ON files.id = symbols.file_id
|
|
1488
|
+
* // → Only symbols WITH a matching file are returned
|
|
1489
|
+
*/
|
|
1490
|
+
readonly join?: 'inner' | 'left' | undefined;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* OrderBy intent - sort results
|
|
1494
|
+
*/
|
|
1495
|
+
interface OrderByIntent {
|
|
1496
|
+
/** Field name to sort by */
|
|
1497
|
+
readonly field?: string;
|
|
1498
|
+
/** Expression to sort by (alternative to field) */
|
|
1499
|
+
readonly expression?: ExpressionIntent;
|
|
1500
|
+
/** Sort direction */
|
|
1501
|
+
readonly direction: SortDirection;
|
|
1502
|
+
/**
|
|
1503
|
+
* Where to place NULL values
|
|
1504
|
+
* @default 'last' for 'asc', 'first' for 'desc' (database default)
|
|
1505
|
+
*/
|
|
1506
|
+
readonly nulls?: NullsPosition;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Row-level locking intent for SELECT queries (E15).
|
|
1511
|
+
*
|
|
1512
|
+
* Maps to PostgreSQL's FOR UPDATE/SHARE/NO KEY UPDATE/KEY SHARE
|
|
1513
|
+
* with SKIP LOCKED / NOWAIT wait policies.
|
|
1514
|
+
*/
|
|
1515
|
+
/** Row-level lock strength for SELECT queries. */
|
|
1516
|
+
type LockStrength = 'forUpdate' | 'forNoKeyUpdate' | 'forShare' | 'forKeyShare';
|
|
1517
|
+
/**
|
|
1518
|
+
* Wait policy when a lock conflict is encountered.
|
|
1519
|
+
*
|
|
1520
|
+
* - `block`: Wait indefinitely (default PostgreSQL behavior)
|
|
1521
|
+
* - `skipLocked`: Skip already-locked rows (job queue pattern)
|
|
1522
|
+
* - `noWait`: Error immediately if any row is locked
|
|
1523
|
+
*/
|
|
1524
|
+
type LockWaitPolicy = 'block' | 'skipLocked' | 'noWait';
|
|
1525
|
+
/** Declarative lock intent for SELECT queries. */
|
|
1526
|
+
interface LockIntent {
|
|
1527
|
+
readonly strength: LockStrength;
|
|
1528
|
+
readonly waitPolicy: LockWaitPolicy;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* @module intent/query-intent
|
|
1533
|
+
* Query intent - complete query definition, main entry point for the intent AST.
|
|
1534
|
+
*/
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* BatchValues join payload — the data needed to compile an unnest() JOIN.
|
|
1538
|
+
* Embedded inside JoinIntent when the user calls .join(batchValuesRef, ...).
|
|
1539
|
+
*/
|
|
1540
|
+
type BatchValuesJoinPayload = {
|
|
1541
|
+
/** Column-major data arrays: one array per column */
|
|
1542
|
+
readonly data: readonly unknown[][];
|
|
1543
|
+
/** Column names for the unnest alias clause */
|
|
1544
|
+
readonly columns: readonly string[];
|
|
1545
|
+
/** PostgreSQL type names for CAST ($1::type[]) */
|
|
1546
|
+
readonly types: readonly string[];
|
|
1547
|
+
/** SQL alias (carries over from BatchValuesRef.alias) */
|
|
1548
|
+
readonly alias: string;
|
|
1549
|
+
/** WITH ORDINALITY flag */
|
|
1550
|
+
readonly ordinality: boolean;
|
|
1551
|
+
};
|
|
1552
|
+
/**
|
|
1553
|
+
* Join intent — represents a SQL JOIN clause on the root query.
|
|
1554
|
+
*
|
|
1555
|
+
* Two discrimination modes (based on `on` presence):
|
|
1556
|
+
* - **Relation mode** (`relation` set, no `on`): FK auto-resolved, like `include` but flat (no hydration).
|
|
1557
|
+
* - **Table mode** (`table` set, `on` required): Explicit table name + ON condition. Required for self-joins.
|
|
1558
|
+
*
|
|
1559
|
+
* @example
|
|
1560
|
+
* ```typescript
|
|
1561
|
+
* // Relation mode — FK auto-resolved
|
|
1562
|
+
* orm.from(calls).join('caller')
|
|
1563
|
+
* orm.from(calls).join('callerFile', { type: 'left' })
|
|
1564
|
+
*
|
|
1565
|
+
* // Table mode — explicit ON condition
|
|
1566
|
+
* orm.from(embeddings).join('embeddings', {
|
|
1567
|
+
* on: lt(ref('embeddings.id'), ref('e2.id')),
|
|
1568
|
+
* as: 'e2',
|
|
1569
|
+
* type: 'inner',
|
|
1570
|
+
* })
|
|
1571
|
+
* ```
|
|
1572
|
+
*/
|
|
1573
|
+
type JoinIntent = {
|
|
1574
|
+
/** FK-based join — relation name resolved to table + FK automatically */
|
|
1575
|
+
readonly relation: string;
|
|
1576
|
+
readonly table?: never;
|
|
1577
|
+
readonly batchValues?: never;
|
|
1578
|
+
readonly on?: never;
|
|
1579
|
+
readonly alias?: string;
|
|
1580
|
+
readonly type: 'inner' | 'left';
|
|
1581
|
+
} | {
|
|
1582
|
+
/** Explicit table join — ON condition required */
|
|
1583
|
+
readonly table: string;
|
|
1584
|
+
readonly relation?: never;
|
|
1585
|
+
readonly batchValues?: never;
|
|
1586
|
+
readonly on: WhereIntent;
|
|
1587
|
+
readonly alias?: string;
|
|
1588
|
+
readonly type: 'inner' | 'left';
|
|
1589
|
+
} | {
|
|
1590
|
+
/** BatchValues join — unnest($N::type[],...) AS alias(...) with explicit ON */
|
|
1591
|
+
readonly batchValues: BatchValuesJoinPayload;
|
|
1592
|
+
readonly relation?: never;
|
|
1593
|
+
readonly table?: never;
|
|
1594
|
+
readonly on: WhereIntent;
|
|
1595
|
+
readonly alias?: string;
|
|
1596
|
+
readonly type: 'inner' | 'left';
|
|
1597
|
+
};
|
|
1598
|
+
/**
|
|
1599
|
+
* Query intent - complete query definition
|
|
1600
|
+
* Main entry point for the intent AST
|
|
1601
|
+
*/
|
|
1602
|
+
interface QueryIntent {
|
|
1603
|
+
/** Query type - currently only 'select' supported */
|
|
1604
|
+
readonly type: 'select';
|
|
1605
|
+
/** Target table name */
|
|
1606
|
+
readonly from: string;
|
|
1607
|
+
/** Columns to retrieve */
|
|
1608
|
+
readonly select?: SelectIntent;
|
|
1609
|
+
/** Filter conditions */
|
|
1610
|
+
readonly where?: WhereIntent;
|
|
1611
|
+
/** Relations to include */
|
|
1612
|
+
readonly include?: readonly IncludeIntent[];
|
|
1613
|
+
/** Sort order */
|
|
1614
|
+
readonly orderBy?: readonly OrderByIntent[];
|
|
1615
|
+
/**
|
|
1616
|
+
* Fields to group by for aggregate queries.
|
|
1617
|
+
* When specified, SELECT must include only grouped fields and aggregates.
|
|
1618
|
+
*/
|
|
1619
|
+
readonly groupBy?: readonly string[];
|
|
1620
|
+
/**
|
|
1621
|
+
* Filter on aggregate results (applied after GROUP BY).
|
|
1622
|
+
* Similar to WHERE but operates on aggregated values.
|
|
1623
|
+
*/
|
|
1624
|
+
readonly having?: WhereIntent;
|
|
1625
|
+
/**
|
|
1626
|
+
* Whether to apply SELECT DISTINCT to deduplicate rows.
|
|
1627
|
+
*/
|
|
1628
|
+
readonly distinct?: boolean;
|
|
1629
|
+
/**
|
|
1630
|
+
* Columns for PostgreSQL DISTINCT ON (...) clause.
|
|
1631
|
+
* Produces: SELECT DISTINCT ON ("col1", "col2") ...
|
|
1632
|
+
* Takes precedence over `distinct` when set.
|
|
1633
|
+
*/
|
|
1634
|
+
readonly distinctOn?: readonly string[];
|
|
1635
|
+
/** Maximum number of rows */
|
|
1636
|
+
readonly limit?: number;
|
|
1637
|
+
/** Number of rows to skip */
|
|
1638
|
+
readonly offset?: number;
|
|
1639
|
+
/**
|
|
1640
|
+
* When true, the adapter wraps the query in SELECT EXISTS(...).
|
|
1641
|
+
* The inner SELECT list is replaced with `1` and the result is `{ exists: boolean }`.
|
|
1642
|
+
*/
|
|
1643
|
+
readonly existsWrap?: boolean;
|
|
1644
|
+
/**
|
|
1645
|
+
* Row-level lock for SELECT queries (e.g., FOR UPDATE SKIP LOCKED).
|
|
1646
|
+
* Only valid in SELECT context — incompatible with GROUP BY, set operations.
|
|
1647
|
+
*/
|
|
1648
|
+
readonly lock?: LockIntent;
|
|
1649
|
+
/**
|
|
1650
|
+
* Explicit SQL JOIN clauses (non-hydrating, flat result).
|
|
1651
|
+
* Columns from joined tables appear in the flat result row.
|
|
1652
|
+
* Two modes: relation-based (FK auto-resolved) or table-based (explicit ON condition).
|
|
1653
|
+
*/
|
|
1654
|
+
readonly joins?: readonly JoinIntent[];
|
|
1655
|
+
/**
|
|
1656
|
+
* When present, the FROM clause is a BatchValues unnest() source instead of a table.
|
|
1657
|
+
* `from` still holds the alias string for column references.
|
|
1658
|
+
*/
|
|
1659
|
+
readonly batchValuesSource?: BatchValuesJoinPayload;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Set operation type: SQL standard set operations.
|
|
1663
|
+
*/
|
|
1664
|
+
type SetOperationType = 'union' | 'intersect' | 'except';
|
|
1665
|
+
/**
|
|
1666
|
+
* Set operation that combines two queries.
|
|
1667
|
+
* The result is a tree: each side can itself be a SetOperationIntent.
|
|
1668
|
+
*
|
|
1669
|
+
* @example
|
|
1670
|
+
* ```
|
|
1671
|
+
* users | select name | union (admins | select name)
|
|
1672
|
+
* → { op: 'union', all: false, left: QueryIntent, right: QueryIntent }
|
|
1673
|
+
* ```
|
|
1674
|
+
*/
|
|
1675
|
+
interface SetOperationIntent {
|
|
1676
|
+
readonly kind: 'setOperation';
|
|
1677
|
+
readonly op: SetOperationType;
|
|
1678
|
+
readonly all: boolean;
|
|
1679
|
+
readonly left: QueryIntent | SetOperationIntent;
|
|
1680
|
+
readonly right: QueryIntent | SetOperationIntent;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* CTE backed by unnest() arrays, optionally with WITH ORDINALITY for 0-based indexing.
|
|
1685
|
+
*
|
|
1686
|
+
* Generates:
|
|
1687
|
+
* SELECT t."col1", t."col2", (t.ordinality - 1) AS "idx"
|
|
1688
|
+
* FROM unnest(CAST($1 AS type[]), CAST($2 AS type[])) WITH ORDINALITY AS t("col1", "col2", ordinality)
|
|
1689
|
+
*/
|
|
1690
|
+
interface UnnestCteIntent {
|
|
1691
|
+
readonly kind: 'unnestCte';
|
|
1692
|
+
readonly name: string;
|
|
1693
|
+
/** Column name → array of values. All arrays must have the same length. */
|
|
1694
|
+
readonly columns: Record<string, readonly unknown[]>;
|
|
1695
|
+
/** Optional column name for the 0-based ordinality index. */
|
|
1696
|
+
readonly indexColumn?: string;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Full CTE query: one or more CTE definitions + an outer query.
|
|
1700
|
+
*/
|
|
1701
|
+
/**
|
|
1702
|
+
* CTE backed by explicit base and step QueryIntent instances (WITH RECURSIVE).
|
|
1703
|
+
*
|
|
1704
|
+
* Used by `orm.recursive(name, { base, step })` to build arbitrary
|
|
1705
|
+
* WITH RECURSIVE queries from query builder instances.
|
|
1706
|
+
*
|
|
1707
|
+
* Generates:
|
|
1708
|
+
* WITH RECURSIVE "name" AS (
|
|
1709
|
+
* <base SELECT>
|
|
1710
|
+
* UNION ALL -- or UNION when unionAll: false
|
|
1711
|
+
* <step SELECT> [WHERE depth_col < maxDepth]
|
|
1712
|
+
* )
|
|
1713
|
+
*/
|
|
1714
|
+
interface RawCteIntent {
|
|
1715
|
+
readonly kind: 'rawCte';
|
|
1716
|
+
readonly name: string;
|
|
1717
|
+
readonly base: QueryIntent;
|
|
1718
|
+
readonly step: QueryIntent;
|
|
1719
|
+
/** When true, use UNION ALL (default). When false, use UNION (deduplicates). */
|
|
1720
|
+
readonly unionAll: boolean;
|
|
1721
|
+
/** Optional depth guard: inject WHERE <depthColumn> < maxDepth in the recursive step. */
|
|
1722
|
+
readonly maxDepth?: number;
|
|
1723
|
+
/** Column name tracking depth in the step query, for maxDepth injection. */
|
|
1724
|
+
readonly depthColumn?: string;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* CTE backed by a standard NQL query (non-recursive, non-unnest).
|
|
1728
|
+
*
|
|
1729
|
+
* Generated by `WITH name AS (query)` syntax in NQL.
|
|
1730
|
+
* Produces: `name AS (SELECT ...)`
|
|
1731
|
+
*/
|
|
1732
|
+
interface SimpleCteIntent {
|
|
1733
|
+
readonly kind: 'simpleCte';
|
|
1734
|
+
readonly name: string;
|
|
1735
|
+
readonly query: QueryIntent;
|
|
1736
|
+
}
|
|
1737
|
+
interface CteQueryIntent {
|
|
1738
|
+
readonly kind: 'cteQuery';
|
|
1739
|
+
readonly ctes: readonly (UnnestCteIntent | RawCteIntent | SimpleCteIntent)[];
|
|
1740
|
+
readonly query: QueryIntent;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* @module intent/mutation-intent
|
|
1745
|
+
* Mutation intent types for Insert, Update, Delete, Upsert (DX-010).
|
|
1746
|
+
*/
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Insert intent - insert one or more rows into a table.
|
|
1750
|
+
* @example { type: 'insert', table: 'users', values: [{ name: 'Alice' }] }
|
|
1751
|
+
*/
|
|
1752
|
+
interface InsertIntent {
|
|
1753
|
+
readonly type: 'insert';
|
|
1754
|
+
/** Target table name */
|
|
1755
|
+
readonly table: string;
|
|
1756
|
+
/** Values to insert (single object or array for bulk insert) */
|
|
1757
|
+
readonly values: readonly Record<string, unknown>[];
|
|
1758
|
+
/**
|
|
1759
|
+
* Columns to return from inserted rows (DX-026).
|
|
1760
|
+
* Requires adapter capability: supportsReturning
|
|
1761
|
+
* @example ['id', 'created_at']
|
|
1762
|
+
*/
|
|
1763
|
+
readonly returning?: readonly string[];
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Insert-from intent - insert rows from a SELECT query.
|
|
1767
|
+
* @example { type: 'insert_from', table: 'archived_users', source: 'users', where: {...} }
|
|
1768
|
+
*/
|
|
1769
|
+
interface InsertFromIntent {
|
|
1770
|
+
readonly type: 'insert_from';
|
|
1771
|
+
/** Target table to insert into */
|
|
1772
|
+
readonly table: string;
|
|
1773
|
+
/** Source table to select from (table name or bound reference) */
|
|
1774
|
+
readonly source: string;
|
|
1775
|
+
/** Optional source query (when source is a bound reference from `| bind`) */
|
|
1776
|
+
readonly sourceQuery?: QueryIntent | undefined;
|
|
1777
|
+
/** Optional column mapping (defaults to same column names) */
|
|
1778
|
+
readonly columns?: readonly string[] | undefined;
|
|
1779
|
+
/** Filter condition for source rows */
|
|
1780
|
+
readonly where?: WhereIntent | undefined;
|
|
1781
|
+
/** Limit number of rows to insert */
|
|
1782
|
+
readonly limit?: number | undefined;
|
|
1783
|
+
/**
|
|
1784
|
+
* Columns to return from inserted rows (DX-026).
|
|
1785
|
+
* Requires adapter capability: supportsReturning
|
|
1786
|
+
* @example ['id', 'created_at']
|
|
1787
|
+
*/
|
|
1788
|
+
readonly returning?: readonly string[];
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Upsert from intent - bulk upsert by selecting rows from a source table or CTE.
|
|
1792
|
+
* Produces: INSERT INTO target SELECT ... FROM source ON CONFLICT (columns) DO UPDATE SET ...
|
|
1793
|
+
* @example upsert into authors on id from counts
|
|
1794
|
+
*/
|
|
1795
|
+
interface UpsertFromIntent {
|
|
1796
|
+
readonly type: 'upsert_from';
|
|
1797
|
+
/** Target table to upsert into */
|
|
1798
|
+
readonly table: string;
|
|
1799
|
+
/** Source table or bound CTE reference */
|
|
1800
|
+
readonly source: string;
|
|
1801
|
+
/** Optional source query (when source is a bound reference from `| bind`) */
|
|
1802
|
+
readonly sourceQuery?: QueryIntent | undefined;
|
|
1803
|
+
/** Conflict target columns for ON CONFLICT */
|
|
1804
|
+
readonly conflictColumns: readonly string[];
|
|
1805
|
+
/** Optional column mapping (defaults to same column names) */
|
|
1806
|
+
readonly columns?: readonly string[] | undefined;
|
|
1807
|
+
/** Filter condition for source rows */
|
|
1808
|
+
readonly where?: WhereIntent | undefined;
|
|
1809
|
+
/** Limit number of rows */
|
|
1810
|
+
readonly limit?: number | undefined;
|
|
1811
|
+
/**
|
|
1812
|
+
* Columns to return from affected rows.
|
|
1813
|
+
* Requires adapter capability: supportsReturning
|
|
1814
|
+
*/
|
|
1815
|
+
readonly returning?: readonly string[];
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Update intent - update rows matching a condition.
|
|
1819
|
+
* @example { type: 'update', table: 'users', set: { name: 'Bob' }, where: ... }
|
|
1820
|
+
*/
|
|
1821
|
+
interface UpdateIntent {
|
|
1822
|
+
readonly type: 'update';
|
|
1823
|
+
/** Target table name */
|
|
1824
|
+
readonly table: string;
|
|
1825
|
+
/** Fields to update with new values */
|
|
1826
|
+
readonly set: Record<string, unknown>;
|
|
1827
|
+
/** Filter condition (required for safety, unless allowAll is true) */
|
|
1828
|
+
readonly where?: WhereIntent;
|
|
1829
|
+
/** Explicitly allow update without WHERE (for updateAll) */
|
|
1830
|
+
readonly allowAll?: boolean;
|
|
1831
|
+
/**
|
|
1832
|
+
* Columns to return from updated rows (DX-026).
|
|
1833
|
+
* Requires adapter capability: supportsReturning
|
|
1834
|
+
* @example ['id', 'updated_at']
|
|
1835
|
+
*/
|
|
1836
|
+
readonly returning?: readonly string[];
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Delete intent - delete rows matching a condition.
|
|
1840
|
+
* @example { type: 'delete', table: 'users', where: ... }
|
|
1841
|
+
*/
|
|
1842
|
+
interface DeleteIntent {
|
|
1843
|
+
readonly type: 'delete';
|
|
1844
|
+
/** Target table name */
|
|
1845
|
+
readonly table: string;
|
|
1846
|
+
/** Filter condition (required for safety, unless allowAll is true) */
|
|
1847
|
+
readonly where?: WhereIntent;
|
|
1848
|
+
/** Explicitly allow delete without WHERE (for deleteAll) */
|
|
1849
|
+
readonly allowAll?: boolean;
|
|
1850
|
+
/**
|
|
1851
|
+
* Relations to cascade delete.
|
|
1852
|
+
* - undefined: no cascade
|
|
1853
|
+
* - true: cascade all relations
|
|
1854
|
+
* - string[]: cascade specific relations
|
|
1855
|
+
*/
|
|
1856
|
+
readonly cascade?: boolean | readonly string[];
|
|
1857
|
+
/**
|
|
1858
|
+
* Columns to return from deleted rows (DX-026).
|
|
1859
|
+
* Requires adapter capability: supportsReturning
|
|
1860
|
+
* @example ['id', 'email']
|
|
1861
|
+
*/
|
|
1862
|
+
readonly returning?: readonly string[];
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Upsert conflict target - specifies which columns determine uniqueness.
|
|
1866
|
+
*/
|
|
1867
|
+
type UpsertConflictTarget = {
|
|
1868
|
+
readonly columns: readonly string[];
|
|
1869
|
+
} | {
|
|
1870
|
+
readonly constraint: string;
|
|
1871
|
+
};
|
|
1872
|
+
/**
|
|
1873
|
+
* Upsert conflict action - what to do when conflict occurs.
|
|
1874
|
+
*/
|
|
1875
|
+
type UpsertConflictAction = {
|
|
1876
|
+
readonly type: 'doNothing';
|
|
1877
|
+
} | {
|
|
1878
|
+
readonly type: 'doUpdate';
|
|
1879
|
+
/** Fields to update on conflict. If undefined, updates all non-conflict columns. */
|
|
1880
|
+
readonly set?: Record<string, unknown>;
|
|
1881
|
+
/** Optional WHERE clause for conditional update */
|
|
1882
|
+
readonly where?: WhereIntent;
|
|
1883
|
+
};
|
|
1884
|
+
/**
|
|
1885
|
+
* Upsert intent - insert or update on conflict (DX-026).
|
|
1886
|
+
* Implements INSERT ... ON CONFLICT ... DO UPDATE/NOTHING pattern.
|
|
1887
|
+
*
|
|
1888
|
+
* @example doNothing
|
|
1889
|
+
* {
|
|
1890
|
+
* type: 'upsert',
|
|
1891
|
+
* table: 'users',
|
|
1892
|
+
* values: [{ email: 'a@b.com', name: 'Alice' }],
|
|
1893
|
+
* onConflict: { columns: ['email'] },
|
|
1894
|
+
* action: { type: 'doNothing' }
|
|
1895
|
+
* }
|
|
1896
|
+
*
|
|
1897
|
+
* @example doUpdate
|
|
1898
|
+
* {
|
|
1899
|
+
* type: 'upsert',
|
|
1900
|
+
* table: 'users',
|
|
1901
|
+
* values: [{ email: 'a@b.com', name: 'Alice' }],
|
|
1902
|
+
* onConflict: { columns: ['email'] },
|
|
1903
|
+
* action: { type: 'doUpdate', set: { name: 'Alice Updated' } }
|
|
1904
|
+
* }
|
|
1905
|
+
*/
|
|
1906
|
+
interface UpsertIntent {
|
|
1907
|
+
readonly type: 'upsert';
|
|
1908
|
+
/** Target table name */
|
|
1909
|
+
readonly table: string;
|
|
1910
|
+
/** Values to insert (single object or array for bulk upsert) */
|
|
1911
|
+
readonly values: readonly Record<string, unknown>[];
|
|
1912
|
+
/** Conflict target - columns or constraint name */
|
|
1913
|
+
readonly onConflict: UpsertConflictTarget;
|
|
1914
|
+
/** Action to take on conflict */
|
|
1915
|
+
readonly action: UpsertConflictAction;
|
|
1916
|
+
/**
|
|
1917
|
+
* Columns to return from affected rows (DX-026).
|
|
1918
|
+
* Requires adapter capability: supportsReturning
|
|
1919
|
+
* @example ['id', 'created_at', 'updated_at']
|
|
1920
|
+
*/
|
|
1921
|
+
readonly returning?: readonly string[];
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Union of all mutation intents.
|
|
1925
|
+
*/
|
|
1926
|
+
/**
|
|
1927
|
+
* Batch update intent - update multiple rows using unnest FROM strategy.
|
|
1928
|
+
* Generates: UPDATE "table" SET "col" = t."col" FROM unnest($1::type[], ...) AS t("match", "col") WHERE "table"."match" = t."match"
|
|
1929
|
+
*
|
|
1930
|
+
* @example { type: 'batchUpdate', table: 'calls', matchColumns: ['id'], updates: [{id: 1, callee_id: 42}] }
|
|
1931
|
+
*/
|
|
1932
|
+
interface BatchUpdateIntent {
|
|
1933
|
+
readonly type: 'batchUpdate';
|
|
1934
|
+
/** Target table name */
|
|
1935
|
+
readonly table: string;
|
|
1936
|
+
/** Column(s) used to match rows (WHERE clause join condition) */
|
|
1937
|
+
readonly matchColumns: readonly string[];
|
|
1938
|
+
/** Array of row objects containing match + update column values */
|
|
1939
|
+
readonly updates: readonly Record<string, unknown>[];
|
|
1940
|
+
/** Optional scalar values applied to ALL rows (non-array SET clause) */
|
|
1941
|
+
readonly scalarSet?: Record<string, unknown>;
|
|
1942
|
+
/** Columns to return from updated rows (RETURNING clause) */
|
|
1943
|
+
readonly returning?: readonly string[];
|
|
1944
|
+
/** Optional WHERE guard applied in addition to match columns (e.g., AND EXISTS(...)) */
|
|
1945
|
+
readonly where?: WhereIntent;
|
|
1946
|
+
}
|
|
1947
|
+
type MutationIntent = InsertIntent | InsertFromIntent | UpsertFromIntent | UpdateIntent | BatchUpdateIntent | DeleteIntent | UpsertIntent;
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* @module intent/recursive-intent
|
|
1951
|
+
* Recursive CTE intent types for hierarchical data traversal (RFC-001).
|
|
1952
|
+
*/
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Node ID expression for recursive CTE anchor.
|
|
1956
|
+
* Used to define the join key for recursive traversal.
|
|
1957
|
+
*/
|
|
1958
|
+
type RecursiveNodeIdExpr = {
|
|
1959
|
+
readonly kind: 'column';
|
|
1960
|
+
readonly name: string;
|
|
1961
|
+
readonly as?: string;
|
|
1962
|
+
} | {
|
|
1963
|
+
readonly kind: 'literal';
|
|
1964
|
+
readonly value: unknown;
|
|
1965
|
+
readonly as?: string;
|
|
1966
|
+
} | {
|
|
1967
|
+
readonly kind: 'binary';
|
|
1968
|
+
readonly left: RecursiveNodeIdExpr;
|
|
1969
|
+
readonly op: string;
|
|
1970
|
+
readonly right: RecursiveNodeIdExpr;
|
|
1971
|
+
readonly as?: string;
|
|
1972
|
+
};
|
|
1973
|
+
/**
|
|
1974
|
+
* Get the alias for a node ID expression.
|
|
1975
|
+
* Used by both planner and compiler for consistent CTE column naming.
|
|
1976
|
+
*
|
|
1977
|
+
* @param expr - The node ID expression
|
|
1978
|
+
* @returns The alias to use (explicit alias, column name, or 'node_id' fallback)
|
|
1979
|
+
*/
|
|
1980
|
+
declare function getNodeIdAlias(expr: RecursiveNodeIdExpr): string;
|
|
1981
|
+
/**
|
|
1982
|
+
* Adjacency-list traversal (self-referential table).
|
|
1983
|
+
* Example: roles.parent_id → roles.id
|
|
1984
|
+
*/
|
|
1985
|
+
interface AdjacencyTraversal {
|
|
1986
|
+
readonly kind: 'adjacency';
|
|
1987
|
+
/** Table containing hierarchical data */
|
|
1988
|
+
readonly nodeTable: string;
|
|
1989
|
+
/** Primary key column (e.g., "id") */
|
|
1990
|
+
readonly nodeId: string;
|
|
1991
|
+
/** Foreign key pointing to parent (e.g., "parent_id") */
|
|
1992
|
+
readonly parentId: string;
|
|
1993
|
+
/** Traversal direction */
|
|
1994
|
+
readonly direction: 'descendants' | 'ancestors';
|
|
1995
|
+
/** Filter applied to each step (e.g., active = true) */
|
|
1996
|
+
readonly stepWhere?: WhereIntent;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Edge-table traversal (separate join table).
|
|
2000
|
+
* Example: role_inheritance(from_role_id, to_role_id)
|
|
2001
|
+
*/
|
|
2002
|
+
interface EdgeTableTraversal {
|
|
2003
|
+
readonly kind: 'edge-table';
|
|
2004
|
+
/** Node table containing hierarchical data */
|
|
2005
|
+
readonly nodeTable: string;
|
|
2006
|
+
/** Edge table containing relationships */
|
|
2007
|
+
readonly edgeTable: string;
|
|
2008
|
+
/** Primary key column in node table (e.g., "id") */
|
|
2009
|
+
readonly nodeId: string;
|
|
2010
|
+
/** Source column in edge table (e.g., "from_role_id") */
|
|
2011
|
+
readonly edgeFrom: string;
|
|
2012
|
+
/** Target column in edge table (e.g., "to_role_id") */
|
|
2013
|
+
readonly edgeTo: string;
|
|
2014
|
+
/** Traversal direction */
|
|
2015
|
+
readonly direction: 'out' | 'in' | 'both';
|
|
2016
|
+
/** Filter on edges (e.g., relationship_type = 'inheritance') */
|
|
2017
|
+
readonly edgeWhere?: WhereIntent;
|
|
2018
|
+
/** Filter on nodes (e.g., active = true) */
|
|
2019
|
+
readonly nodeWhere?: WhereIntent;
|
|
2020
|
+
/** Edge attributes to include in result */
|
|
2021
|
+
readonly edgeSelect?: readonly string[];
|
|
2022
|
+
/**
|
|
2023
|
+
* Hint for edge storage semantics (only affects `direction: 'both'`).
|
|
2024
|
+
*
|
|
2025
|
+
* - 'unknown' (default): Edges may exist in both directions (A→B and B→A).
|
|
2026
|
+
* Uses UNION (distinct) to avoid duplicates. Safe but slower.
|
|
2027
|
+
* - 'directed-only': Caller guarantees edges are stored once only.
|
|
2028
|
+
* Uses UNION ALL for performance. INCORRECT if duplicates exist.
|
|
2029
|
+
*/
|
|
2030
|
+
readonly edgeStorageHint?: 'unknown' | 'directed-only';
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Custom traversal for complex cases (P2 escape hatch).
|
|
2034
|
+
*/
|
|
2035
|
+
interface CustomTraversal {
|
|
2036
|
+
readonly kind: 'custom';
|
|
2037
|
+
/** Explicit step query builder - reserved for P2 */
|
|
2038
|
+
readonly stepBuilder?: unknown;
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Recursive traversal type union.
|
|
2042
|
+
*/
|
|
2043
|
+
type RecursiveTraversal = AdjacencyTraversal | EdgeTableTraversal | CustomTraversal;
|
|
2044
|
+
/**
|
|
2045
|
+
* Tracking options for recursive traversal.
|
|
2046
|
+
*/
|
|
2047
|
+
interface RecursiveTrackOptions {
|
|
2048
|
+
/** Depth counter (starts at 0) */
|
|
2049
|
+
readonly depth?: {
|
|
2050
|
+
readonly as?: string;
|
|
2051
|
+
};
|
|
2052
|
+
/** Path tracking for cycle detection + debugging */
|
|
2053
|
+
readonly path?: {
|
|
2054
|
+
/** Columns to trace in path (default: nodeId only) */
|
|
2055
|
+
readonly by?: 'nodeId' | readonly string[];
|
|
2056
|
+
/** Result column name (default: "path") */
|
|
2057
|
+
readonly as?: string;
|
|
2058
|
+
/** Storage strategy (default: 'array' for PostgreSQL, 'string' for others) */
|
|
2059
|
+
readonly strategy?: 'array' | 'string';
|
|
2060
|
+
/** Separator for string strategy (default: '/') */
|
|
2061
|
+
readonly separator?: string;
|
|
2062
|
+
};
|
|
2063
|
+
/** Cycle detection marker */
|
|
2064
|
+
readonly isCycle?: {
|
|
2065
|
+
readonly as?: string;
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Join clause for CTE emit composition.
|
|
2070
|
+
* Allows joining the CTE result with additional tables for final projection.
|
|
2071
|
+
*/
|
|
2072
|
+
interface EmitJoinClause {
|
|
2073
|
+
/** Table to join with */
|
|
2074
|
+
readonly table: string;
|
|
2075
|
+
/** Join type (default: 'inner') */
|
|
2076
|
+
readonly type?: 'inner' | 'left';
|
|
2077
|
+
/** Alias for this table (auto-generated if not provided) */
|
|
2078
|
+
readonly as?: string;
|
|
2079
|
+
/** Join condition */
|
|
2080
|
+
readonly on: {
|
|
2081
|
+
/** Column from CTE or previous joined table */
|
|
2082
|
+
readonly left: string;
|
|
2083
|
+
/** Column from this table */
|
|
2084
|
+
readonly right: string;
|
|
2085
|
+
};
|
|
2086
|
+
/** Columns to select from this table */
|
|
2087
|
+
readonly select?: readonly (string | {
|
|
2088
|
+
readonly column: string;
|
|
2089
|
+
readonly as: string;
|
|
2090
|
+
})[];
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Emit options for recursive CTE final projection.
|
|
2094
|
+
*/
|
|
2095
|
+
interface RecursiveEmitOptions {
|
|
2096
|
+
/** Fields to select from CTE */
|
|
2097
|
+
readonly select?: readonly string[];
|
|
2098
|
+
/** Filter on generated rows */
|
|
2099
|
+
readonly where?: WhereIntent;
|
|
2100
|
+
/** Ordering */
|
|
2101
|
+
readonly orderBy?: readonly OrderByIntent[];
|
|
2102
|
+
/** Join CTE result with additional tables for composition */
|
|
2103
|
+
readonly joinWith?: readonly EmitJoinClause[];
|
|
2104
|
+
/** Apply DISTINCT to final result */
|
|
2105
|
+
readonly distinct?: boolean;
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* PostgreSQL-specific options for recursive CTE (capability-gated).
|
|
2109
|
+
*/
|
|
2110
|
+
interface RecursiveAdvancedOptions {
|
|
2111
|
+
/**
|
|
2112
|
+
* Cycle detection strategy (adapter-specific implementation).
|
|
2113
|
+
* - 'error': Throw on cycle detection
|
|
2114
|
+
* - 'stop': Stop traversal at cycle (prune branch)
|
|
2115
|
+
* - 'mark': Add is_cycle column to results
|
|
2116
|
+
*
|
|
2117
|
+
* PostgreSQL 14+ uses native CYCLE clause.
|
|
2118
|
+
* Other adapters may use application-level detection.
|
|
2119
|
+
*/
|
|
2120
|
+
readonly cycle?: 'error' | 'stop' | 'mark';
|
|
2121
|
+
/**
|
|
2122
|
+
* Traversal search order (adapter-specific implementation).
|
|
2123
|
+
* - 'depth': Depth-first search order
|
|
2124
|
+
* - 'breadth': Breadth-first search order
|
|
2125
|
+
*
|
|
2126
|
+
* PostgreSQL 14+ uses native SEARCH clause.
|
|
2127
|
+
* Other adapters may use ORDER BY on depth column.
|
|
2128
|
+
*/
|
|
2129
|
+
readonly search?: 'depth' | 'breadth';
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Deduplication strategy for recursive CTE.
|
|
2133
|
+
*
|
|
2134
|
+
* - 'none': No dedup. May return same node multiple times via different paths.
|
|
2135
|
+
* Fastest. Use when you need all paths or when graph is known to be a tree.
|
|
2136
|
+
*
|
|
2137
|
+
* - 'final': One row per nodeId in final output.
|
|
2138
|
+
* Implemented via `DISTINCT ON (nodeId)` (PostgreSQL) or
|
|
2139
|
+
* `ROW_NUMBER() OVER (PARTITION BY nodeId)` fallback.
|
|
2140
|
+
* ⚠️ NOT the same as `query.distinct()` which dedupes on entire row!
|
|
2141
|
+
*
|
|
2142
|
+
* Note: 'global' (UNION instead of UNION ALL) was considered but not implemented.
|
|
2143
|
+
* 'final' provides the same end result with better performance characteristics.
|
|
2144
|
+
*/
|
|
2145
|
+
type RecursiveDedupe = 'none' | 'final';
|
|
2146
|
+
/**
|
|
2147
|
+
* Recursive CTE intent for hierarchical data traversal.
|
|
2148
|
+
*
|
|
2149
|
+
* Key invariant: anchor and step MUST produce identical column shape.
|
|
2150
|
+
* The planner validates this and auto-injects nodeIdExpr.
|
|
2151
|
+
*
|
|
2152
|
+
* @see RFC-001 for detailed specification
|
|
2153
|
+
*/
|
|
2154
|
+
interface RecursiveIntent {
|
|
2155
|
+
readonly type: 'recursive';
|
|
2156
|
+
/** CTE name for the recursive query */
|
|
2157
|
+
readonly cteName: string;
|
|
2158
|
+
readonly start: {
|
|
2159
|
+
/** Source table for anchor query */
|
|
2160
|
+
readonly from: string;
|
|
2161
|
+
/** Filter for seed rows (e.g., where id = $userId) */
|
|
2162
|
+
readonly where?: WhereIntent;
|
|
2163
|
+
/**
|
|
2164
|
+
* REQUIRED: Expression for node ID. Auto-injected into select.
|
|
2165
|
+
* This ensures the recursive join always has the key column.
|
|
2166
|
+
*/
|
|
2167
|
+
readonly nodeIdExpr: RecursiveNodeIdExpr;
|
|
2168
|
+
/** Additional fields to select (beyond nodeId) */
|
|
2169
|
+
readonly select?: readonly string[];
|
|
2170
|
+
};
|
|
2171
|
+
/** Traversal configuration (adjacency-list or edge-table) */
|
|
2172
|
+
readonly traversal: RecursiveTraversal;
|
|
2173
|
+
/** Tracking options for depth, path, and cycle detection */
|
|
2174
|
+
readonly track?: RecursiveTrackOptions;
|
|
2175
|
+
/** Maximum recursion depth (REQUIRED) */
|
|
2176
|
+
readonly maxDepth: number;
|
|
2177
|
+
/** Maximum rows (optional safety limit) */
|
|
2178
|
+
readonly maxRows?: number;
|
|
2179
|
+
/** Deduplication strategy */
|
|
2180
|
+
readonly dedupe?: RecursiveDedupe;
|
|
2181
|
+
/** Final projection options */
|
|
2182
|
+
readonly emit?: RecursiveEmitOptions;
|
|
2183
|
+
/** Advanced recursive options (cycle detection, search order) */
|
|
2184
|
+
readonly advancedOptions?: RecursiveAdvancedOptions;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
/**
|
|
2188
|
+
* @module intent/type-guards
|
|
2189
|
+
* Type guard functions for all intent AST types.
|
|
2190
|
+
*/
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Check if an intent is a window function intent
|
|
2194
|
+
*/
|
|
2195
|
+
declare function isWindowIntent(intent: unknown): intent is WindowIntent;
|
|
2196
|
+
/**
|
|
2197
|
+
* Check if a window function requires a field (aggregate or offset functions)
|
|
2198
|
+
*/
|
|
2199
|
+
declare function isAggregateWindowFunction(fn: WindowFunction): fn is AggregateWindowFunction | OffsetWindowFunction;
|
|
2200
|
+
/**
|
|
2201
|
+
* Check if a window function is a ranking function (no field required)
|
|
2202
|
+
*/
|
|
2203
|
+
declare function isRankingWindowFunction(fn: WindowFunction): fn is RankingWindowFunction;
|
|
2204
|
+
/**
|
|
2205
|
+
* Check if a where intent is a comparison
|
|
2206
|
+
*/
|
|
2207
|
+
declare function isWhereComparison(where: WhereIntent): where is WhereComparisonIntent;
|
|
2208
|
+
/**
|
|
2209
|
+
* Check if a where intent is a like filter
|
|
2210
|
+
*/
|
|
2211
|
+
declare function isWhereLike(where: WhereIntent): where is WhereLikeIntent;
|
|
2212
|
+
/**
|
|
2213
|
+
* Check if a where intent is a subquery filter
|
|
2214
|
+
*/
|
|
2215
|
+
declare function isWhereSubquery(where: WhereIntent): where is WhereSubqueryIntent;
|
|
2216
|
+
/**
|
|
2217
|
+
* Check if a value is a subquery ref (column reference in subquery)
|
|
2218
|
+
*/
|
|
2219
|
+
declare function isSubqueryRef(value: unknown): value is SubqueryRefIntent;
|
|
2220
|
+
/**
|
|
2221
|
+
* Check if a where intent is an in filter
|
|
2222
|
+
*/
|
|
2223
|
+
declare function isWhereIn(where: WhereIntent): where is WhereInIntent;
|
|
2224
|
+
/**
|
|
2225
|
+
* Check if a where intent is an any filter (= ANY($N::type[]))
|
|
2226
|
+
*/
|
|
2227
|
+
declare function isWhereAny(where: WhereIntent): where is WhereAnyIntent;
|
|
2228
|
+
/**
|
|
2229
|
+
* Check if a where intent is a null filter
|
|
2230
|
+
*/
|
|
2231
|
+
declare function isWhereNull(where: WhereIntent): where is WhereNullIntent;
|
|
2232
|
+
/**
|
|
2233
|
+
* Check if a where intent is a range filter (PostgreSQL range types)
|
|
2234
|
+
*/
|
|
2235
|
+
declare function isWhereRange(where: WhereIntent): where is WhereRangeIntent;
|
|
2236
|
+
/**
|
|
2237
|
+
* Check if a where intent is a logical AND
|
|
2238
|
+
*/
|
|
2239
|
+
declare function isWhereAnd(where: WhereIntent): where is WhereAndIntent;
|
|
2240
|
+
/**
|
|
2241
|
+
* Check if a where intent is a logical OR
|
|
2242
|
+
*/
|
|
2243
|
+
declare function isWhereOr(where: WhereIntent): where is WhereOrIntent;
|
|
2244
|
+
/**
|
|
2245
|
+
* Check if a where intent is a logical NOT
|
|
2246
|
+
*/
|
|
2247
|
+
declare function isWhereNot(where: WhereIntent): where is WhereNotIntent;
|
|
2248
|
+
/**
|
|
2249
|
+
* Check if a where intent is an exists filter
|
|
2250
|
+
*/
|
|
2251
|
+
declare function isWhereExists(where: WhereIntent): where is WhereExistsIntent;
|
|
2252
|
+
/**
|
|
2253
|
+
* Check if a where intent is a not exists filter
|
|
2254
|
+
*/
|
|
2255
|
+
declare function isWhereNotExists(where: WhereIntent): where is WhereNotExistsIntent;
|
|
2256
|
+
/**
|
|
2257
|
+
* Check if a where intent is a relation filter
|
|
2258
|
+
*/
|
|
2259
|
+
declare function isWhereRelationFilter(where: WhereIntent): where is WhereRelationFilterIntent;
|
|
2260
|
+
/**
|
|
2261
|
+
* Check if a where intent is any relation-based filter
|
|
2262
|
+
*/
|
|
2263
|
+
declare function isWhereRelationBased(where: WhereIntent): where is WhereExistsIntent | WhereNotExistsIntent | WhereRelationFilterIntent;
|
|
2264
|
+
/**
|
|
2265
|
+
* Check if a where intent is a logical operator (and/or/not)
|
|
2266
|
+
*/
|
|
2267
|
+
declare function isWhereLogical(where: WhereIntent): where is WhereAndIntent | WhereOrIntent | WhereNotIntent;
|
|
2268
|
+
/**
|
|
2269
|
+
* Check if a select intent selects all columns
|
|
2270
|
+
*/
|
|
2271
|
+
declare function isSelectAll(select: SelectIntent): select is SelectAllIntent;
|
|
2272
|
+
/**
|
|
2273
|
+
* Check if a select intent selects specific fields
|
|
2274
|
+
*/
|
|
2275
|
+
declare function isSelectFields(select: SelectIntent): select is SelectFieldsIntent;
|
|
2276
|
+
/**
|
|
2277
|
+
* Check if a select intent is an aggregate select
|
|
2278
|
+
*/
|
|
2279
|
+
declare function isSelectAggregate(select: SelectIntent): select is SelectAggregateIntent;
|
|
2280
|
+
/**
|
|
2281
|
+
* Check if a select intent has expressions
|
|
2282
|
+
*/
|
|
2283
|
+
declare function isSelectWithExpressions(select: SelectIntent): select is SelectWithExpressionsIntent;
|
|
2284
|
+
/**
|
|
2285
|
+
* Check if an expression is a COALESCE expression
|
|
2286
|
+
*/
|
|
2287
|
+
declare function isCoalesceExpression(expr: ExpressionIntent): expr is CoalesceExpressionIntent;
|
|
2288
|
+
/**
|
|
2289
|
+
* Check if an expression is a raw SQL expression
|
|
2290
|
+
*/
|
|
2291
|
+
declare function isRawExpression(expr: ExpressionIntent): expr is RawExpressionIntent;
|
|
2292
|
+
/**
|
|
2293
|
+
* Check if an expression is a column alias expression
|
|
2294
|
+
*/
|
|
2295
|
+
declare function isColumnAliasExpression(expr: ExpressionIntent): expr is ColumnAliasIntent;
|
|
2296
|
+
/**
|
|
2297
|
+
* Check if an expression is a relation column expression
|
|
2298
|
+
*/
|
|
2299
|
+
declare function isRelationColumnExpression(expr: ExpressionIntent): expr is RelationColumnIntent;
|
|
2300
|
+
/**
|
|
2301
|
+
* Check if a traversal is adjacency-list based
|
|
2302
|
+
*/
|
|
2303
|
+
declare function isAdjacencyTraversal(traversal: RecursiveTraversal): traversal is AdjacencyTraversal;
|
|
2304
|
+
/**
|
|
2305
|
+
* Check if a traversal is edge-table based
|
|
2306
|
+
*/
|
|
2307
|
+
declare function isEdgeTableTraversal(traversal: RecursiveTraversal): traversal is EdgeTableTraversal;
|
|
2308
|
+
/**
|
|
2309
|
+
* Check if a traversal is custom
|
|
2310
|
+
*/
|
|
2311
|
+
declare function isCustomTraversal(traversal: RecursiveTraversal): traversal is CustomTraversal;
|
|
2312
|
+
/**
|
|
2313
|
+
* Check if an intent is a recursive CTE intent
|
|
2314
|
+
*/
|
|
2315
|
+
declare function isRecursiveIntent(intent: QueryIntent | RecursiveIntent): intent is RecursiveIntent;
|
|
2316
|
+
/**
|
|
2317
|
+
* Check if an intent is an insert intent
|
|
2318
|
+
*/
|
|
2319
|
+
declare function isInsertIntent(intent: QueryIntent | RecursiveIntent | MutationIntent): intent is InsertIntent;
|
|
2320
|
+
/**
|
|
2321
|
+
* Check if an intent is an update intent
|
|
2322
|
+
*/
|
|
2323
|
+
declare function isUpdateIntent(intent: QueryIntent | RecursiveIntent | MutationIntent): intent is UpdateIntent;
|
|
2324
|
+
/**
|
|
2325
|
+
* Check if an intent is a delete intent
|
|
2326
|
+
*/
|
|
2327
|
+
declare function isDeleteIntent(intent: QueryIntent | RecursiveIntent | MutationIntent): intent is DeleteIntent;
|
|
2328
|
+
/**
|
|
2329
|
+
* Check if an intent is an upsert intent (DX-026)
|
|
2330
|
+
*/
|
|
2331
|
+
declare function isUpsertIntent(intent: QueryIntent | RecursiveIntent | MutationIntent): intent is UpsertIntent;
|
|
2332
|
+
/**
|
|
2333
|
+
* Check if an intent is any mutation intent
|
|
2334
|
+
*/
|
|
2335
|
+
declare function isMutationIntent(intent: QueryIntent | RecursiveIntent | MutationIntent): intent is MutationIntent;
|
|
2336
|
+
|
|
2337
|
+
/**
|
|
2338
|
+
* @module planner
|
|
2339
|
+
* Planner type definitions - PlanReport, PlanDecision, PlanWarning, etc.
|
|
2340
|
+
*
|
|
2341
|
+
* Runtime functions (plan(), planRecursive(), etc.) remain in @dbsp/core.
|
|
2342
|
+
*/
|
|
2343
|
+
|
|
2344
|
+
/**
|
|
2345
|
+
* Decision types made by the planner
|
|
2346
|
+
*/
|
|
2347
|
+
type DecisionType = 'filter-strategy' | 'join-type' | 'include-strategy' | 'cte-extraction' | 'ambiguity' | 'recursive-cte' | 'bidirectional-edges';
|
|
2348
|
+
interface PlanDecision {
|
|
2349
|
+
/** Unique identifier for the decision */
|
|
2350
|
+
readonly id: string;
|
|
2351
|
+
/** Type of decision */
|
|
2352
|
+
readonly type: DecisionType;
|
|
2353
|
+
/** Context: what triggered this decision */
|
|
2354
|
+
readonly context: {
|
|
2355
|
+
/** Source table in the decision */
|
|
2356
|
+
readonly sourceTable: string;
|
|
2357
|
+
/** Target table or relation name */
|
|
2358
|
+
readonly target?: string;
|
|
2359
|
+
/** Relation name if applicable */
|
|
2360
|
+
readonly relation?: string;
|
|
2361
|
+
/** Relation type (belongsTo, hasMany, hasOne, manyToMany) */
|
|
2362
|
+
readonly relationType?: string;
|
|
2363
|
+
/** Intent path (e.g., "where.exists.posts") */
|
|
2364
|
+
readonly intentPath?: string;
|
|
2365
|
+
/** Full relation path for multi-hop (SPEC-002), e.g., "author.company" */
|
|
2366
|
+
readonly relationPath?: string;
|
|
2367
|
+
/** User-provided include alias (e.g., 'author' from .include('author')) */
|
|
2368
|
+
readonly includeAlias?: string;
|
|
2369
|
+
/** Foreign key column(s) for include-strategy (Phase 3) */
|
|
2370
|
+
readonly foreignKey?: string | readonly string[];
|
|
2371
|
+
/** Whether the relation is self-referential (source === target) */
|
|
2372
|
+
readonly isSelfRef?: boolean;
|
|
2373
|
+
};
|
|
2374
|
+
/** The choice made */
|
|
2375
|
+
readonly choice: string;
|
|
2376
|
+
/**
|
|
2377
|
+
* Join type for include-strategy decisions using the 'join' strategy.
|
|
2378
|
+
* Set when the user explicitly requests 'inner' or 'left' via IncludeIntent.join.
|
|
2379
|
+
* When absent, the join handler defaults to LEFT JOIN.
|
|
2380
|
+
*/
|
|
2381
|
+
readonly joinType?: 'inner' | 'left';
|
|
2382
|
+
/** Human-readable reasoning */
|
|
2383
|
+
readonly reasoning: string;
|
|
2384
|
+
/** Other options that were available */
|
|
2385
|
+
readonly alternatives: readonly string[];
|
|
2386
|
+
}
|
|
2387
|
+
type PlanWarningCode = 'AMBIGUOUS_RELATION' | 'POTENTIAL_ROW_EXPLOSION' | 'CIRCULAR_INCLUDE' | 'MISSING_INDEX_HINT' | 'DEEP_NESTING' | 'INVALID_RECURSIVE_INCLUDE' | 'RAW_SQL_USAGE';
|
|
2388
|
+
interface PlanWarning {
|
|
2389
|
+
/** Warning code for programmatic handling */
|
|
2390
|
+
readonly code: PlanWarningCode;
|
|
2391
|
+
/** Human-readable message */
|
|
2392
|
+
readonly message: string;
|
|
2393
|
+
/** Suggested action to resolve */
|
|
2394
|
+
readonly suggestion?: string;
|
|
2395
|
+
/** Related decision ID if applicable */
|
|
2396
|
+
readonly relatedDecision?: string;
|
|
2397
|
+
}
|
|
2398
|
+
interface CTEDefinition {
|
|
2399
|
+
/** CTE name (used in WITH clause) */
|
|
2400
|
+
readonly name: string;
|
|
2401
|
+
/** Purpose of this CTE */
|
|
2402
|
+
readonly purpose: string;
|
|
2403
|
+
/** Which query parts reference this CTE */
|
|
2404
|
+
readonly referencedBy: readonly string[];
|
|
2405
|
+
/** The intent fragment this CTE represents */
|
|
2406
|
+
readonly sourceIntent: string;
|
|
2407
|
+
/**
|
|
2408
|
+
* CLI-012c: Whether this CTE should use WITH RECURSIVE.
|
|
2409
|
+
* Set when include.recursive is specified and relation is self-referential.
|
|
2410
|
+
*/
|
|
2411
|
+
readonly recursive?: boolean;
|
|
2412
|
+
}
|
|
2413
|
+
interface PlanReport {
|
|
2414
|
+
/** Root table for the query */
|
|
2415
|
+
readonly rootTable: string;
|
|
2416
|
+
/** All decisions made during planning */
|
|
2417
|
+
readonly decisions: readonly PlanDecision[];
|
|
2418
|
+
/** Warnings about the plan */
|
|
2419
|
+
readonly warnings: readonly PlanWarning[];
|
|
2420
|
+
/** CTEs to be extracted */
|
|
2421
|
+
readonly ctes: readonly CTEDefinition[];
|
|
2422
|
+
/** Original intent (for reference) */
|
|
2423
|
+
readonly intent: QueryIntent;
|
|
2424
|
+
/** Planning metadata */
|
|
2425
|
+
readonly metadata: {
|
|
2426
|
+
/** Planning duration in ms */
|
|
2427
|
+
readonly planningTimeMs: number;
|
|
2428
|
+
/** Number of relations traversed */
|
|
2429
|
+
readonly relationsAnalyzed: number;
|
|
2430
|
+
/** Whether the plan is ambiguous */
|
|
2431
|
+
readonly isAmbiguous: boolean;
|
|
2432
|
+
/** Ambiguous relation options (if isAmbiguous) */
|
|
2433
|
+
readonly ambiguousOptions?: readonly string[];
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
interface PlanOptions {
|
|
2437
|
+
/**
|
|
2438
|
+
* Force a specific filter strategy (overrides auto-detection)
|
|
2439
|
+
*/
|
|
2440
|
+
forceFilterStrategy?: 'exists' | 'join';
|
|
2441
|
+
/**
|
|
2442
|
+
* Force a specific join type (overrides auto-detection)
|
|
2443
|
+
*/
|
|
2444
|
+
forceJoinType?: 'left' | 'inner';
|
|
2445
|
+
/**
|
|
2446
|
+
* Enable CTE extraction for repeated subqueries
|
|
2447
|
+
* @default true
|
|
2448
|
+
*/
|
|
2449
|
+
enableCTEs?: boolean;
|
|
2450
|
+
/**
|
|
2451
|
+
* Threshold for CTE extraction (min references)
|
|
2452
|
+
* @default 2
|
|
2453
|
+
*/
|
|
2454
|
+
cteThreshold?: number;
|
|
2455
|
+
/**
|
|
2456
|
+
* Maximum include depth before warning
|
|
2457
|
+
* @default 5
|
|
2458
|
+
*/
|
|
2459
|
+
maxIncludeDepth?: number;
|
|
2460
|
+
/**
|
|
2461
|
+
* Disambiguation hints for ambiguous relations
|
|
2462
|
+
* Map of "sourceTable.targetTable" -> relation name
|
|
2463
|
+
*/
|
|
2464
|
+
disambiguate?: Record<string, string>;
|
|
2465
|
+
/**
|
|
2466
|
+
* Default include strategy for relations when set to 'auto'.
|
|
2467
|
+
* - 'join': Use JOIN (single query, database optimizes) - RECOMMENDED for to-one
|
|
2468
|
+
* - 'subquery': Use subquery queries (N+1 style with batching) - safe for to-many
|
|
2469
|
+
* - 'cte': Use CTE-based include (good for recursive/hierarchical)
|
|
2470
|
+
* - 'lateral': Use LATERAL JOIN (PostgreSQL) / CROSS APPLY (MSSQL)
|
|
2471
|
+
* - 'json_agg': Use JSON aggregation (PostgreSQL/MySQL/DuckDB)
|
|
2472
|
+
* - 'auto': Smart selection based on relation type + dialect capabilities
|
|
2473
|
+
* @default 'auto'
|
|
2474
|
+
*/
|
|
2475
|
+
defaultIncludeStrategy?: IncludeStrategy;
|
|
2476
|
+
/**
|
|
2477
|
+
* Dialect capabilities for smart strategy selection.
|
|
2478
|
+
* When provided, 'auto' strategy uses dialect-aware selection.
|
|
2479
|
+
* When absent, 'auto' falls back to 'join'.
|
|
2480
|
+
*/
|
|
2481
|
+
dialectCapabilities?: DialectCapabilities;
|
|
2482
|
+
}
|
|
2483
|
+
interface RecursivePlanReport extends Omit<PlanReport, 'intent' | 'metadata'> {
|
|
2484
|
+
readonly intent: RecursiveIntent;
|
|
2485
|
+
readonly metadata: PlanReport['metadata'] & {
|
|
2486
|
+
readonly isRecursive: true;
|
|
2487
|
+
readonly traversalKind: 'adjacency' | 'edge-table' | 'custom';
|
|
2488
|
+
readonly usesBidirectional: boolean;
|
|
2489
|
+
readonly dedupeStrategy: 'none' | 'final';
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
interface RecursivePlanOptions {
|
|
2493
|
+
/** Force bidirectional edge handling strategy */
|
|
2494
|
+
readonly forceBidirectionalStrategy?: 'union' | 'union-all';
|
|
2495
|
+
}
|
|
2496
|
+
/** Include strategy after 'auto' has been resolved to a concrete strategy */
|
|
2497
|
+
type ResolvedIncludeStrategy = Exclude<IncludeStrategy, 'auto'>;
|
|
2498
|
+
|
|
2499
|
+
/**
|
|
2500
|
+
* @module adapter
|
|
2501
|
+
* Adapter interface type definitions.
|
|
2502
|
+
*
|
|
2503
|
+
* Runtime functions (assertCapability, supportsExecution, etc.) and
|
|
2504
|
+
* error classes (AdapterRequiredError, UnsupportedCapabilityError)
|
|
2505
|
+
* remain in @dbsp/core.
|
|
2506
|
+
*/
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* Minimal logger interface for adapter debug/error logging.
|
|
2510
|
+
* Adapters accept an optional logger for observability without
|
|
2511
|
+
* coupling to any specific logging framework.
|
|
2512
|
+
*/
|
|
2513
|
+
interface AdapterLogger {
|
|
2514
|
+
debug?(message: string, ...args: unknown[]): void;
|
|
2515
|
+
warn?(message: string, ...args: unknown[]): void;
|
|
2516
|
+
error?(message: string, ...args: unknown[]): void;
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Adapter capabilities - what the underlying database/ORM supports.
|
|
2520
|
+
* Used for feature detection and graceful degradation.
|
|
2521
|
+
*/
|
|
2522
|
+
interface AdapterCapabilities {
|
|
2523
|
+
readonly supportsReturning: boolean;
|
|
2524
|
+
readonly supportsSchemas: boolean;
|
|
2525
|
+
readonly supportsStreaming: boolean;
|
|
2526
|
+
readonly supportsRecursiveCTE: boolean;
|
|
2527
|
+
readonly supportsWindowFunctions: boolean;
|
|
2528
|
+
readonly supportsArrayType: boolean;
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* A compiled query ready for execution.
|
|
2532
|
+
*
|
|
2533
|
+
* @typeParam T - The expected result type (phantom type for inference)
|
|
2534
|
+
*/
|
|
2535
|
+
interface CompiledQuery<T = unknown> {
|
|
2536
|
+
readonly sql: string;
|
|
2537
|
+
readonly parameters: readonly unknown[];
|
|
2538
|
+
/** Phantom type for result inference - not used at runtime */
|
|
2539
|
+
readonly __resultType?: T;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Base compile options shared across all adapters.
|
|
2543
|
+
* Adapters can extend this with adapter-specific options.
|
|
2544
|
+
*/
|
|
2545
|
+
interface CompileOptionsBase {
|
|
2546
|
+
/** Schema name for schema-scoped/multi-tenant queries */
|
|
2547
|
+
readonly schemaName?: string;
|
|
2548
|
+
/** Query name for logging */
|
|
2549
|
+
readonly queryName?: string;
|
|
2550
|
+
/** Correlation ID for distributed tracing */
|
|
2551
|
+
readonly correlationId?: string;
|
|
2552
|
+
/**
|
|
2553
|
+
* Row count threshold for switching INSERT compilation from VALUES to unnest strategy.
|
|
2554
|
+
* Rows <= threshold use VALUES ($1,$2),... Rows > threshold use SELECT unnest($1::type[]),...
|
|
2555
|
+
* Set to 0 to force unnest for all batch sizes.
|
|
2556
|
+
* @default 50
|
|
2557
|
+
*/
|
|
2558
|
+
readonly batchThreshold?: number;
|
|
2559
|
+
/**
|
|
2560
|
+
* Maximum allowed batch size for INSERT operations.
|
|
2561
|
+
* If set and the number of rows exceeds this limit, an InvalidOperationError is thrown.
|
|
2562
|
+
* Useful to prevent accidental unbounded inserts.
|
|
2563
|
+
* @default undefined (no limit)
|
|
2564
|
+
*/
|
|
2565
|
+
readonly maxBatchSize?: number;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Options for streaming query results.
|
|
2569
|
+
*/
|
|
2570
|
+
interface AdapterStreamOptions {
|
|
2571
|
+
/** Number of rows to fetch per chunk */
|
|
2572
|
+
readonly chunkSize?: number;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Alias mode for included relation columns.
|
|
2576
|
+
*
|
|
2577
|
+
* - `'always'` (default): Alias all columns from included tables (e.g., `"author.id"`, `"author.name"`)
|
|
2578
|
+
* - `'onCollision'`: Only alias columns that exist in multiple tables (e.g., `id`, `createdAt`)
|
|
2579
|
+
*
|
|
2580
|
+
* Note: `'never'` is intentionally excluded as it would cause data loss from duplicate column names.
|
|
2581
|
+
*/
|
|
2582
|
+
type AliasIncludedColumnsMode = 'always' | 'onCollision';
|
|
2583
|
+
/**
|
|
2584
|
+
* Describes the casing convention used for column names in the database.
|
|
2585
|
+
* This is the intuitive "what does your DB look like?" type:
|
|
2586
|
+
*
|
|
2587
|
+
* - `'snake_case'`: DB columns use snake_case → adapter transforms to camelCase for JS
|
|
2588
|
+
* - `'camelCase'`: DB columns use camelCase → no transformation needed
|
|
2589
|
+
* - `'preserve'`: No transformation applied
|
|
2590
|
+
*/
|
|
2591
|
+
type DbCasing = 'snake_case' | 'camelCase' | 'preserve';
|
|
2592
|
+
/**
|
|
2593
|
+
* Options for query compilation.
|
|
2594
|
+
* Extends CompileOptionsBase with core-specific options.
|
|
2595
|
+
*/
|
|
2596
|
+
interface CompileOptions extends CompileOptionsBase {
|
|
2597
|
+
/** Model IR for relation lookups during compilation */
|
|
2598
|
+
readonly model?: ModelIR;
|
|
2599
|
+
/**
|
|
2600
|
+
* Alias mode for included relation columns.
|
|
2601
|
+
* @default 'always'
|
|
2602
|
+
*/
|
|
2603
|
+
readonly aliasIncludedColumns?: AliasIncludedColumnsMode;
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Metadata for a subquery include query.
|
|
2607
|
+
* Used when planner decides include-strategy: 'subquery' for hasMany/manyToMany relations.
|
|
2608
|
+
*/
|
|
2609
|
+
interface SubqueryIncludeInfo {
|
|
2610
|
+
/** Name of the relation being included */
|
|
2611
|
+
readonly relationName: string;
|
|
2612
|
+
/** Target table to fetch from */
|
|
2613
|
+
readonly targetTable: string;
|
|
2614
|
+
/** Foreign key column(s) in target table */
|
|
2615
|
+
readonly foreignKey: string | readonly string[];
|
|
2616
|
+
/** Source key column(s) in parent table */
|
|
2617
|
+
readonly sourceKey: string | readonly string[];
|
|
2618
|
+
/** Optional select clause from include intent */
|
|
2619
|
+
readonly select?: SelectIntent;
|
|
2620
|
+
/** Optional where clause from include intent */
|
|
2621
|
+
readonly where?: WhereIntent;
|
|
2622
|
+
/** Optional nested includes (for recursive hydration) */
|
|
2623
|
+
readonly nestedIncludes?: readonly SubqueryIncludeInfo[];
|
|
2624
|
+
/** Junction table for M:N relations (e.g., 'postTags') */
|
|
2625
|
+
readonly through?: string;
|
|
2626
|
+
/** FK in junction table pointing to source (e.g., 'postId') */
|
|
2627
|
+
readonly throughSourceKey?: string;
|
|
2628
|
+
/** FK in junction table pointing to target (e.g., 'tagId') */
|
|
2629
|
+
readonly throughTargetKey?: string;
|
|
2630
|
+
/** Relation type for to-one unwrapping (belongsTo/hasOne → single object) */
|
|
2631
|
+
readonly relationType?: string;
|
|
2632
|
+
/** Source/parent table name for subquery optimization */
|
|
2633
|
+
readonly sourceTable?: string;
|
|
2634
|
+
/** Parent query's WHERE conditions for subquery optimization */
|
|
2635
|
+
readonly parentWhere?: WhereIntent;
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Result of compiling a query with subquery includes.
|
|
2639
|
+
*/
|
|
2640
|
+
interface CompileResultWithIncludes<T = unknown> {
|
|
2641
|
+
/** The main query (includes any JOIN includes) */
|
|
2642
|
+
readonly main: CompiledQuery<T>;
|
|
2643
|
+
/** Metadata for subquery include queries (empty if all includes use JOIN) */
|
|
2644
|
+
readonly subqueryIncludes: readonly SubqueryIncludeInfo[];
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Metadata for a query dump.
|
|
2648
|
+
*/
|
|
2649
|
+
interface DumpMeta {
|
|
2650
|
+
readonly schema?: string;
|
|
2651
|
+
readonly queryName?: string;
|
|
2652
|
+
readonly correlationId?: string;
|
|
2653
|
+
readonly compiledAt?: Date;
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* A dump contains the plan, compiled SQL, and parameters for observability.
|
|
2657
|
+
*/
|
|
2658
|
+
interface Dump {
|
|
2659
|
+
readonly plan?: PlanReport | undefined;
|
|
2660
|
+
readonly sql: string;
|
|
2661
|
+
readonly params: readonly unknown[];
|
|
2662
|
+
readonly meta?: DumpMeta;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Base adapter interface - core capabilities all adapters must have.
|
|
2666
|
+
*/
|
|
2667
|
+
interface BaseAdapter {
|
|
2668
|
+
/** Adapter capabilities for feature detection */
|
|
2669
|
+
readonly capabilities: AdapterCapabilities;
|
|
2670
|
+
/**
|
|
2671
|
+
* Dialect capabilities for planner strategy selection.
|
|
2672
|
+
* Determines which SQL features the adapter's database supports
|
|
2673
|
+
* (LATERAL JOIN, json_agg, window functions, etc.).
|
|
2674
|
+
*/
|
|
2675
|
+
readonly dialectCapabilities: DialectCapabilities;
|
|
2676
|
+
/**
|
|
2677
|
+
* Validate an identifier (table name, column name, schema name).
|
|
2678
|
+
* Throws if the identifier contains unsafe characters.
|
|
2679
|
+
*/
|
|
2680
|
+
validateIdentifier(value: string, type: string): void;
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Compiling adapter - can compile plans to SQL queries.
|
|
2684
|
+
*/
|
|
2685
|
+
interface CompilingAdapter extends BaseAdapter {
|
|
2686
|
+
/** Compile a plan to executable SQL. */
|
|
2687
|
+
compile<T = unknown>(plan: PlanReport, options?: CompileOptions): CompiledQuery<T>;
|
|
2688
|
+
/** Compile a plan with includes, returning subquery include metadata (DX-033). */
|
|
2689
|
+
compileWithIncludes<T = unknown>(plan: PlanReport, options?: CompileOptions): CompileResultWithIncludes<T>;
|
|
2690
|
+
/** Compile a subquery include query for given parent IDs (DX-033). */
|
|
2691
|
+
compileSubqueryInclude(info: SubqueryIncludeInfo, parentIds: readonly unknown[], options?: CompileOptions): CompiledQuery;
|
|
2692
|
+
/** Compile an insert intent to executable SQL. */
|
|
2693
|
+
compileInsert(intent: InsertIntent, options?: CompileOptions): CompiledQuery;
|
|
2694
|
+
/** Compile an insert-from intent to executable SQL (NQL-ALIGN). */
|
|
2695
|
+
compileInsertFrom(intent: InsertFromIntent, options?: CompileOptions): CompiledQuery;
|
|
2696
|
+
/** Compile an update intent to executable SQL. */
|
|
2697
|
+
compileUpdate(intent: UpdateIntent, options?: CompileOptions): CompiledQuery;
|
|
2698
|
+
/** Compile a batch update intent to executable SQL (BATCH-001). */
|
|
2699
|
+
compileBatchUpdate(intent: BatchUpdateIntent, options?: CompileOptions): CompiledQuery;
|
|
2700
|
+
/** Compile a delete intent to executable SQL. */
|
|
2701
|
+
compileDelete(intent: DeleteIntent, options?: CompileOptions): CompiledQuery;
|
|
2702
|
+
/** Compile an upsert intent to executable SQL (DX-026). */
|
|
2703
|
+
compileUpsert(intent: UpsertIntent, options?: CompileOptions): CompiledQuery;
|
|
2704
|
+
/** Compile an upsert-from intent to executable SQL (NQL-BIND). */
|
|
2705
|
+
compileUpsertFrom(intent: UpsertFromIntent, options?: CompileOptions): CompiledQuery;
|
|
2706
|
+
/** Compile a recursive CTE plan to executable SQL. */
|
|
2707
|
+
compileRecursive(report: RecursivePlanReport, model: ModelIR, options?: CompileOptions): CompiledQuery;
|
|
2708
|
+
/** Compile a CTE query backed by unnest() arrays (BATCH-001). */
|
|
2709
|
+
compileCteQuery(intent: CteQueryIntent, options?: CompileOptions): CompiledQuery;
|
|
2710
|
+
/** Compile a set operation (UNION / INTERSECT / EXCEPT) to SQL. */
|
|
2711
|
+
compileSetOperation(intent: SetOperationIntent, model: ModelIR, options?: CompileOptions): CompiledQuery;
|
|
2712
|
+
/** Compile a FROM-less SELECT expression to SQL (e.g. SELECT nextval('seq')). */
|
|
2713
|
+
compileSelectExpression(expr: ExpressionIntent): CompiledQuery;
|
|
2714
|
+
/** Create a dump for observability. */
|
|
2715
|
+
createDump(plan: PlanReport, query: CompiledQuery, meta?: DumpMeta): Dump;
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Executing adapter - can execute compiled queries.
|
|
2719
|
+
*/
|
|
2720
|
+
interface ExecutingAdapter extends BaseAdapter {
|
|
2721
|
+
/** Execute a query and return all results. */
|
|
2722
|
+
execute<T>(query: CompiledQuery<T>): Promise<T[]>;
|
|
2723
|
+
/** Execute a query and return the first result or null. */
|
|
2724
|
+
executeOne<T>(query: CompiledQuery<T>): Promise<T | null>;
|
|
2725
|
+
/** Execute a query and return the first result or throw. */
|
|
2726
|
+
executeOneOrThrow<T>(query: CompiledQuery<T>): Promise<T>;
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Streaming adapter - can stream query results.
|
|
2730
|
+
*/
|
|
2731
|
+
interface StreamingAdapter extends BaseAdapter {
|
|
2732
|
+
/** Stream query results as an async iterable iterator. */
|
|
2733
|
+
stream<T>(query: CompiledQuery<T>, options?: AdapterStreamOptions): AsyncIterableIterator<T>;
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Options for database introspection.
|
|
2737
|
+
*/
|
|
2738
|
+
interface IntrospectionOptions {
|
|
2739
|
+
/** Schema name to introspect (default: 'public' for PostgreSQL) */
|
|
2740
|
+
readonly schema?: string;
|
|
2741
|
+
/** Tables to include (default: all). Applied before exclude. */
|
|
2742
|
+
readonly include?: readonly string[];
|
|
2743
|
+
/** Tables to exclude (glob patterns: * matches any chars) */
|
|
2744
|
+
readonly exclude?: readonly string[];
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Result of database introspection.
|
|
2748
|
+
* Extends ModelIR with introspection-specific metadata.
|
|
2749
|
+
*/
|
|
2750
|
+
interface IntrospectionResult extends ModelIR {
|
|
2751
|
+
/** Timestamp when introspection was performed */
|
|
2752
|
+
readonly introspectedAt: Date;
|
|
2753
|
+
/** Warnings from introspection (e.g., unsupported types) */
|
|
2754
|
+
readonly warnings?: readonly string[];
|
|
2755
|
+
/** Hierarchy patterns detected during introspection (adjacency-list / edge-table) */
|
|
2756
|
+
readonly hierarchies?: readonly HierarchyIR[];
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Introspecting adapter - can introspect database schema.
|
|
2760
|
+
*/
|
|
2761
|
+
interface IntrospectingAdapter extends BaseAdapter {
|
|
2762
|
+
/** Database column casing convention */
|
|
2763
|
+
readonly dbCasing?: DbCasing;
|
|
2764
|
+
/** Introspect the database schema and return a ModelIR. */
|
|
2765
|
+
introspect(options?: IntrospectionOptions): Promise<IntrospectionResult>;
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Transactional adapter - supports database transactions.
|
|
2769
|
+
*/
|
|
2770
|
+
interface TransactionalAdapter<DB = unknown> extends BaseAdapter {
|
|
2771
|
+
/** Execute a callback within a database transaction. */
|
|
2772
|
+
transaction<T>(fn: (adapter: Adapter<DB>) => Promise<T>): Promise<T>;
|
|
2773
|
+
/** Create a schema-scoped adapter for multi-tenant queries. */
|
|
2774
|
+
withSchema(schemaName: string): Adapter<DB>;
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Raw SQL adapter - can execute raw SQL directly.
|
|
2778
|
+
*
|
|
2779
|
+
* @warning **SECURITY RISK: POTENTIAL SQL INJECTION**
|
|
2780
|
+
* Use parameter placeholders ($1, $2, etc.) for ALL values.
|
|
2781
|
+
*/
|
|
2782
|
+
interface RawSqlAdapter extends BaseAdapter {
|
|
2783
|
+
/**
|
|
2784
|
+
* Execute raw SQL directly - the ultimate escape hatch.
|
|
2785
|
+
*
|
|
2786
|
+
* @param sql - Raw SQL string with parameter placeholders
|
|
2787
|
+
* @param parameters - Parameter values (safely bound by driver)
|
|
2788
|
+
*/
|
|
2789
|
+
executeRaw<T = unknown>(sql: string, parameters?: readonly unknown[]): Promise<T[]>;
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* PostgreSQL index access method.
|
|
2793
|
+
*/
|
|
2794
|
+
type IndexMethod = 'btree' | 'hash' | 'gist' | 'gin' | 'brin' | 'hnsw' | 'ivfflat' | 'bm25';
|
|
2795
|
+
/** A column reference in an index: either a column name or an expression. */
|
|
2796
|
+
type IndexColumnDef = string | {
|
|
2797
|
+
expression: string;
|
|
2798
|
+
opclass?: string;
|
|
2799
|
+
};
|
|
2800
|
+
/** Options for CREATE INDEX. */
|
|
2801
|
+
type CreateIndexOptions = {
|
|
2802
|
+
name: string;
|
|
2803
|
+
columns: IndexColumnDef[];
|
|
2804
|
+
method?: IndexMethod;
|
|
2805
|
+
opclass?: Record<string, string>;
|
|
2806
|
+
include?: string[];
|
|
2807
|
+
with?: Record<string, unknown>;
|
|
2808
|
+
where?: string;
|
|
2809
|
+
unique?: boolean;
|
|
2810
|
+
ifNotExists?: boolean;
|
|
2811
|
+
concurrently?: boolean;
|
|
2812
|
+
};
|
|
2813
|
+
/** Options for DROP INDEX. */
|
|
2814
|
+
type DropIndexOptions = {
|
|
2815
|
+
ifExists?: boolean;
|
|
2816
|
+
cascade?: boolean;
|
|
2817
|
+
concurrently?: boolean;
|
|
2818
|
+
schema?: string;
|
|
2819
|
+
};
|
|
2820
|
+
/** Options for VACUUM. */
|
|
2821
|
+
type VacuumOptions = {
|
|
2822
|
+
full?: boolean;
|
|
2823
|
+
analyze?: boolean;
|
|
2824
|
+
};
|
|
2825
|
+
/** Options for TRUNCATE. */
|
|
2826
|
+
type TruncateOptions = {
|
|
2827
|
+
cascade?: boolean;
|
|
2828
|
+
restartIdentity?: boolean;
|
|
2829
|
+
};
|
|
2830
|
+
/** Options for ALTER COLUMN. */
|
|
2831
|
+
type AlterColumnOptions = {
|
|
2832
|
+
type?: string;
|
|
2833
|
+
using?: string;
|
|
2834
|
+
setNotNull?: boolean;
|
|
2835
|
+
setDefault?: unknown;
|
|
2836
|
+
dropDefault?: boolean;
|
|
2837
|
+
};
|
|
2838
|
+
/** Index metadata returned by listIndexes(). */
|
|
2839
|
+
type IndexInfo = {
|
|
2840
|
+
name: string;
|
|
2841
|
+
definition: string;
|
|
2842
|
+
unique: boolean;
|
|
2843
|
+
method: string;
|
|
2844
|
+
};
|
|
2845
|
+
/**
|
|
2846
|
+
* Optional mixin for adapters that can generate table-scoped DDL SQL strings.
|
|
2847
|
+
* When present on an adapter, buildTableDDL in core delegates SQL generation
|
|
2848
|
+
* to these methods instead of generating SQL inline.
|
|
2849
|
+
*/
|
|
2850
|
+
interface TableDDLGeneratorAdapter {
|
|
2851
|
+
/**
|
|
2852
|
+
* Generate SQL for TRUNCATE TABLE.
|
|
2853
|
+
*/
|
|
2854
|
+
generateTruncate?(table: string, schema?: string, options?: TruncateOptions): string;
|
|
2855
|
+
/**
|
|
2856
|
+
* Generate SQL for VACUUM.
|
|
2857
|
+
*/
|
|
2858
|
+
generateVacuum?(table: string, schema?: string, options?: VacuumOptions): string;
|
|
2859
|
+
/**
|
|
2860
|
+
* Generate SQL for ALTER TABLE ... ALTER COLUMN.
|
|
2861
|
+
*/
|
|
2862
|
+
generateAlterColumn?(table: string, column: string, options: AlterColumnOptions, schema?: string): string;
|
|
2863
|
+
/**
|
|
2864
|
+
* Generate SQL for CREATE INDEX.
|
|
2865
|
+
*/
|
|
2866
|
+
generateCreateIndex?(table: string, options: CreateIndexOptions, schema?: string): string;
|
|
2867
|
+
/**
|
|
2868
|
+
* Generate SQL for DROP INDEX.
|
|
2869
|
+
*/
|
|
2870
|
+
generateDropIndex?(name: string, options?: DropIndexOptions): string;
|
|
2871
|
+
/**
|
|
2872
|
+
* List all indexes on a table, with optional name pattern filter.
|
|
2873
|
+
*/
|
|
2874
|
+
listIndexes?(table: string, schema?: string, options?: {
|
|
2875
|
+
namePattern?: string;
|
|
2876
|
+
}): Promise<IndexInfo[]>;
|
|
2877
|
+
/**
|
|
2878
|
+
* Check whether an index with the given name exists on a table.
|
|
2879
|
+
*/
|
|
2880
|
+
indexExists?(name: string, table: string, schema?: string): Promise<boolean>;
|
|
2881
|
+
/**
|
|
2882
|
+
* Return the total storage size of a table in bytes.
|
|
2883
|
+
* Requires a live pool connection — compile-only adapters must throw.
|
|
2884
|
+
*/
|
|
2885
|
+
storageSize?(table: string, schema?: string): Promise<number>;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* DDL-generating adapter - can generate DDL (CREATE TABLE statements) from a schema.
|
|
2889
|
+
*/
|
|
2890
|
+
interface DDLGeneratingAdapter extends BaseAdapter {
|
|
2891
|
+
/**
|
|
2892
|
+
* Generate DDL statements from a schema.
|
|
2893
|
+
*
|
|
2894
|
+
* @param schema - The ModelIR schema to generate DDL from
|
|
2895
|
+
* @param options - Optional adapter-specific options (e.g., includeDropStatements)
|
|
2896
|
+
* @returns Array of DDL statements (CREATE TABLE, CREATE INDEX, etc.)
|
|
2897
|
+
*/
|
|
2898
|
+
generateDDL(schema: ModelIR, options?: Record<string, unknown>): string[];
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Compile-only adapter - can compile SQL and generate DDL, but cannot execute
|
|
2902
|
+
* queries or stream results. Useful for tooling, CLI, and testing without a
|
|
2903
|
+
* live database connection.
|
|
2904
|
+
*
|
|
2905
|
+
* Includes DDLGeneratingAdapter because DDL generation is purely compile-time
|
|
2906
|
+
* (no DB connection required — same rationale as SQL compilation).
|
|
2907
|
+
*
|
|
2908
|
+
* Explicitly excludes execution interfaces so that type-checking catches
|
|
2909
|
+
* misuse (e.g. calling execute() on a compile-only instance) at compile time.
|
|
2910
|
+
*/
|
|
2911
|
+
type CompileOnlyAdapter = CompilingAdapter & DDLGeneratingAdapter & {
|
|
2912
|
+
/** Naming convention used by this adapter. */
|
|
2913
|
+
readonly dbCasing: DbCasing;
|
|
2914
|
+
/**
|
|
2915
|
+
* Create a schema-scoped compile-only adapter.
|
|
2916
|
+
* Purely a schema-name prefix — no DB interaction required.
|
|
2917
|
+
*/
|
|
2918
|
+
withSchema(schemaName: string): CompileOnlyAdapter;
|
|
2919
|
+
readonly execute?: never;
|
|
2920
|
+
readonly executeOne?: never;
|
|
2921
|
+
readonly executeOneOrThrow?: never;
|
|
2922
|
+
readonly stream?: never;
|
|
2923
|
+
readonly introspect?: never;
|
|
2924
|
+
readonly transaction?: never;
|
|
2925
|
+
readonly executeRaw?: never;
|
|
2926
|
+
readonly executeDDL?: never;
|
|
2927
|
+
};
|
|
2928
|
+
/**
|
|
2929
|
+
* Database adapter interface - full adapter with all capabilities.
|
|
2930
|
+
*
|
|
2931
|
+
* @typeParam DB - Database schema type for type inference
|
|
2932
|
+
*/
|
|
2933
|
+
interface Adapter<DB = unknown> extends CompilingAdapter, ExecutingAdapter, StreamingAdapter, IntrospectingAdapter, TransactionalAdapter<DB>, RawSqlAdapter, DDLGeneratingAdapter, TableDDLGeneratorAdapter {
|
|
2934
|
+
/**
|
|
2935
|
+
* Naming convention used by this adapter.
|
|
2936
|
+
*
|
|
2937
|
+
* @since ARCH-006
|
|
2938
|
+
*/
|
|
2939
|
+
readonly dbCasing: DbCasing;
|
|
2940
|
+
/**
|
|
2941
|
+
* Execute a DDL statement directly (e.g. TRUNCATE, VACUUM, ALTER TABLE, CREATE INDEX).
|
|
2942
|
+
* Optional — compile-only adapters do not implement this.
|
|
2943
|
+
*
|
|
2944
|
+
* @since DDL-TABLE-001
|
|
2945
|
+
*/
|
|
2946
|
+
executeDDL?(sql: string): Promise<void>;
|
|
2947
|
+
/**
|
|
2948
|
+
* Whether this adapter instance is scoped inside a transaction.
|
|
2949
|
+
* Used by table DDL methods to guard against unsafe operations (e.g. VACUUM).
|
|
2950
|
+
*
|
|
2951
|
+
* @since DDL-TABLE-001
|
|
2952
|
+
*/
|
|
2953
|
+
readonly inTransaction?: boolean;
|
|
2954
|
+
}
|
|
2955
|
+
/** Behavior when schema uses features the adapter doesn't support */
|
|
2956
|
+
type UnsupportedFeatureBehavior = 'error' | 'warning' | 'ignore';
|
|
2957
|
+
/** Aligned with DialectCapabilities supportsDDL* flags (1:1 mapping) */
|
|
2958
|
+
type DDLFeature = 'enum' | 'sequence' | 'extension' | 'partition' | 'checkConstraint' | 'onUpdateFK' | 'deferredFK' | 'identity' | 'collation' | 'comment' | 'indexMethod' | 'indexOpclass' | 'indexInclude' | 'partialIndex' | 'expressionIndex';
|
|
2959
|
+
/** Version range for a DDL feature — resolved at createDialectCapabilities() time */
|
|
2960
|
+
interface DDLFeatureVersionRange {
|
|
2961
|
+
/** Minimum database version required (inclusive). E.g., '8.0.16' */
|
|
2962
|
+
readonly min?: string;
|
|
2963
|
+
/** Maximum database version supported (inclusive, optional). For deprecation. */
|
|
2964
|
+
readonly max?: string;
|
|
2965
|
+
}
|
|
2966
|
+
/** Per-feature behavior overrides (global default + optional per-feature) */
|
|
2967
|
+
interface FeatureBehaviorConfig {
|
|
2968
|
+
/** Global default behavior (default: 'warning') */
|
|
2969
|
+
readonly default: UnsupportedFeatureBehavior;
|
|
2970
|
+
/** Per-feature overrides */
|
|
2971
|
+
readonly overrides?: Partial<Record<DDLFeature, UnsupportedFeatureBehavior>>;
|
|
2972
|
+
}
|
|
2973
|
+
/** Error thrown when behavior = 'error' and unsupported feature detected */
|
|
2974
|
+
declare class UnsupportedFeatureError extends Error {
|
|
2975
|
+
readonly feature: string;
|
|
2976
|
+
readonly adapter: string;
|
|
2977
|
+
readonly element: string;
|
|
2978
|
+
constructor(feature: string, adapter: string, element: string);
|
|
2979
|
+
}
|
|
2980
|
+
/** Warning emitted when behavior = 'warning' */
|
|
2981
|
+
interface FeatureWarning {
|
|
2982
|
+
readonly feature: string;
|
|
2983
|
+
readonly adapter: string;
|
|
2984
|
+
readonly element: string;
|
|
2985
|
+
readonly message: string;
|
|
2986
|
+
}
|
|
2987
|
+
/** Type-safe element map: DDLFeature → IR type (INV-12) */
|
|
2988
|
+
interface DDLFeatureElementMap {
|
|
2989
|
+
enum: EnumIR;
|
|
2990
|
+
sequence: SequenceIR;
|
|
2991
|
+
extension: string;
|
|
2992
|
+
partition: PartitionIR;
|
|
2993
|
+
checkConstraint: CheckConstraintIR;
|
|
2994
|
+
onUpdateFK: ForeignKeyIR;
|
|
2995
|
+
deferredFK: ForeignKeyIR;
|
|
2996
|
+
identity: ColumnIR;
|
|
2997
|
+
collation: ColumnIR;
|
|
2998
|
+
comment: {
|
|
2999
|
+
target: 'table' | 'column';
|
|
3000
|
+
name: string;
|
|
3001
|
+
comment: string;
|
|
3002
|
+
};
|
|
3003
|
+
indexMethod: IndexIR;
|
|
3004
|
+
indexOpclass: IndexIR;
|
|
3005
|
+
indexInclude: IndexIR;
|
|
3006
|
+
partialIndex: IndexIR;
|
|
3007
|
+
expressionIndex: IndexIR;
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Interface for translating IR features to dialect-specific SQL.
|
|
3011
|
+
* Adapters register translators to handle features their way.
|
|
3012
|
+
*
|
|
3013
|
+
* @example PG enum translator
|
|
3014
|
+
* ```typescript
|
|
3015
|
+
* const pgEnumTranslator: FeatureTranslator<'enum'> = {
|
|
3016
|
+
* feature: 'enum',
|
|
3017
|
+
* translate(element, context) {
|
|
3018
|
+
* return [`CREATE TYPE "${element.name}" AS ENUM (${element.values.map(v => `'${v}'`).join(', ')})`];
|
|
3019
|
+
* },
|
|
3020
|
+
* };
|
|
3021
|
+
* ```
|
|
3022
|
+
*/
|
|
3023
|
+
interface FeatureTranslator<F extends DDLFeature = DDLFeature> {
|
|
3024
|
+
/** Which IR feature this translator handles */
|
|
3025
|
+
readonly feature: F;
|
|
3026
|
+
/** Generate SQL for this feature. Return null to skip (use default behavior). */
|
|
3027
|
+
translate(element: DDLFeatureElementMap[F], context: TranslationContext): string[] | null;
|
|
3028
|
+
}
|
|
3029
|
+
interface TranslationContext {
|
|
3030
|
+
readonly schemaName?: string;
|
|
3031
|
+
readonly tableName?: string;
|
|
3032
|
+
readonly dialectCapabilities: DialectCapabilities;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
/**
|
|
3036
|
+
* Canonical shape of a `schema()` factory result as produced by
|
|
3037
|
+
* `createOrm({schema: ...})` callers and consumed by tooling
|
|
3038
|
+
* (cli, gui sidecar, mcp-server).
|
|
3039
|
+
*
|
|
3040
|
+
* ARCH-005: Schema type from schema() function.
|
|
3041
|
+
* Contains the definition, pre-computed ModelIR, and table names.
|
|
3042
|
+
*/
|
|
3043
|
+
interface LoadedSchema {
|
|
3044
|
+
readonly definition: Record<string, unknown>;
|
|
3045
|
+
readonly model: ModelIR;
|
|
3046
|
+
readonly tableNames: string[];
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Runtime type guard for `LoadedSchema` — verifies the object has
|
|
3050
|
+
* `definition`, `model` (with nested `tables` + `relations`), and
|
|
3051
|
+
* `tableNames` as an array. Structural check only — does not validate
|
|
3052
|
+
* the inner values of any field.
|
|
3053
|
+
*
|
|
3054
|
+
* Type guard for ARCH-005 schema() output.
|
|
3055
|
+
*/
|
|
3056
|
+
declare function isValidSchema(schema: unknown): schema is LoadedSchema;
|
|
3057
|
+
|
|
3058
|
+
export { type Adapter, type AdapterCapabilities, type AdapterLogger, type AdapterStreamOptions, type AdjacencyTraversal, type AggOrderByArg, type AggregateExpressionIntent, type AggregateFunction, type AggregateIntent, type AggregateWindowFunction, type AliasIncludedColumnsMode, type AlterColumnOptions, type AmbiguityCheckResult, type ArithmeticExpressionIntent, type ArrayExpressionIntent, type ArrayOperator, type BaseAdapter, type BatchUpdateIntent, type BatchValuesJoinPayload, type CTEDefinition, type Cardinality, type CaseExpressionIntent, type CastExpressionIntent, type CheckConstraintIR, type CoalesceExpressionIntent, type ColumnAliasIntent, type ColumnExpressionIntent, type ColumnIR, type ColumnType, type CommonColumnType, type ComparisonExpressionIntent, type ComparisonOperator, type CompileOnlyAdapter, type CompileOptions, type CompileOptionsBase, type CompileResultWithIncludes, type CompiledQuery, type CompilingAdapter, type CreateIndexOptions, type CteQueryIntent, type CustomFnExpressionIntent, type CustomOpExpressionIntent, type CustomTraversal, type DDLFeature, type DDLFeatureElementMap, type DDLFeatureVersionRange, type DDLGeneratingAdapter, type DbCasing, type DecisionType, type DeleteIntent, type DialectCapabilities, type DialectName, type DropIndexOptions, type DuckDBColumnType, type Dump, type DumpMeta, type EdgeTableTraversal, type EmitJoinClause, type EnumIR, type ExecutingAdapter, type ExpressionIntent, type FeatureBehaviorConfig, type FeatureTranslator, type FeatureWarning, type FieldRef, type FilterStrategy, type ForeignKeyIR, type FunctionExpressionIntent, type HierarchyIR, type IncludeIntent, type IncludeRecursiveOptions, type IncludeStrategy, type IndexColumnDef, type IndexIR, type IndexInfo, type IndexMethod, type InsertFromIntent, type InsertIntent, type IntrospectingAdapter, type IntrospectionOptions, type IntrospectionResult, type IsTypeSupported, type JoinDefault, type JoinIntent, type JsonContainsIntent, type JsonExistsIntent, type JsonExtractIntent, type JsonPathExtractIntent, type LiteralExpressionIntent, type LoadedSchema, type LockIntent, type LockStrength, type LockWaitPolicy, type LogicalOperator, type MSSQLColumnType, type ModelIR, type MutationIntent, type MySQLColumnType, type NamedArgExpressionIntent, type NullOperator, type NullsPosition, type OffsetWindowFunction, type OnDeleteAction, type Optionality, type OrderByIntent, type ParamExpressionIntent, type PartitionIR, type PlanDecision, type PlanOptions, type PlanReport, type PlanWarning, type PlanWarningCode, type PolicyIR, type PostgresColumnType, type PostgresOnlyColumnType, type PseudoColumnExpressionIntent, type PseudoColumnMetadata, type PseudoColumnTraversal, type QueryIntent, type RangeOperator, type RangeValue, type RankingWindowFunction, type RawCteIntent, type RawExpressionIntent, type RawSqlAdapter, type RecursiveAdvancedOptions, type RecursiveDedupe, type RecursiveDirection, type RecursiveEmitOptions, type RecursiveExistsOptions, type RecursiveIntent, type RecursiveMetadata, type RecursiveNodeIdExpr, type RecursivePlanOptions, type RecursivePlanReport, type RecursiveTrackOptions, type RecursiveTraversal, type RefExpressionIntent, type RelationColumnIntent, type RelationIR, type RelationKind, type RelationOperator, type RelationType, type ResolvedIncludeStrategy, type SQLiteColumnType, type ScalarSubqueryIntent, type SelectAggregateIntent, type SelectAllIntent, type SelectFieldsIntent, type SelectIntent, type SelectWithExpressionsIntent, type SequenceIR, type SetOperationIntent, type SetOperationType, type SimpleCteIntent, type SortDirection, type StarExpressionIntent, type StreamingAdapter, type StringOperator, type SubqueryExpressionIntent, type SubqueryIncludeInfo, type SubqueryRefIntent, type SupportedColumnTypes, type TableDDLGeneratorAdapter, type TableIR, type TransactionalAdapter, type TranslationContext, type TruncateOptions, type UnaryExpressionIntent, type UnnestCteIntent, type UnsupportedFeatureBehavior, UnsupportedFeatureError, type UpdateIntent, type UpsertConflictAction, type UpsertConflictTarget, type UpsertFromIntent, type UpsertIntent, type VacuumOptions, type WhereAndIntent, type WhereAnyIntent, type WhereComparisonIntent, type WhereExistsIntent, type WhereExpressionIntent, type WhereInIntent, type WhereIntent, type WhereJsonContainsIntent, type WhereJsonExistsIntent, type WhereLikeIntent, type WhereNotExistsIntent, type WhereNotIntent, type WhereNullIntent, type WhereOrIntent, type WhereRangeIntent, type WhereRawExistsIntent, type WhereRawNotExistsIntent, type WhereRelationFilterIntent, type WhereSubqueryIntent, type WindowFunction, type WindowIntent, type WindowOrderBy, getNodeIdAlias, isAdjacencyTraversal, isAggregateWindowFunction, isCoalesceExpression, isColumnAliasExpression, isCustomTraversal, isDeleteIntent, isEdgeTableTraversal, isFieldRef, isInsertIntent, isMutationIntent, isRankingWindowFunction, isRawExpression, isRecursiveIntent, isRelationColumnExpression, isSelectAggregate, isSelectAll, isSelectFields, isSelectWithExpressions, isSubqueryRef, isUpdateIntent, isUpsertIntent, isValidSchema, isWhereAnd, isWhereAny, isWhereComparison, isWhereExists, isWhereIn, isWhereLike, isWhereLogical, isWhereNot, isWhereNotExists, isWhereNull, isWhereOr, isWhereRange, isWhereRelationBased, isWhereRelationFilter, isWhereSubquery, isWindowIntent };
|