uql-orm 0.56.0 → 0.58.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/README.md +7 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +196 -33
- package/dist/dialect/abstractSqlDialect.js +410 -203
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +23 -48
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +26 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +54 -42
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +2 -2
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
- package/dist/migrate/ddl/pgIndexDdl.js +29 -12
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +4 -4
- package/dist/migrate/schemaGenerator.d.ts +8 -8
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +146 -104
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -106
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +35 -129
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +4 -17
- package/dist/querier/abstractSqlQuerier.js +40 -50
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +32 -3
- package/dist/schema/schemaASTDiffer.js +5 -5
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +38 -7
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +3 -3
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +110 -69
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +41 -72
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +43 -34
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +4 -4
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/relationQuery.util.d.ts +12 -65
- package/dist/util/relationQuery.util.js +27 -81
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
|
-
import { type ParentPartition } from '../util/index.js';
|
|
4
3
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
5
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
5
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -58,19 +57,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
58
57
|
*/
|
|
59
58
|
private applyVectorTuning;
|
|
60
59
|
protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
61
|
-
/**
|
|
62
|
-
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
63
|
-
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
64
|
-
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
65
|
-
*
|
|
66
|
-
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
67
|
-
* compound branch, and the wrapper costs nothing elsewhere.
|
|
68
|
-
*
|
|
69
|
-
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
70
|
-
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
71
|
-
* query, so neither can reach here.
|
|
72
|
-
*/
|
|
73
|
-
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
74
60
|
/**
|
|
75
61
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
76
62
|
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
@@ -107,9 +93,10 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
107
93
|
*/
|
|
108
94
|
private hydrateAll;
|
|
109
95
|
/**
|
|
110
|
-
* One row of {@link hydrateAll}.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
96
|
+
* One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
|
|
97
|
+
* joined and unflattened, there only when its key is, since an unmatched join still fills a computed
|
|
98
|
+
* column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
|
|
99
|
+
* Each is an object of its own, so the walk reaches none twice.
|
|
113
100
|
*/
|
|
114
101
|
private hydrateFields;
|
|
115
102
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
3
|
import { getMeta, idOf, namesKey } from '../entity/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
5
|
+
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, idOnlyQuery, isAutoIncrement, isPagedQuery, isRecord, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
6
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
7
|
import { enrichError } from './queryError.js';
|
|
7
8
|
/**
|
|
@@ -153,24 +154,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
async internalFindMany(entity, q, opts) {
|
|
156
|
-
return this.hydrateRows(entity,
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
160
|
-
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
161
|
-
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
162
|
-
*
|
|
163
|
-
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
164
|
-
* compound branch, and the wrapper costs nothing elsewhere.
|
|
165
|
-
*
|
|
166
|
-
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
167
|
-
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
168
|
-
* query, so neither can reach here.
|
|
169
|
-
*/
|
|
170
|
-
async internalFindManyPerParent(entity, q, partition) {
|
|
171
|
-
const ctx = this.dialect.createContext();
|
|
172
|
-
this.dialect.findPerParent(ctx, entity, q, partition);
|
|
173
|
-
return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
|
|
157
|
+
return this.hydrateRows(entity, await this.selectRows(entity, q, opts));
|
|
174
158
|
}
|
|
175
159
|
/**
|
|
176
160
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
@@ -208,7 +192,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
208
192
|
for (const row of rows) {
|
|
209
193
|
delete row[TOTAL_ALIAS];
|
|
210
194
|
}
|
|
211
|
-
return [
|
|
195
|
+
return [this.hydrateRows(entity, rows), total];
|
|
212
196
|
}
|
|
213
197
|
async selectRows(entity, q, opts, totalAlias) {
|
|
214
198
|
this.assertLockable(entity, q);
|
|
@@ -222,12 +206,9 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
222
206
|
this.dialect.find(ctx, entity, q, opts, totalAlias);
|
|
223
207
|
return this.all(ctx.sql, ctx.values);
|
|
224
208
|
}
|
|
225
|
-
|
|
209
|
+
hydrateRows(entity, rows) {
|
|
226
210
|
const founds = unflatObjects(rows);
|
|
227
211
|
this.hydrateAll(entity, founds);
|
|
228
|
-
if (q.$populate) {
|
|
229
|
-
await this.fillToManyRelations(entity, founds, q.$populate);
|
|
230
|
-
}
|
|
231
212
|
return founds;
|
|
232
213
|
}
|
|
233
214
|
async *internalFindManyStream(entity, q, opts) {
|
|
@@ -237,10 +218,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
237
218
|
await this.applyVectorTuning(entity, q);
|
|
238
219
|
}
|
|
239
220
|
const meta = getMeta(entity);
|
|
240
|
-
const { toManyKeys } = getRelationRequestSummary(meta, q.$populate);
|
|
241
|
-
if (toManyKeys.length) {
|
|
242
|
-
throw new TypeError(`findManyStream does not load to-many relations (${toManyKeys.join(', ')}). Use findMany so fillToManyRelations can run, or omit those keys from the stream query.`);
|
|
243
|
-
}
|
|
244
221
|
// The one path that does not go through `all`/`run`, so it connects on its own: streaming first on
|
|
245
222
|
// a freshly acquired querier used to reach `getConn()` with nothing acquired.
|
|
246
223
|
await this.lazyConnect();
|
|
@@ -276,23 +253,20 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
276
253
|
* rows; the per-cell decode is `decodeColumn`. Both live with the dialect, because a `sparsevec` is
|
|
277
254
|
* only sparse on Postgres.
|
|
278
255
|
*/
|
|
279
|
-
hydrateAll(entity, dtos
|
|
256
|
+
hydrateAll(entity, dtos) {
|
|
280
257
|
const meta = getMeta(entity);
|
|
281
258
|
const fields = this.dialect.hydratableFields(entity);
|
|
282
259
|
for (const dto of dtos) {
|
|
283
|
-
this.hydrateFields(meta, fields, dto
|
|
260
|
+
this.hydrateFields(meta, fields, dto);
|
|
284
261
|
}
|
|
285
262
|
}
|
|
286
263
|
/**
|
|
287
|
-
* One row of {@link hydrateAll}.
|
|
288
|
-
*
|
|
289
|
-
*
|
|
264
|
+
* One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
|
|
265
|
+
* joined and unflattened, there only when its key is, since an unmatched join still fills a computed
|
|
266
|
+
* column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
|
|
267
|
+
* Each is an object of its own, so the walk reaches none twice.
|
|
290
268
|
*/
|
|
291
|
-
hydrateFields(meta, fields, dto
|
|
292
|
-
if (!dto || typeof dto !== 'object' || visited?.has(dto)) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
visited?.add(dto);
|
|
269
|
+
hydrateFields(meta, fields, dto) {
|
|
296
270
|
const row = dto;
|
|
297
271
|
for (const [key, kind] of fields) {
|
|
298
272
|
const value = row[key];
|
|
@@ -300,23 +274,39 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
300
274
|
row[key] = decodeColumn(value, kind);
|
|
301
275
|
}
|
|
302
276
|
}
|
|
277
|
+
// A tally read inside the statement comes back as its driver reads a COUNT, which on some is text.
|
|
278
|
+
const counts = row[COUNT_RESULT_KEY];
|
|
279
|
+
if (isRecord(counts)) {
|
|
280
|
+
for (const relKey in counts) {
|
|
281
|
+
counts[relKey] = Number(counts[relKey]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
303
284
|
// The value is read before the relation's target is resolved: a query that populated nothing
|
|
304
285
|
// still walks every relation the entity declares, and `rel.entity()` is a call per row per
|
|
305
286
|
// relation that only the populated ones need.
|
|
306
287
|
for (const key in meta.relations) {
|
|
307
288
|
const value = row[key];
|
|
308
|
-
if (!value
|
|
289
|
+
if (!value)
|
|
309
290
|
continue;
|
|
310
291
|
const rel = meta.relations[key];
|
|
311
292
|
if (!rel)
|
|
312
293
|
continue;
|
|
313
|
-
visited ??= new WeakSet([dto]);
|
|
314
294
|
const relEntity = rel.entity();
|
|
315
|
-
if (Array.isArray(value)) {
|
|
316
|
-
|
|
317
|
-
|
|
295
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
296
|
+
// A to-many's rows, as flat as a statement's own.
|
|
297
|
+
const rows = unflatObjects(typeof value === 'string' ? JSON.parse(value) : value);
|
|
298
|
+
row[key] = rows;
|
|
299
|
+
this.hydrateAll(relEntity, rows);
|
|
300
|
+
}
|
|
301
|
+
else if (isRecord(value)) {
|
|
302
|
+
const relMeta = getMeta(relEntity);
|
|
303
|
+
if (value[relMeta.ids[0]] == null) {
|
|
304
|
+
delete row[key];
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.hydrateFields(relMeta, this.dialect.hydratableFields(relEntity), value);
|
|
308
|
+
}
|
|
318
309
|
}
|
|
319
|
-
this.hydrateFields(getMeta(relEntity), this.dialect.hydratableFields(relEntity), value, visited);
|
|
320
310
|
}
|
|
321
311
|
}
|
|
322
312
|
/**
|
|
@@ -339,17 +329,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
339
329
|
async internalAggregate(entity, q, opts) {
|
|
340
330
|
const ctx = this.dialect.createContext();
|
|
341
331
|
this.dialect.aggregate(ctx, entity, q, opts);
|
|
342
|
-
|
|
343
|
-
const res = await this.all(ctx.sql, ctx.values);
|
|
332
|
+
const rows = await this.all(ctx.sql, ctx.values);
|
|
344
333
|
const hydratable = this.dialect.hydratableAggregates(entity, q);
|
|
345
|
-
for (const row of
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const cells = row;
|
|
346
336
|
for (const [alias, kind] of hydratable) {
|
|
347
|
-
if (
|
|
348
|
-
|
|
337
|
+
if (cells[alias] != null) {
|
|
338
|
+
cells[alias] = decodeColumn(cells[alias], kind);
|
|
349
339
|
}
|
|
350
340
|
}
|
|
351
341
|
}
|
|
352
|
-
return
|
|
342
|
+
return rows;
|
|
353
343
|
}
|
|
354
344
|
async internalInsertMany(entity, rows) {
|
|
355
345
|
const meta = getMeta(entity);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Canonical types (dialect-agnostic)
|
|
7
7
|
* - TypeScript types (for entity generation)
|
|
8
8
|
*/
|
|
9
|
-
import { columnFamily } from '../util/field.util.js';
|
|
9
|
+
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
10
10
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
11
11
|
export function isVectorCategory(category) {
|
|
12
12
|
return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
|
|
@@ -395,9 +395,9 @@ export function fieldOptionsToCanonical(options) {
|
|
|
395
395
|
case 'numeric':
|
|
396
396
|
// BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
|
|
397
397
|
// to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
|
|
398
|
-
return
|
|
399
|
-
? { category: '
|
|
400
|
-
: { category: '
|
|
398
|
+
return isIntegerColumn(options)
|
|
399
|
+
? { category: 'integer', size: 'big' }
|
|
400
|
+
: { category: 'decimal', precision: options.precision, scale: options.scale };
|
|
401
401
|
case 'boolean':
|
|
402
402
|
return { category: 'boolean' };
|
|
403
403
|
case 'date':
|
|
@@ -61,20 +61,20 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
61
61
|
if (comparableEntries) {
|
|
62
62
|
const [sourceColumns, targetColumns] = [source.entries, target.entries].map((entries) => entries.map((entry) => entrySignature(entry, facets)).join(', '));
|
|
63
63
|
if (sourceColumns !== targetColumns) {
|
|
64
|
-
differences.push(`columns: (${targetColumns})
|
|
64
|
+
differences.push(`columns: (${targetColumns}) -> (${sourceColumns})`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
if (source.unique !== target.unique) {
|
|
68
|
-
differences.push(`unique: ${target.unique}
|
|
68
|
+
differences.push(`unique: ${target.unique} -> ${source.unique}`);
|
|
69
69
|
}
|
|
70
70
|
if (facets.has('accessMethod') && (source.type ?? 'btree') !== (target.type ?? 'btree')) {
|
|
71
|
-
differences.push(`type: ${target.type ?? 'btree'}
|
|
71
|
+
differences.push(`type: ${target.type ?? 'btree'} -> ${source.type ?? 'btree'}`);
|
|
72
72
|
}
|
|
73
73
|
if (facets.has('include')) {
|
|
74
74
|
// Order carries no meaning in an `INCLUDE` list, so it is compared as a set.
|
|
75
75
|
const [sourceInclude, targetInclude] = [source.include ?? [], target.include ?? []].map((columns) => [...columns].sort().join(', '));
|
|
76
76
|
if (sourceInclude !== targetInclude) {
|
|
77
|
-
differences.push(`include: (${targetInclude})
|
|
77
|
+
differences.push(`include: (${targetInclude}) -> (${sourceInclude})`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
return differences;
|
|
@@ -15,9 +15,9 @@ import { type CanonicalType, type ForeignKeyAction } from './types.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface BuildSchemaASTOptions {
|
|
17
17
|
/** Custom resolver for a table's own name, unqualified. */
|
|
18
|
-
resolveTableName?: (meta: EntityMeta<
|
|
18
|
+
resolveTableName?: (meta: EntityMeta<object>) => string;
|
|
19
19
|
/** Custom resolver for the schema a table lives in; `undefined` leaves it unqualified. */
|
|
20
|
-
resolveSchema?: (meta: EntityMeta<
|
|
20
|
+
resolveSchema?: (meta: EntityMeta<object>) => string | undefined;
|
|
21
21
|
/** Custom column name resolver */
|
|
22
22
|
resolveColumnName?: (key: string, field: FieldOptions) => string;
|
|
23
23
|
/** Naming strategy to use */
|
|
@@ -31,7 +31,7 @@ export interface BuildSchemaASTOptions {
|
|
|
31
31
|
* Three passes, because each needs the one before it to have finished for *every* entity: a relation
|
|
32
32
|
* resolves against a table another entity declares, and an index against the columns of its own.
|
|
33
33
|
*/
|
|
34
|
-
export declare function buildSchemaAST(entities: readonly Type<
|
|
34
|
+
export declare function buildSchemaAST(entities: readonly Type<object>[], options?: BuildSchemaASTOptions): SchemaAST;
|
|
35
35
|
/**
|
|
36
36
|
* Resolve the canonical type for a field, inheriting from the referenced
|
|
37
37
|
* entity's primary key when the field is a foreign-key reference
|
|
@@ -168,8 +168,8 @@ function addRelationshipsFromEntity(ctx, meta) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* Add indexes from field options (`@Field({ index })`)
|
|
172
|
-
*
|
|
171
|
+
* Add indexes from field options (`@Field({ index })`), from `@Index`, and for every foreign
|
|
172
|
+
* key none of those already serves.
|
|
173
173
|
*/
|
|
174
174
|
function addIndexesFromEntity(ctx, meta) {
|
|
175
175
|
const table = tableOf(ctx, meta);
|
|
@@ -194,6 +194,35 @@ function addIndexesFromEntity(ctx, meta) {
|
|
|
194
194
|
for (const idxMeta of meta.indexes ?? []) {
|
|
195
195
|
addCompositeIndex(ctx, table, meta, idxMeta);
|
|
196
196
|
}
|
|
197
|
+
addForeignKeyIndexes(ctx, meta, table);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* An index over each foreign key the table owns, unless one already leads with its columns or a field
|
|
201
|
+
* of it says `index: false`. A relation looks its rows up by these columns, and MySQL alone indexes
|
|
202
|
+
* them on its own. Last, so it sees every index the entity declared.
|
|
203
|
+
*/
|
|
204
|
+
function addForeignKeyIndexes(ctx, meta, table) {
|
|
205
|
+
const optedOut = new Set(definedEntries(meta.fields).flatMap(([key, field]) => field.index === false ? [ctx.resolveColumnName(key, field)] : []));
|
|
206
|
+
for (const { from } of table.outgoingRelations) {
|
|
207
|
+
const columns = from.columns.map((column) => column.name);
|
|
208
|
+
if (columns.some((column) => optedOut.has(column)) || isIndexedBy(table, columns))
|
|
209
|
+
continue;
|
|
210
|
+
ctx.ast.addIndex({
|
|
211
|
+
name: derivedIndexName(table.name, columns),
|
|
212
|
+
table,
|
|
213
|
+
entries: columns.map((column) => ({ column })),
|
|
214
|
+
unique: false,
|
|
215
|
+
source: 'entity',
|
|
216
|
+
syncStatus: 'entity_only',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Whether the key, a unique column or an index already leads with `columns`, which is all a lookup needs. */
|
|
221
|
+
function isIndexedBy(table, columns) {
|
|
222
|
+
const leads = (indexed) => columns.every((column, at) => indexed[at] === column);
|
|
223
|
+
return (leads(table.primaryKey.map((column) => column.name)) ||
|
|
224
|
+
(columns.length === 1 && table.columns.get(columns[0])?.isUnique === true) ||
|
|
225
|
+
table.indexes.some((index) => leads(index.entries.map((entry) => entry.expression || entry.jsonPath || entry.jsonArray ? undefined : entry.column))));
|
|
197
226
|
}
|
|
198
227
|
/** An `include` column is named like any other, so a naming strategy has to reach it too. */
|
|
199
228
|
function resolveIncludeColumn(ctx, meta, column) {
|
|
@@ -201,7 +230,7 @@ function resolveIncludeColumn(ctx, meta, column) {
|
|
|
201
230
|
return field ? ctx.resolveColumnName(column, field) : column;
|
|
202
231
|
}
|
|
203
232
|
/**
|
|
204
|
-
* One `@Index
|
|
233
|
+
* One `@Index`. Its entries keep the authored form (expression, prefix length, order) with
|
|
205
234
|
* names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
|
|
206
235
|
* subset, which is what diffing and introspection compare.
|
|
207
236
|
*/
|
|
@@ -179,7 +179,7 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
179
179
|
const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
|
|
180
180
|
const typeChanged = !generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type));
|
|
181
181
|
if (typeChanged) {
|
|
182
|
-
differences.push(`type: ${formatType(source.type)}
|
|
182
|
+
differences.push(`type: ${formatType(source.type)} -> ${formatType(target.type)}`);
|
|
183
183
|
}
|
|
184
184
|
// Signedness is the one thing compared on a generated key, because it is the one part of the serial
|
|
185
185
|
// spelling that does round trip - and a key left unsigned refuses every foreign key pointing at it,
|
|
@@ -187,14 +187,14 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
187
187
|
// a database created before the serial became signed could never gain a foreign key.
|
|
188
188
|
const signednessChanged = generatedType && !!source.type.unsigned !== !!target.type.unsigned;
|
|
189
189
|
if (signednessChanged) {
|
|
190
|
-
differences.push(`type: ${formatType(source.type)}
|
|
190
|
+
differences.push(`type: ${formatType(source.type)} -> ${formatType(target.type)}`);
|
|
191
191
|
}
|
|
192
192
|
if (!impliedNotNull && source.nullable !== target.nullable) {
|
|
193
|
-
differences.push(`nullable: ${target.nullable}
|
|
193
|
+
differences.push(`nullable: ${target.nullable} -> ${source.nullable}`);
|
|
194
194
|
}
|
|
195
195
|
// Compare unique constraint
|
|
196
196
|
if (source.isUnique !== target.isUnique) {
|
|
197
|
-
differences.push(`unique: ${target.isUnique}
|
|
197
|
+
differences.push(`unique: ${target.isUnique} -> ${source.isUnique}`);
|
|
198
198
|
}
|
|
199
199
|
// Four things are deliberately not compared, all for one reason: a difference here could only be
|
|
200
200
|
// reported, never settled, because no statement this generator emits would change it.
|
|
@@ -206,7 +206,7 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
206
206
|
// The same rule `describeIndexDifferences` follows for what it cannot read.
|
|
207
207
|
// Compare default values (if both defined)
|
|
208
208
|
if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
|
|
209
|
-
differences.push(`default: ${target.defaultValue ?? 'NULL'}
|
|
209
|
+
differences.push(`default: ${target.defaultValue ?? 'NULL'} -> ${source.defaultValue ?? 'NULL'}`);
|
|
210
210
|
}
|
|
211
211
|
if (differences.length === 0) {
|
|
212
212
|
return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
|
|
1
|
+
import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
2
2
|
import type { DialectFeatures, EntityMeta, FieldOptions, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
3
|
export declare class SqliteDialect extends AbstractSqlDialect {
|
|
4
4
|
/** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
|
|
@@ -16,6 +16,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
16
16
|
/** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
|
|
17
17
|
readonly supportsRowLocks = false;
|
|
18
18
|
readonly booleanLiteral = "integer";
|
|
19
|
+
/** A function call takes 127 arguments before SQLite 3.48, as libSQL and Turso embed. */
|
|
20
|
+
readonly maxFunctionArgs: number;
|
|
19
21
|
readonly insertIdSource = "returning";
|
|
20
22
|
/**
|
|
21
23
|
* The [sqlite-vec](https://github.com/asg017/sqlite-vec) functions, which need that extension
|
|
@@ -36,6 +38,23 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
36
38
|
* SQLite's own spelling of "no limit", where the MySQL family uses its largest `BIGINT`.
|
|
37
39
|
*/
|
|
38
40
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
41
|
+
/** `json_group_array` of each row's object, ordered by the sort terms carried out beside them. */
|
|
42
|
+
protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
|
|
43
|
+
/**
|
|
44
|
+
* `json_object` of each key and its column. A row wider than one call takes inserts the rest into it,
|
|
45
|
+
* addressing each key as one path segment.
|
|
46
|
+
*/
|
|
47
|
+
protected jsonObject(pairs: DerivedRelation['pairs']): string;
|
|
48
|
+
/**
|
|
49
|
+
* An integer crosses JSON as its exact text, which JSON would round past 2^53, and bytes as hex, which
|
|
50
|
+
* `json_object` cannot hold at all. A real stays a number, since SQLite writes one to text with 15 digits.
|
|
51
|
+
*/
|
|
52
|
+
protected readonly carriedFields: {
|
|
53
|
+
numeric: (expr: string, field: FieldOptions) => string;
|
|
54
|
+
blob: (expr: string) => string;
|
|
55
|
+
};
|
|
56
|
+
/** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
|
|
57
|
+
protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
|
|
39
58
|
/**
|
|
40
59
|
* FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
|
|
41
60
|
* FTS5 virtual table (UQL does not create those; declare it outside your entities).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { AbstractSqlDialect, } from '../dialect/abstractSqlDialect.js';
|
|
2
|
+
import { JSON_ELEM_ALIAS, JSON_PULL_ALIAS } from '../dialect/aliases.js';
|
|
3
|
+
import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
4
|
+
import { chainedCall, groupsPerCall, jsonAssignCall, jsonElemExists, jsonPath, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
|
|
4
5
|
import { textSearchFields } from '../util/dialect.util.js';
|
|
6
|
+
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
5
7
|
export class SqliteDialect extends AbstractSqlDialect {
|
|
6
8
|
/** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
|
|
7
9
|
featureDefaults = {
|
|
@@ -37,6 +39,8 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
37
39
|
/** SQLite locks the whole database, not rows, so `$lock` has nothing to map onto. */
|
|
38
40
|
supportsRowLocks = false;
|
|
39
41
|
booleanLiteral = 'integer';
|
|
42
|
+
/** A function call takes 127 arguments before SQLite 3.48, as libSQL and Turso embed. */
|
|
43
|
+
maxFunctionArgs = 127;
|
|
40
44
|
// SQLite supports `RETURNING` (including on `INSERT ... ON CONFLICT`), so IDs are exact per row.
|
|
41
45
|
insertIdSource = 'returning';
|
|
42
46
|
/**
|
|
@@ -82,6 +86,33 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
82
86
|
}
|
|
83
87
|
super.pager(ctx, opts);
|
|
84
88
|
}
|
|
89
|
+
/** `json_group_array` of each row's object, ordered by the sort terms carried out beside them. */
|
|
90
|
+
appendRelationArray(ctx, rows) {
|
|
91
|
+
const { from, pairs, order } = this.derivedRelation(ctx, rows);
|
|
92
|
+
ctx.append(`(SELECT json_group_array(${this.jsonObject(pairs)}${order ? ` ORDER BY ${order}` : ''}) FROM ${from})`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `json_object` of each key and its column. A row wider than one call takes inserts the rest into it,
|
|
96
|
+
* addressing each key as one path segment.
|
|
97
|
+
*/
|
|
98
|
+
jsonObject(pairs) {
|
|
99
|
+
const perCall = groupsPerCall(this.maxFunctionArgs, 2);
|
|
100
|
+
const object = `json_object(${this.jsonObjectArgs(pairs.slice(0, perCall))})`;
|
|
101
|
+
const inserts = pairs.slice(perCall).map(([key, sql]) => `${this.escape(`$."${key}"`)}, ${sql}`);
|
|
102
|
+
return chainedCall('json_insert', object, inserts, 2, this.maxFunctionArgs);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* An integer crosses JSON as its exact text, which JSON would round past 2^53, and bytes as hex, which
|
|
106
|
+
* `json_object` cannot hold at all. A real stays a number, since SQLite writes one to text with 15 digits.
|
|
107
|
+
*/
|
|
108
|
+
carriedFields = {
|
|
109
|
+
numeric: (expr, field) => (isIntegerColumn(field) ? `CAST(${expr} AS TEXT)` : expr),
|
|
110
|
+
blob: (expr) => `${this.escape(BYTES_PREFIX)} || hex(${expr})`,
|
|
111
|
+
};
|
|
112
|
+
/** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
|
|
113
|
+
hydrateKind(field) {
|
|
114
|
+
return columnFamily(field?.type) === 'date' ? undefined : super.hydrateKind(field);
|
|
115
|
+
}
|
|
85
116
|
/**
|
|
86
117
|
* FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
|
|
87
118
|
* FTS5 virtual table (UQL does not create those; declare it outside your entities).
|
|
@@ -104,7 +135,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
104
135
|
* vs `"a"`), flattens booleans to 0/1, and stringifies objects.
|
|
105
136
|
*/
|
|
106
137
|
jsonAll(ctx, jsonField, value) {
|
|
107
|
-
const alias = ctx.
|
|
138
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
108
139
|
const from = this.jsonElemFrom(jsonField, [], alias);
|
|
109
140
|
const conditions = value.map((val) => jsonElemExists(from, [`${jsonField} -> ${alias}.fullkey = ${this.jsonScalarParam(ctx, val)}`]));
|
|
110
141
|
return `(${conditions.join(' AND ')})`;
|
|
@@ -143,7 +174,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
143
174
|
return `JSON_REPLACE(${expr}, ${path}, (${kept}))`;
|
|
144
175
|
}
|
|
145
176
|
jsonSet(ctx, expr, set, field) {
|
|
146
|
-
return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set);
|
|
177
|
+
return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', jsonSetTarget(expr, field, `'{}'`), set, this.maxFunctionArgs);
|
|
147
178
|
}
|
|
148
179
|
/**
|
|
149
180
|
* `[#]` appends, creating the array when the key is absent.
|
|
@@ -153,9 +184,9 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
153
184
|
* so it silently drops the element when the array already exists.
|
|
154
185
|
*/
|
|
155
186
|
jsonPush(ctx, expr, push) {
|
|
156
|
-
return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, '[#]');
|
|
187
|
+
return jsonAssignCall((value) => this.jsonScalarParam(ctx, value), 'JSON_SET', expr, push, this.maxFunctionArgs, '[#]');
|
|
157
188
|
}
|
|
158
189
|
jsonUnset(_ctx, expr, unset) {
|
|
159
|
-
return jsonRemoveCall('JSON_REMOVE', expr, unset);
|
|
190
|
+
return jsonRemoveCall('JSON_REMOVE', expr, unset, this.maxFunctionArgs);
|
|
160
191
|
}
|
|
161
192
|
}
|
|
@@ -12,4 +12,6 @@ import type { VectorDistance, VectorMetric } from '../type/index.js';
|
|
|
12
12
|
export declare class TursoDialect extends LibsqlDialect {
|
|
13
13
|
/** The Rust engine adds a dot-product distance to libSQL's cosine and L2. */
|
|
14
14
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
15
|
+
/** The Rust engine takes no `ORDER BY` inside an aggregate. */
|
|
16
|
+
protected readonly orderedAggregates = false;
|
|
15
17
|
}
|
package/dist/type/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
2
1
|
import type { ForeignKeyAction } from '../schema/types.js';
|
|
2
|
+
import type { MigratorDialect } from './migratorDialect.js';
|
|
3
3
|
import type { Querier } from './querier.js';
|
|
4
4
|
import type { QuerierPool } from './querierPool.js';
|
|
5
5
|
import type { Type } from './utility.js';
|
|
@@ -12,12 +12,12 @@ export interface Config {
|
|
|
12
12
|
* This is required for both the application and the migrations CLI.
|
|
13
13
|
* Must expose {@link QuerierPool.dialect}; migrations and the CLI read `pool.dialect.dialectName`.
|
|
14
14
|
*/
|
|
15
|
-
pool: QuerierPool<Querier,
|
|
15
|
+
pool: QuerierPool<Querier, MigratorDialect>;
|
|
16
16
|
/**
|
|
17
17
|
* List of entity classes to be managed by the ORM.
|
|
18
18
|
* If omitted, classes that completed `defineEntity` (including via `@Entity()`) are discovered via `getEntities()`.
|
|
19
19
|
*/
|
|
20
|
-
entities?: Type<
|
|
20
|
+
entities?: Type<object>[];
|
|
21
21
|
/**
|
|
22
22
|
* The directory where migration files are stored.
|
|
23
23
|
* @default './migrations'
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -33,12 +33,11 @@ export interface QueryContext {
|
|
|
33
33
|
addValue(value: unknown): this;
|
|
34
34
|
pushValue(...values: unknown[]): this;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* alias would let the inner occurrence shadow the outer one it needs to correlate against.
|
|
36
|
+
* An alias for a table or row source the statement reads: `name`, or `name_2`, `name_3`... - the
|
|
37
|
+
* first no other took, so a nested one never shadows what it correlates against, and never `parent`,
|
|
38
|
+
* the one a correlated subquery compares against. Compared without case, as MySQL does on macOS.
|
|
40
39
|
*/
|
|
41
|
-
|
|
40
|
+
claimAlias(name: string, parent?: string): string;
|
|
42
41
|
/**
|
|
43
42
|
* A context for a fragment of this same statement: it renders its own SQL in isolation while
|
|
44
43
|
* sharing the bound values and the generated aliases, so both stay unique and correctly numbered
|