@atscript/db-sql-tools 0.1.146 → 0.1.148

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
1
+ import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
2
2
  import { FilterExpr, FilterVisitor } from "@uniqu/core";
3
3
  import { TDbAggregateFn } from "@atscript/db/agg";
4
4
 
@@ -34,8 +34,10 @@ interface SqlDialect {
34
34
  geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
35
35
  /**
36
36
  * Calendar-bucket label expression over one column: TEXT `'YYYY-MM-DD'`
37
- * (the local calendar date of the bucket's first day in `b.tz`), or NULL for
38
- * a NULL source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
37
+ * (the local calendar date of the bucket's first day in `b.tz`; for unit
38
+ * `hour`, `'YYYY-MM-DDTHH:00'`, the local wall-clock hour — truncate the
39
+ * zone's wall time, never the UTC instant), or NULL for a NULL source or
40
+ * one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
39
41
  * `quotedCol` is already quoted; `b.fd` identifies the storage kind.
40
42
  *
41
43
  * The expression must be PARAMETER-FREE — inline the zone with
@@ -83,6 +85,56 @@ interface SqlDialect {
83
85
  * @since 0.1.136
84
86
  */
85
87
  jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
88
+ /**
89
+ * `expr` cast to an IEEE double — how a computed view column
90
+ * (`@db.compute`) evaluates every field / literal leaf, so `7 / 2 = 3.5`
91
+ * everywhere (no integer division, no DECIMAL rounding): SQLite
92
+ * `CAST(x AS REAL)`, MySQL `CAST(x AS DOUBLE)`, PostgreSQL
93
+ * `CAST(x AS DOUBLE PRECISION)`. Parameter-free. Dialects without it fail
94
+ * view sync with `computed view columns are not supported by this adapter`.
95
+ * @since 0.1.147
96
+ */
97
+ castDouble?(expr: string): string;
98
+ /**
99
+ * The aggregate functions that stand in for `MIN` / `MAX` over a BOOLEAN
100
+ * column on an engine that has no `MIN(boolean)` (PostgreSQL: `BOOL_AND` /
101
+ * `BOOL_OR`). Absent: `MIN` / `MAX` apply to booleans as to any column.
102
+ * @since 0.1.148
103
+ */
104
+ booleanAggregates?: {
105
+ min: string;
106
+ max: string;
107
+ };
108
+ /**
109
+ * Renders the "any value of the group" pick over an already rendered column
110
+ * — what a `first` / `last` derived column (constant within its group)
111
+ * aggregates through — for the column's source `field` (`undefined` when
112
+ * unknown). The dialect picks the cheapest aggregate its engine has for the
113
+ * column's type: a streaming `MIN` where one exists, a type-agnostic form
114
+ * only where it does not (PostgreSQL: `MIN` for ordered types, `BOOL_AND`
115
+ * for a boolean, `(ARRAY_AGG(x))[1]` for uuid, bytea, point, json, …).
116
+ * Absent: `MIN(x)`.
117
+ * @since 0.1.148
118
+ */
119
+ anyValue?(expr: string, field: TDbFieldMeta | undefined): string;
120
+ /**
121
+ * Maps a driver error of a grouped query to the `DbError` it means — a
122
+ * numeric overflow (`arithOverflowError` when the query has arithmetic
123
+ * expressions, else `numericOutOfRangeError`), an unknown calendar-bucket
124
+ * zone — or `undefined` to let it propagate unchanged. `arithmetic` is
125
+ * whether the query selects any arithmetic expression. Run by
126
+ * {@link mapQueryErrors}.
127
+ * @since 0.1.148
128
+ */
129
+ mapQueryError?(error: unknown, arithmetic: boolean): Error | undefined;
130
+ /**
131
+ * `true` when the database sorts NULL as the LARGEST value (PostgreSQL):
132
+ * first-row join order keys then render `ASC NULLS FIRST` /
133
+ * `DESC NULLS LAST`, keeping the uniform "NULL is the smallest value"
134
+ * ordering SQLite, MySQL and MongoDB have natively.
135
+ * @since 0.1.147
136
+ */
137
+ nullsSortLargest?: boolean;
86
138
  /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
87
139
  createViewPrefix: string;
88
140
  /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
@@ -93,6 +145,21 @@ interface SqlDialect {
93
145
  * (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
94
146
  */
95
147
  declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
148
+ /**
149
+ * Runs `fn`, rethrowing a driver error as the `DbError`
150
+ * {@link SqlDialect.mapQueryError} maps it to (any other error unchanged).
151
+ * `controls` of the query tell whether it has arithmetic expressions.
152
+ * @since 0.1.148
153
+ */
154
+ declare function mapQueryErrors<R>(dialect: SqlDialect, fn: () => Promise<R>, controls?: DbControls): Promise<R>;
155
+ /**
156
+ * One `ORDER BY` key with the uniform "NULL is the smallest value" ordering:
157
+ * `<expr> ASC` / `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a
158
+ * dialect where NULL sorts largest ({@link SqlDialect.nullsSortLargest}).
159
+ * Shared by first-row joins and `first` / `last` aggregates.
160
+ * @since 0.1.148
161
+ */
162
+ declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean): string;
96
163
  /**
97
164
  * Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
98
165
  * {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
@@ -108,6 +175,34 @@ declare function quotedJsonPathSegments(path: readonly string[]): string[];
108
175
  declare const EMPTY_AND: TSqlFragment;
109
176
  declare const EMPTY_OR: TSqlFragment;
110
177
  //#endregion
178
+ //#region src/arith.d.ts
179
+ /** Why {@link renderArith} cannot render an expression. */
180
+ type TArithFailure = "no-cast" | "non-finite";
181
+ /**
182
+ * Renders a computed expression tree (`@db.compute` on a view, or a query-time
183
+ * arithmetic `$select` entry) as SQL, in IEEE double on every dialect: each
184
+ * literal is cast with {@link SqlDialect.castDouble}; `+ - *` render as
185
+ * `(l op r)`, `/` as `(l / NULLIF(r, 0))` (division by zero is NULL), unary
186
+ * minus as `(-x)` and `coalesce` as `COALESCE(…)`. `leaf` renders a field
187
+ * reference as raw SQL, which is cast to double here — or, as `{ double }`,
188
+ * SQL that already is a double (a nested computed column) and stays as is.
189
+ *
190
+ * The one renderer of both paths, so declared and query-time arithmetic
191
+ * cannot diverge.
192
+ *
193
+ * @param fail - the error to throw for a failure (default `DbError`:
194
+ * `AGG_EXPR_NOT_SUPPORTED` without `castDouble`, `INVALID_QUERY` for a
195
+ * non-finite literal).
196
+ * @since 0.1.148
197
+ */
198
+ declare function renderArith(dialect: SqlDialect, node: AtscriptExprNode, leaf: (field: string) => string | {
199
+ double: string;
200
+ }, fail?: (reason: TArithFailure) => Error): string;
201
+ /** The error of a numeric overflow that is no arithmetic expression's (`INVALID_QUERY`, `path` `""`). */
202
+ declare function numericOutOfRangeError(): DbError;
203
+ /** The error of a double overflow in aggregate arithmetic (`INVALID_QUERY`, `path` `$select`). */
204
+ declare function arithOverflowError(): DbError;
205
+ //#endregion
111
206
  //#region src/filter-builder.d.ts
112
207
  interface TFilterVisitorOptions {
113
208
  /**
@@ -117,6 +212,35 @@ interface TFilterVisitorOptions {
117
212
  * expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
118
213
  */
119
214
  columnRef?: (field: string) => string;
215
+ /**
216
+ * The quoted outer table or alias that relational predicates
217
+ * (`{ nav: { $some | $none: … } }`) correlate to: their `EXISTS` subqueries
218
+ * compare the related rows with `<qualifier>."<column>"`. Defaults to the
219
+ * predicate's source table (`dialect.quoteTable(node.source.table)`), which
220
+ * is right for every statement whose FROM is the bare table; a statement
221
+ * that aliases its FROM (`FROM "items" AS "t"`) must pass that alias here.
222
+ *
223
+ * @since 0.1.147
224
+ */
225
+ qualifier?: string;
226
+ /**
227
+ * Alias sequence of the relational-predicate subqueries (`_rf1`, `_rf2`, …),
228
+ * shared by the visitors of one statement so nested predicates get distinct
229
+ * aliases. A visitor created without one starts its own.
230
+ *
231
+ * @internal
232
+ * @since 0.1.147
233
+ */
234
+ aliasSeq?: TRelationAliasSeq;
235
+ }
236
+ /**
237
+ * Alias counter of the relational-predicate subqueries of one statement.
238
+ *
239
+ * @internal
240
+ * @since 0.1.147
241
+ */
242
+ interface TRelationAliasSeq {
243
+ n: number;
120
244
  }
121
245
  /**
122
246
  * Creates a dialect-specific filter visitor for `walkFilter`.
@@ -124,8 +248,18 @@ interface TFilterVisitorOptions {
124
248
  declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
125
249
  /**
126
250
  * Translates a filter expression into a parameterized SQL WHERE clause.
251
+ *
252
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
253
+ * core into `ResolvedRelationFilter` operands) render as correlated
254
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
255
+ * alias they correlate to (default: the source table — see
256
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
257
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
258
+ * Placeholders stay positional `?` in textual order.
259
+ *
260
+ * @param opts - since 0.1.147
127
261
  */
128
- declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
262
+ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
129
263
  //#endregion
130
264
  //#region src/geo.d.ts
131
265
  /**
@@ -135,6 +269,13 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
135
269
  * across dialects, so the public name can't be used directly.
136
270
  */
137
271
  declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
272
+ /**
273
+ * Alias of the searched table inside the geo and vector search statements —
274
+ * a filter rendered into them correlates its relational predicates to it
275
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
276
+ * @since 0.1.147
277
+ */
278
+ declare const SEARCH_SOURCE_ALIAS = "t";
138
279
  /** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
139
280
  interface TGeoSearchControls {
140
281
  $limit?: number;
@@ -260,6 +401,15 @@ declare function buildInsert(dialect: SqlDialect, table: string, data: Record<st
260
401
  * some).
261
402
  */
262
403
  declare function insertManyColumns(rows: readonly Record<string, unknown>[]): string[];
404
+ /**
405
+ * Splits `rows` into batches that stay under the driver's bind-parameter limit
406
+ * (PostgreSQL ~65535, MySQL packet size): `maxParams` (default 60000) divided
407
+ * by the column count. Returns the shared column union and the batches.
408
+ */
409
+ declare function chunkInsertRows(rows: readonly Record<string, unknown>[], maxParams?: number): {
410
+ columns: string[];
411
+ batches: Record<string, unknown>[][];
412
+ };
263
413
  /**
264
414
  * Builds a multi-row `INSERT … VALUES (…), (…)` statement over `columns`
265
415
  * (default: {@link insertManyColumns} of `rows`). A row lacking a column gets
@@ -272,6 +422,25 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
272
422
  * Builds a SELECT statement with optional sort, limit, offset, projection.
273
423
  */
274
424
  declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
425
+ /**
426
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
427
+ * {@link stripPartitionRowNumber} removes it from the result rows.
428
+ * @since 0.1.147
429
+ */
430
+ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
431
+ /**
432
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
433
+ * sharing the values of the `partitionBy` columns (physical names) — instead
434
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
435
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
436
+ * of each partition come out in `$sort` order (partitions interleave); every
437
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
438
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
439
+ * @since 0.1.147
440
+ */
441
+ declare function buildPartitionedSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
442
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
443
+ declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
275
444
  /**
276
445
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
277
446
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -376,6 +545,13 @@ declare function jsonDollarPath(path: readonly string[]): string;
376
545
  declare function sqlTimeZoneLiteral(tz: string): string;
377
546
  /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
378
547
  declare function toSqlValue(value: unknown): unknown;
548
+ /**
549
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
550
+ * foreign key over its physical columns ({@link fkColumns}); the target is
551
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
552
+ * identifier.
553
+ */
554
+ declare function foreignKeySql(quote: (name: string) => string, fk: TDbForeignKey): string;
379
555
  declare function refActionToSql(action: TDbReferentialAction): string;
380
556
  /** Returns a safe SQL DEFAULT literal for a given design type. */
381
557
  declare function defaultValueForType(designType: string): string;
@@ -403,7 +579,7 @@ declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref:
403
579
  * SQL function name of each single-name aggregate. `countDistinct` is not a
404
580
  * name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
405
581
  */
406
- declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct">, string>>;
582
+ declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct" | "first" | "last">, string>>;
407
583
  /**
408
584
  * The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
409
585
  * bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
@@ -420,7 +596,9 @@ declare function groupKeySql(dialect: SqlDialect, controls: DbControls, key: str
420
596
  * Builds a SELECT ... GROUP BY statement with aggregate functions.
421
597
  *
422
598
  * SELECT lists the plain grouped columns, then `<bucket expr> AS "alias"`
423
- * per calendar bucket, then the aggregates. Bucket expressions are
599
+ * per calendar bucket, then every computed alias (aggregates, row-level
600
+ * expression aggregates, `first` / `last`, group-level expressions — the
601
+ * order of `UniquSelect.computedAliases`). Bucket expressions are
424
602
  * parameter-free, so the bind parameters are exactly those of the same query
425
603
  * without buckets (WHERE, HAVING, LIMIT, OFFSET).
426
604
  */
@@ -429,7 +607,9 @@ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where:
429
607
  * Builds a COUNT query for the number of distinct groups — the groups that
430
608
  * survive `$having` when one is given (the same predicate the row query
431
609
  * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
432
- * executed.
610
+ * executed. The rows come straight from the table, unless `$having` reads a
611
+ * `first` / `last` value (or an expression over one): only then the window
612
+ * derived table is built.
433
613
  */
434
614
  declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
435
615
  //#endregion
@@ -446,4 +626,4 @@ declare function parseRegexString(value: unknown): {
446
626
  flags: string;
447
627
  };
448
628
  //#endregion
449
- export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
629
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
+ import { AtscriptExprNode, AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, DbError, TDbDefaultFn, TDbFieldMeta, TDbForeignKey, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
1
2
  import { FilterExpr, FilterVisitor } from "@uniqu/core";
2
- import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TResolvedBucket, TViewColumnMapping, TViewJsonType, TViewPlan, UniquSelect } from "@atscript/db";
3
3
  import { TDbAggregateFn } from "@atscript/db/agg";
4
4
 
5
5
  //#region src/dialect.d.ts
@@ -34,8 +34,10 @@ interface SqlDialect {
34
34
  geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
35
35
  /**
36
36
  * Calendar-bucket label expression over one column: TEXT `'YYYY-MM-DD'`
37
- * (the local calendar date of the bucket's first day in `b.tz`), or NULL for
38
- * a NULL source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
37
+ * (the local calendar date of the bucket's first day in `b.tz`; for unit
38
+ * `hour`, `'YYYY-MM-DDTHH:00'`, the local wall-clock hour — truncate the
39
+ * zone's wall time, never the UTC instant), or NULL for a NULL source or
40
+ * one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
39
41
  * `quotedCol` is already quoted; `b.fd` identifies the storage kind.
40
42
  *
41
43
  * The expression must be PARAMETER-FREE — inline the zone with
@@ -83,6 +85,56 @@ interface SqlDialect {
83
85
  * @since 0.1.136
84
86
  */
85
87
  jsonExtract?(quotedCol: string, path: readonly string[], type: TViewJsonType): string;
88
+ /**
89
+ * `expr` cast to an IEEE double — how a computed view column
90
+ * (`@db.compute`) evaluates every field / literal leaf, so `7 / 2 = 3.5`
91
+ * everywhere (no integer division, no DECIMAL rounding): SQLite
92
+ * `CAST(x AS REAL)`, MySQL `CAST(x AS DOUBLE)`, PostgreSQL
93
+ * `CAST(x AS DOUBLE PRECISION)`. Parameter-free. Dialects without it fail
94
+ * view sync with `computed view columns are not supported by this adapter`.
95
+ * @since 0.1.147
96
+ */
97
+ castDouble?(expr: string): string;
98
+ /**
99
+ * The aggregate functions that stand in for `MIN` / `MAX` over a BOOLEAN
100
+ * column on an engine that has no `MIN(boolean)` (PostgreSQL: `BOOL_AND` /
101
+ * `BOOL_OR`). Absent: `MIN` / `MAX` apply to booleans as to any column.
102
+ * @since 0.1.148
103
+ */
104
+ booleanAggregates?: {
105
+ min: string;
106
+ max: string;
107
+ };
108
+ /**
109
+ * Renders the "any value of the group" pick over an already rendered column
110
+ * — what a `first` / `last` derived column (constant within its group)
111
+ * aggregates through — for the column's source `field` (`undefined` when
112
+ * unknown). The dialect picks the cheapest aggregate its engine has for the
113
+ * column's type: a streaming `MIN` where one exists, a type-agnostic form
114
+ * only where it does not (PostgreSQL: `MIN` for ordered types, `BOOL_AND`
115
+ * for a boolean, `(ARRAY_AGG(x))[1]` for uuid, bytea, point, json, …).
116
+ * Absent: `MIN(x)`.
117
+ * @since 0.1.148
118
+ */
119
+ anyValue?(expr: string, field: TDbFieldMeta | undefined): string;
120
+ /**
121
+ * Maps a driver error of a grouped query to the `DbError` it means — a
122
+ * numeric overflow (`arithOverflowError` when the query has arithmetic
123
+ * expressions, else `numericOutOfRangeError`), an unknown calendar-bucket
124
+ * zone — or `undefined` to let it propagate unchanged. `arithmetic` is
125
+ * whether the query selects any arithmetic expression. Run by
126
+ * {@link mapQueryErrors}.
127
+ * @since 0.1.148
128
+ */
129
+ mapQueryError?(error: unknown, arithmetic: boolean): Error | undefined;
130
+ /**
131
+ * `true` when the database sorts NULL as the LARGEST value (PostgreSQL):
132
+ * first-row join order keys then render `ASC NULLS FIRST` /
133
+ * `DESC NULLS LAST`, keeping the uniform "NULL is the smallest value"
134
+ * ordering SQLite, MySQL and MongoDB have natively.
135
+ * @since 0.1.147
136
+ */
137
+ nullsSortLargest?: boolean;
86
138
  /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
87
139
  createViewPrefix: string;
88
140
  /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
@@ -93,6 +145,21 @@ interface SqlDialect {
93
145
  * (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
94
146
  */
95
147
  declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
148
+ /**
149
+ * Runs `fn`, rethrowing a driver error as the `DbError`
150
+ * {@link SqlDialect.mapQueryError} maps it to (any other error unchanged).
151
+ * `controls` of the query tell whether it has arithmetic expressions.
152
+ * @since 0.1.148
153
+ */
154
+ declare function mapQueryErrors<R>(dialect: SqlDialect, fn: () => Promise<R>, controls?: DbControls): Promise<R>;
155
+ /**
156
+ * One `ORDER BY` key with the uniform "NULL is the smallest value" ordering:
157
+ * `<expr> ASC` / `<expr> DESC`, plus `NULLS FIRST` / `NULLS LAST` on a
158
+ * dialect where NULL sorts largest ({@link SqlDialect.nullsSortLargest}).
159
+ * Shared by first-row joins and `first` / `last` aggregates.
160
+ * @since 0.1.148
161
+ */
162
+ declare function orderKeySql(dialect: SqlDialect, expr: string, desc: boolean): string;
96
163
  /**
97
164
  * Each JSON path segment wrapped in double quotes (`"a"`), for the dialects'
98
165
  * {@link SqlDialect.jsonExtract} path literals (`'$."a"."b"'`, `'{"a","b"}'`).
@@ -108,6 +175,34 @@ declare function quotedJsonPathSegments(path: readonly string[]): string[];
108
175
  declare const EMPTY_AND: TSqlFragment;
109
176
  declare const EMPTY_OR: TSqlFragment;
110
177
  //#endregion
178
+ //#region src/arith.d.ts
179
+ /** Why {@link renderArith} cannot render an expression. */
180
+ type TArithFailure = "no-cast" | "non-finite";
181
+ /**
182
+ * Renders a computed expression tree (`@db.compute` on a view, or a query-time
183
+ * arithmetic `$select` entry) as SQL, in IEEE double on every dialect: each
184
+ * literal is cast with {@link SqlDialect.castDouble}; `+ - *` render as
185
+ * `(l op r)`, `/` as `(l / NULLIF(r, 0))` (division by zero is NULL), unary
186
+ * minus as `(-x)` and `coalesce` as `COALESCE(…)`. `leaf` renders a field
187
+ * reference as raw SQL, which is cast to double here — or, as `{ double }`,
188
+ * SQL that already is a double (a nested computed column) and stays as is.
189
+ *
190
+ * The one renderer of both paths, so declared and query-time arithmetic
191
+ * cannot diverge.
192
+ *
193
+ * @param fail - the error to throw for a failure (default `DbError`:
194
+ * `AGG_EXPR_NOT_SUPPORTED` without `castDouble`, `INVALID_QUERY` for a
195
+ * non-finite literal).
196
+ * @since 0.1.148
197
+ */
198
+ declare function renderArith(dialect: SqlDialect, node: AtscriptExprNode, leaf: (field: string) => string | {
199
+ double: string;
200
+ }, fail?: (reason: TArithFailure) => Error): string;
201
+ /** The error of a numeric overflow that is no arithmetic expression's (`INVALID_QUERY`, `path` `""`). */
202
+ declare function numericOutOfRangeError(): DbError;
203
+ /** The error of a double overflow in aggregate arithmetic (`INVALID_QUERY`, `path` `$select`). */
204
+ declare function arithOverflowError(): DbError;
205
+ //#endregion
111
206
  //#region src/filter-builder.d.ts
112
207
  interface TFilterVisitorOptions {
113
208
  /**
@@ -117,6 +212,35 @@ interface TFilterVisitorOptions {
117
212
  * expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
118
213
  */
119
214
  columnRef?: (field: string) => string;
215
+ /**
216
+ * The quoted outer table or alias that relational predicates
217
+ * (`{ nav: { $some | $none: … } }`) correlate to: their `EXISTS` subqueries
218
+ * compare the related rows with `<qualifier>."<column>"`. Defaults to the
219
+ * predicate's source table (`dialect.quoteTable(node.source.table)`), which
220
+ * is right for every statement whose FROM is the bare table; a statement
221
+ * that aliases its FROM (`FROM "items" AS "t"`) must pass that alias here.
222
+ *
223
+ * @since 0.1.147
224
+ */
225
+ qualifier?: string;
226
+ /**
227
+ * Alias sequence of the relational-predicate subqueries (`_rf1`, `_rf2`, …),
228
+ * shared by the visitors of one statement so nested predicates get distinct
229
+ * aliases. A visitor created without one starts its own.
230
+ *
231
+ * @internal
232
+ * @since 0.1.147
233
+ */
234
+ aliasSeq?: TRelationAliasSeq;
235
+ }
236
+ /**
237
+ * Alias counter of the relational-predicate subqueries of one statement.
238
+ *
239
+ * @internal
240
+ * @since 0.1.147
241
+ */
242
+ interface TRelationAliasSeq {
243
+ n: number;
120
244
  }
121
245
  /**
122
246
  * Creates a dialect-specific filter visitor for `walkFilter`.
@@ -124,8 +248,18 @@ interface TFilterVisitorOptions {
124
248
  declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
125
249
  /**
126
250
  * Translates a filter expression into a parameterized SQL WHERE clause.
251
+ *
252
+ * Relational predicates (`{ nav: { $some | $none: … } }`, resolved by the
253
+ * core into `ResolvedRelationFilter` operands) render as correlated
254
+ * `[NOT] EXISTS (…)` subqueries; `opts.qualifier` names the outer table or
255
+ * alias they correlate to (default: the source table — see
256
+ * {@link TFilterVisitorOptions.qualifier}). `opts.columnRef` overrides how
257
+ * the filter's own columns render (e.g. `t."col"` for an aliased FROM).
258
+ * Placeholders stay positional `?` in textual order.
259
+ *
260
+ * @param opts - since 0.1.147
127
261
  */
128
- declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
262
+ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
129
263
  //#endregion
130
264
  //#region src/geo.d.ts
131
265
  /**
@@ -135,6 +269,13 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
135
269
  * across dialects, so the public name can't be used directly.
136
270
  */
137
271
  declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
272
+ /**
273
+ * Alias of the searched table inside the geo and vector search statements —
274
+ * a filter rendered into them correlates its relational predicates to it
275
+ * (`buildWhere(filter, { qualifier: dialect.quoteTable(SEARCH_SOURCE_ALIAS) })`).
276
+ * @since 0.1.147
277
+ */
278
+ declare const SEARCH_SOURCE_ALIAS = "t";
138
279
  /** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
139
280
  interface TGeoSearchControls {
140
281
  $limit?: number;
@@ -260,6 +401,15 @@ declare function buildInsert(dialect: SqlDialect, table: string, data: Record<st
260
401
  * some).
261
402
  */
262
403
  declare function insertManyColumns(rows: readonly Record<string, unknown>[]): string[];
404
+ /**
405
+ * Splits `rows` into batches that stay under the driver's bind-parameter limit
406
+ * (PostgreSQL ~65535, MySQL packet size): `maxParams` (default 60000) divided
407
+ * by the column count. Returns the shared column union and the batches.
408
+ */
409
+ declare function chunkInsertRows(rows: readonly Record<string, unknown>[], maxParams?: number): {
410
+ columns: string[];
411
+ batches: Record<string, unknown>[][];
412
+ };
263
413
  /**
264
414
  * Builds a multi-row `INSERT … VALUES (…), (…)` statement over `columns`
265
415
  * (default: {@link insertManyColumns} of `rows`). A row lacking a column gets
@@ -272,6 +422,25 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
272
422
  * Builds a SELECT statement with optional sort, limit, offset, projection.
273
423
  */
274
424
  declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
425
+ /**
426
+ * The row-number column {@link buildPartitionedSelect} adds to every row;
427
+ * {@link stripPartitionRowNumber} removes it from the result rows.
428
+ * @since 0.1.147
429
+ */
430
+ declare const PARTITION_ROW_NUMBER_ALIAS = "__atscript_rn";
431
+ /**
432
+ * Builds a SELECT whose `$skip` / `$limit` apply to each partition — the rows
433
+ * sharing the values of the `partitionBy` columns (physical names) — instead
434
+ * of to the whole result: a `ROW_NUMBER() OVER (PARTITION BY … ORDER BY
435
+ * <$sort>)` window in a derived table, filtered on the row number. The rows
436
+ * of each partition come out in `$sort` order (partitions interleave); every
437
+ * row carries {@link PARTITION_ROW_NUMBER_ALIAS}. Window functions need
438
+ * SQLite ≥ 3.25, MySQL ≥ 8.0 or MariaDB ≥ 10.2.
439
+ * @since 0.1.147
440
+ */
441
+ declare function buildPartitionedSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls, partitionBy: readonly string[]): TSqlFragment;
442
+ /** Removes {@link PARTITION_ROW_NUMBER_ALIAS} from rows read by {@link buildPartitionedSelect}. */
443
+ declare function stripPartitionRowNumber<R extends Record<string, unknown>>(rows: R[]): R[];
275
444
  /**
276
445
  * Marker value for {@link buildUpdate}: the column is assigned its DDL
277
446
  * `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
@@ -376,6 +545,13 @@ declare function jsonDollarPath(path: readonly string[]): string;
376
545
  declare function sqlTimeZoneLiteral(tz: string): string;
377
546
  /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
378
547
  declare function toSqlValue(value: unknown): unknown;
548
+ /**
549
+ * `FOREIGN KEY (…) REFERENCES <target> (…)[ ON DELETE …][ ON UPDATE …]` of a
550
+ * foreign key over its physical columns ({@link fkColumns}); the target is
551
+ * schema-qualified when it declares `@db.schema`. `quote` quotes one
552
+ * identifier.
553
+ */
554
+ declare function foreignKeySql(quote: (name: string) => string, fk: TDbForeignKey): string;
379
555
  declare function refActionToSql(action: TDbReferentialAction): string;
380
556
  /** Returns a safe SQL DEFAULT literal for a given design type. */
381
557
  declare function defaultValueForType(designType: string): string;
@@ -403,7 +579,7 @@ declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref:
403
579
  * SQL function name of each single-name aggregate. `countDistinct` is not a
404
580
  * name but a form (`COUNT(DISTINCT x)`) — see {@link renderAggCall}.
405
581
  */
406
- declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct">, string>>;
582
+ declare const AGG_FN_SQL: Readonly<Record<Exclude<TDbAggregateFn, "countDistinct" | "first" | "last">, string>>;
407
583
  /**
408
584
  * The SQL a `$groupBy` key renders as: a calendar-bucket alias renders the
409
585
  * bucket EXPRESSION (`dialect.calendarBucket`), anything else the quoted
@@ -420,7 +596,9 @@ declare function groupKeySql(dialect: SqlDialect, controls: DbControls, key: str
420
596
  * Builds a SELECT ... GROUP BY statement with aggregate functions.
421
597
  *
422
598
  * SELECT lists the plain grouped columns, then `<bucket expr> AS "alias"`
423
- * per calendar bucket, then the aggregates. Bucket expressions are
599
+ * per calendar bucket, then every computed alias (aggregates, row-level
600
+ * expression aggregates, `first` / `last`, group-level expressions — the
601
+ * order of `UniquSelect.computedAliases`). Bucket expressions are
424
602
  * parameter-free, so the bind parameters are exactly those of the same query
425
603
  * without buckets (WHERE, HAVING, LIMIT, OFFSET).
426
604
  */
@@ -429,7 +607,9 @@ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where:
429
607
  * Builds a COUNT query for the number of distinct groups — the groups that
430
608
  * survive `$having` when one is given (the same predicate the row query
431
609
  * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
432
- * executed.
610
+ * executed. The rows come straight from the table, unless `$having` reads a
611
+ * `first` / `last` value (or an expression over one): only then the window
612
+ * derived table is built.
433
613
  */
434
614
  declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
435
615
  //#endregion
@@ -446,4 +626,4 @@ declare function parseRegexString(value: unknown): {
446
626
  flags: string;
447
627
  };
448
628
  //#endregion
449
- export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
629
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };