uql-orm 0.81.0 → 0.83.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 +3 -3
- package/dist/browser/querier/httpQuerier.d.ts +2 -2
- package/dist/browser/querier/httpQuerier.js +2 -1
- package/dist/browser/type/clientQuerier.d.ts +2 -2
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +9 -8
- package/dist/bunSql/bunSql.util.js +2 -1
- package/dist/cockroachdb/crdbQuerierPool.js +2 -2
- package/dist/dialect/abstractSqlDialect.d.ts +16 -6
- package/dist/dialect/abstractSqlDialect.js +110 -41
- package/dist/dialect/hydrateColumn.js +2 -12
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
- package/dist/dialect/operators.d.ts +7 -1
- package/dist/dialect/operators.js +13 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +13 -4
- package/dist/entity/metadata/definition.d.ts +1 -2
- package/dist/entity/metadata/definition.js +37 -39
- package/dist/http/handler.js +5 -4
- package/dist/http/query.d.ts +1 -1
- package/dist/http/query.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/maria/mariadbQuerierPool.js +4 -2
- package/dist/migrate/acquireQuerierForMigrations.js +2 -1
- package/dist/migrate/assertCliConfig.js +7 -6
- package/dist/migrate/bin.js +0 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +20 -10
- package/dist/migrate/builder/tableBuilder.js +1 -1
- package/dist/migrate/cli-config.js +5 -4
- package/dist/migrate/ddl/indexDdl.js +4 -3
- package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
- package/dist/migrate/ddl/pgIndexDdl.js +2 -1
- package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
- package/dist/migrate/ddl/tableDdl.js +2 -1
- package/dist/migrate/generator/mongoCommand.js +2 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
- package/dist/migrate/indexPredicate.js +2 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +3 -2
- package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/migrationTarget.js +2 -1
- package/dist/migrate/migrator.js +2 -1
- package/dist/migrate/schemaGenerator.js +5 -4
- package/dist/migrate/storage/databaseStorage.js +1 -1
- package/dist/migrate/triggerSql.d.ts +1 -1
- package/dist/migrate/triggerSql.js +77 -61
- package/dist/mongo/mongoDialect.d.ts +1 -3
- package/dist/mongo/mongoDialect.js +9 -14
- package/dist/mongo/mongodbQuerier.js +7 -10
- package/dist/mssql/mssqlQuerier.d.ts +2 -0
- package/dist/mssql/mssqlQuerier.js +8 -5
- package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
- package/dist/mysql/mysql2QuerierPool.js +20 -2
- package/dist/neon/neonQuerierPool.js +2 -2
- package/dist/pglite/pgliteQuerierPool.js +10 -4
- package/dist/postgres/pgQuerierPool.js +2 -2
- package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
- package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
- package/dist/querier/abstractQuerier.d.ts +9 -4
- package/dist/querier/abstractQuerier.js +26 -19
- package/dist/querier/abstractQuerierPool.d.ts +3 -3
- package/dist/querier/abstractSqlQuerier.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.js +1 -1
- package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
- package/dist/querier/queryError.d.ts +2 -2
- package/dist/schema/canonicalType.d.ts +3 -0
- package/dist/schema/canonicalType.js +31 -9
- package/dist/schema/schemaASTBuilder.js +2 -1
- package/dist/schema/schemaASTDiffer.js +4 -2
- package/dist/sqlite/sqliteDialect.d.ts +1 -3
- package/dist/sqlite/sqliteDialect.js +3 -6
- package/dist/type/dialect.d.ts +23 -1
- package/dist/type/entity.d.ts +16 -12
- package/dist/type/logger.d.ts +2 -2
- package/dist/type/querier.d.ts +3 -3
- package/dist/type/query.d.ts +3 -13
- package/dist/type/queryAggregate.d.ts +4 -10
- package/dist/type/queryRaw.d.ts +17 -3
- package/dist/type/queryRaw.js +2 -1
- package/dist/type/queryWhere.d.ts +7 -7
- package/dist/type/universalQuerier.d.ts +3 -3
- package/dist/type/vector.d.ts +2 -1
- package/dist/type/vector.js +2 -1
- package/dist/util/date.d.ts +11 -0
- package/dist/util/date.js +19 -0
- package/dist/util/dialect.util.d.ts +13 -5
- package/dist/util/dialect.util.js +28 -20
- package/dist/util/field.util.d.ts +4 -4
- package/dist/util/field.util.js +10 -2
- package/dist/util/fieldOption.util.d.ts +5 -3
- package/dist/util/fieldOption.util.js +6 -5
- package/dist/util/hook.util.d.ts +1 -1
- package/dist/util/hook.util.js +8 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/logger.d.ts +3 -3
- package/dist/util/object.util.js +3 -2
- package/dist/util/raw.d.ts +6 -7
- package/dist/util/raw.js +10 -12
- package/dist/util/sqlLiteral.d.ts +8 -1
- package/dist/util/sqlLiteral.js +14 -9
- package/dist/util/triggerWrite.d.ts +15 -0
- package/dist/util/triggerWrite.js +20 -0
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
-
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
-
import { entitySql, entityWhere, fieldOptionConflict,
|
|
2
|
+
import { fieldKeys, isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
+
import { entitySql, entityWhere, fieldOptionConflict, hasKeys, isToManyRelation, memberRefs, fulltextWeights, normalizeIndexColumn, definedEntries, whereWith, } from '../../util/index.js';
|
|
4
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
4
5
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
5
6
|
/**
|
|
6
7
|
* A map held on `globalThis` through the global symbol registry, so a single one survives multiple
|
|
@@ -22,18 +23,18 @@ export function defineField(entity, key, opts = {}) {
|
|
|
22
23
|
// A relation aggregate reads as a correlated subquery, which no engine accepts in a generated column:
|
|
23
24
|
// keeping one on the row takes the triggers a write fires, which are not built yet.
|
|
24
25
|
if (opts.stored && sql instanceof RelationAggregate) {
|
|
25
|
-
throw new
|
|
26
|
+
throw new UqlUsageError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
|
|
26
27
|
"engine keeps in a generated column. Drop 'stored' to have it read on each query.");
|
|
27
28
|
}
|
|
28
29
|
// A stored computed column is a real column and still needs a type; only an inlined one is exempt,
|
|
29
30
|
// its expression being spliced in rather than declared.
|
|
30
31
|
if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
|
|
31
|
-
throw new
|
|
32
|
+
throw new UqlUsageError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
|
|
32
33
|
"at another entity with 'references', which resolves the column type from its primary key.");
|
|
33
34
|
}
|
|
34
35
|
const conflict = fieldOptionConflict(opts);
|
|
35
36
|
if (conflict) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new UqlUsageError(`'${entity.name}.${key}' ${conflict}.`);
|
|
37
38
|
}
|
|
38
39
|
const fieldKey = key;
|
|
39
40
|
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
@@ -75,10 +76,10 @@ function keyMap() {
|
|
|
75
76
|
const KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
76
77
|
function addRelation(entity, key, registration) {
|
|
77
78
|
if (!registration.entity) {
|
|
78
|
-
throw new
|
|
79
|
+
throw new UqlUsageError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
79
80
|
}
|
|
80
81
|
if (registration.through && registration.references) {
|
|
81
|
-
throw new
|
|
82
|
+
throw new UqlUsageError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
|
|
82
83
|
"'references' pairs the declaring entity's columns with the target's instead.");
|
|
83
84
|
}
|
|
84
85
|
const meta = ensureWritableMeta(entity);
|
|
@@ -120,10 +121,10 @@ export function defineTrigger(entity, trigger) {
|
|
|
120
121
|
const meta = ensureWritableMeta(entity);
|
|
121
122
|
// The type already refuses an empty map; this is the same answer for plain JavaScript.
|
|
122
123
|
if (typeof trigger.run !== 'function' && !hasKeys(trigger.run)) {
|
|
123
|
-
throw new
|
|
124
|
+
throw new UqlUsageError(`'${entity.name}' has a trigger whose body names at least one engine to run on`);
|
|
124
125
|
}
|
|
125
126
|
if (trigger.name && meta.triggers?.some((it) => it.name === trigger.name)) {
|
|
126
|
-
throw new
|
|
127
|
+
throw new UqlUsageError(`'${entity.name}' already has a trigger named '${trigger.name}'`);
|
|
127
128
|
}
|
|
128
129
|
(meta.triggers ??= []).push({ ...trigger, of: trigger.of?.(memberRefs()).map((ref) => ref.key) });
|
|
129
130
|
return meta;
|
|
@@ -131,12 +132,12 @@ export function defineTrigger(entity, trigger) {
|
|
|
131
132
|
export function defineFilter(entity, name, opts) {
|
|
132
133
|
const meta = ensureWritableMeta(entity);
|
|
133
134
|
if (name === SOFT_DELETE_FILTER) {
|
|
134
|
-
throw
|
|
135
|
+
throw new UqlUsageError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
|
|
135
136
|
}
|
|
136
137
|
// Widened for a caller the types did not reach, which is the only one this can refuse.
|
|
137
138
|
const { security, onMissing } = opts;
|
|
138
139
|
if (security && onMissing === 'skip') {
|
|
139
|
-
throw
|
|
140
|
+
throw new UqlUsageError(`'${entity.name}' security filter '${name}' cannot use onMissing: 'skip' (it must fail closed)`);
|
|
140
141
|
}
|
|
141
142
|
(meta.filters ??= {})[name] = opts;
|
|
142
143
|
return meta;
|
|
@@ -173,7 +174,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
173
174
|
// statement builds and then fails at the database. `schema` is the way to say it.
|
|
174
175
|
if (opts.name?.includes('.')) {
|
|
175
176
|
const [schema, ...rest] = opts.name.split('.');
|
|
176
|
-
throw new
|
|
177
|
+
throw new UqlUsageError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
|
|
177
178
|
`{ schema: '${schema}', name: '${rest.join('.')}' }.`);
|
|
178
179
|
}
|
|
179
180
|
const meta = ensureWritableMeta(entity);
|
|
@@ -201,7 +202,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
201
202
|
defineFilter(entity, name, filter);
|
|
202
203
|
}
|
|
203
204
|
if (!hasKeys(meta.fields)) {
|
|
204
|
-
throw
|
|
205
|
+
throw new UqlUsageError(`'${entity.name}' must have fields`);
|
|
205
206
|
}
|
|
206
207
|
// A later call composes onto the entity, so a name is only ever set, and `derivedName` records that
|
|
207
208
|
// the class name stood in, which is what a naming strategy derives from.
|
|
@@ -221,21 +222,18 @@ export function defineEntity(entity, opts = {}) {
|
|
|
221
222
|
// Derive soft-delete from the (inheritance-merged) fields, so own and inherited markers are handled
|
|
222
223
|
// uniformly. Exactly one field may be marked; it auto-registers the built-in `softDelete` read
|
|
223
224
|
// filter (a reserved name - see defineFilter - so it never clobbers a user filter).
|
|
224
|
-
const softDeleteKeys =
|
|
225
|
-
const softDelete = meta.fields[key]?.softDelete;
|
|
226
|
-
return softDelete !== undefined && softDelete !== false;
|
|
227
|
-
});
|
|
225
|
+
const softDeleteKeys = fieldKeys(meta, ({ softDelete }) => softDelete !== undefined && softDelete !== false);
|
|
228
226
|
if (softDeleteKeys.length > 1) {
|
|
229
|
-
throw
|
|
227
|
+
throw new UqlUsageError(`'${entity.name}' must have at most one field with 'softDelete'`);
|
|
230
228
|
}
|
|
231
229
|
if (softDeleteKeys.length) {
|
|
232
230
|
meta.softDelete = softDeleteKeys[0];
|
|
233
|
-
(meta.filters ??= {})[SOFT_DELETE_FILTER] = { where:
|
|
231
|
+
(meta.filters ??= {})[SOFT_DELETE_FILTER] = { where: whereWith(meta.softDelete, null), default: true };
|
|
234
232
|
}
|
|
235
233
|
// The optimistic lock, derived the same way and just as singular: one row has one version.
|
|
236
|
-
const versionKeys =
|
|
234
|
+
const versionKeys = fieldKeys(meta, (field) => field.version);
|
|
237
235
|
if (versionKeys.length > 1) {
|
|
238
|
-
throw
|
|
236
|
+
throw new UqlUsageError(`'${entity.name}' must have at most one field with 'version'`);
|
|
239
237
|
}
|
|
240
238
|
if (versionKeys.length) {
|
|
241
239
|
meta.version = versionKeys[0];
|
|
@@ -246,7 +244,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
246
244
|
}
|
|
247
245
|
const ids = getIdKeys(meta);
|
|
248
246
|
if (!ids.length) {
|
|
249
|
-
throw
|
|
247
|
+
throw new UqlUsageError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
|
|
250
248
|
}
|
|
251
249
|
meta.ids = ids;
|
|
252
250
|
return meta;
|
|
@@ -262,7 +260,7 @@ export function assertSoleId(meta, what) {
|
|
|
262
260
|
if (ids.length === 1) {
|
|
263
261
|
return;
|
|
264
262
|
}
|
|
265
|
-
throw new
|
|
263
|
+
throw new UqlUsageError(ids.length
|
|
266
264
|
? `'${meta.entity.name}' has a composite primary key (${ids.join(', ')}), which ${what} does not support yet.`
|
|
267
265
|
: // An entity registered with `@Field` but no `@Entity` never ran the check in `defineEntity`.
|
|
268
266
|
`'${meta.entity.name}' has no primary key, which ${what} needs.`);
|
|
@@ -276,7 +274,7 @@ export function soleIdOf(meta, what) {
|
|
|
276
274
|
export function fieldOf(meta, key) {
|
|
277
275
|
const field = meta.fields[key];
|
|
278
276
|
if (!field) {
|
|
279
|
-
throw new
|
|
277
|
+
throw new UqlUsageError(`'${meta.entity.name}' has no field '${key}'`);
|
|
280
278
|
}
|
|
281
279
|
return field;
|
|
282
280
|
}
|
|
@@ -284,7 +282,7 @@ export function fieldOf(meta, key) {
|
|
|
284
282
|
export function relationOf(meta, key) {
|
|
285
283
|
const relation = meta.relations[key];
|
|
286
284
|
if (!relation) {
|
|
287
|
-
throw new
|
|
285
|
+
throw new UqlUsageError(`'${meta.entity.name}' has no relation '${key}'`);
|
|
288
286
|
}
|
|
289
287
|
return relation;
|
|
290
288
|
}
|
|
@@ -342,7 +340,7 @@ export function getMeta(entity) {
|
|
|
342
340
|
function registeredMeta(entity) {
|
|
343
341
|
const meta = metas.get(entity);
|
|
344
342
|
if (!meta) {
|
|
345
|
-
throw
|
|
343
|
+
throw new UqlUsageError(`'${entity.name}' is not an entity`);
|
|
346
344
|
}
|
|
347
345
|
return meta;
|
|
348
346
|
}
|
|
@@ -351,7 +349,7 @@ function fillRelations(meta) {
|
|
|
351
349
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
352
350
|
const references = settledReferences(at, meta, relKey, relation);
|
|
353
351
|
if (!references.length) {
|
|
354
|
-
throw new
|
|
352
|
+
throw new UqlUsageError(`${at} has no columns to join on.`);
|
|
355
353
|
}
|
|
356
354
|
if (!relation.through) {
|
|
357
355
|
assertJoins(at, meta, relation, references);
|
|
@@ -371,7 +369,7 @@ function settledReferences(at, meta, relKey, relOpts) {
|
|
|
371
369
|
if (typeof references === 'string') {
|
|
372
370
|
const target = ensureMeta(relOpts.entity());
|
|
373
371
|
if (mappedBy || isToManyRelation(relOpts) || target.ids.length > 1) {
|
|
374
|
-
throw new
|
|
372
|
+
throw new UqlUsageError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
|
|
375
373
|
'can: pair the columns, [{ local, foreign }].');
|
|
376
374
|
}
|
|
377
375
|
relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
|
|
@@ -383,7 +381,7 @@ function settledReferences(at, meta, relKey, relOpts) {
|
|
|
383
381
|
return fillInverseSide(at, meta, relOpts, mappedBy);
|
|
384
382
|
if (through)
|
|
385
383
|
return fillThrough(at, meta, relOpts, through);
|
|
386
|
-
throw new
|
|
384
|
+
throw new UqlUsageError(isToManyRelation(relOpts)
|
|
387
385
|
? `${at} is a to-many relation with no way to join: it needs 'mappedBy' (the member on the other side), ` +
|
|
388
386
|
"'through' (a junction entity), or 'references' (the columns)."
|
|
389
387
|
: `${at} needs 'references', the foreign key column it joins by, or 'mappedBy', the member on the other ` +
|
|
@@ -407,7 +405,7 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
407
405
|
const other = `'${relMeta.entity.name}.${mappedBy}'`;
|
|
408
406
|
if (relMeta.fields[mappedBy]) {
|
|
409
407
|
if (meta.ids.length > 1) {
|
|
410
|
-
throw new
|
|
408
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, one column, but the primary key of ` +
|
|
411
409
|
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
412
410
|
'instead, which joins every column of the key.');
|
|
413
411
|
}
|
|
@@ -417,14 +415,14 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
417
415
|
}
|
|
418
416
|
const owner = relMeta.relations[mappedBy];
|
|
419
417
|
if (!owner) {
|
|
420
|
-
throw new
|
|
418
|
+
throw new UqlUsageError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relMeta.entity.name}'.`);
|
|
421
419
|
}
|
|
422
420
|
if (owner.mappedBy) {
|
|
423
|
-
throw new
|
|
421
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
|
|
424
422
|
}
|
|
425
423
|
const ownerTarget = owner.entity();
|
|
426
424
|
if (!isA(meta.entity, ownerTarget)) {
|
|
427
|
-
throw new
|
|
425
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, a relation to '${ownerTarget.name}', not to '${meta.entity.name}'.`);
|
|
428
426
|
}
|
|
429
427
|
const ownerReferences = settledReferences(other, relMeta, mappedBy, owner);
|
|
430
428
|
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
@@ -454,11 +452,11 @@ function assertJoins(at, meta, relOpts, pairs) {
|
|
|
454
452
|
const column = `'${side.meta.entity.name}.${key}'`;
|
|
455
453
|
const field = side.meta.fields[key];
|
|
456
454
|
if (!field || isInlinedExpression(field)) {
|
|
457
|
-
throw new
|
|
455
|
+
throw new UqlUsageError(`${at} joins ${column}, which is not a column: declare it with '@Field'.`);
|
|
458
456
|
}
|
|
459
457
|
const referenced = side.holds && pairs.length === 1 ? field.references?.() : undefined;
|
|
460
458
|
if (referenced && !isA(side.joins, referenced)) {
|
|
461
|
-
throw new
|
|
459
|
+
throw new UqlUsageError(`${at} joins ${column}, a foreign key to '${referenced.name}', not to '${side.joins.name}'.`);
|
|
462
460
|
}
|
|
463
461
|
}
|
|
464
462
|
}
|
|
@@ -491,7 +489,7 @@ export function foreignKeysOf(meta) {
|
|
|
491
489
|
if (!target.ids.length)
|
|
492
490
|
return [];
|
|
493
491
|
if (target.ids.length > 1) {
|
|
494
|
-
throw new
|
|
492
|
+
throw new UqlUsageError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
|
|
495
493
|
`(${target.ids.join(', ')}): a column points at one. Declare a column per key and pair each with it ` +
|
|
496
494
|
`in a '@ManyToOne' to '${target.entity.name}'.`);
|
|
497
495
|
}
|
|
@@ -509,11 +507,11 @@ function junctionReferences(at, junction, side) {
|
|
|
509
507
|
const declare = side.ids.length > 1
|
|
510
508
|
? `a column per key, paired in a '@ManyToOne' to '${side.entity.name}'`
|
|
511
509
|
: `'@Field({ references: () => ${side.entity.name} })'`;
|
|
512
|
-
throw new
|
|
510
|
+
throw new UqlUsageError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare ${declare}.`);
|
|
513
511
|
}
|
|
514
512
|
if (others.length) {
|
|
515
513
|
const columns = [pair, ...others].map(({ local }) => `'${local}'`).join(' and ');
|
|
516
|
-
throw new
|
|
514
|
+
throw new UqlUsageError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
|
|
517
515
|
'needs exactly one column per key of each side.');
|
|
518
516
|
}
|
|
519
517
|
return { local: pair.local, foreign: key };
|
|
@@ -521,7 +519,7 @@ function junctionReferences(at, junction, side) {
|
|
|
521
519
|
}
|
|
522
520
|
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
523
521
|
function getIdKeys(meta) {
|
|
524
|
-
return
|
|
522
|
+
return fieldKeys(meta, (field) => field.isId);
|
|
525
523
|
}
|
|
526
524
|
/**
|
|
527
525
|
* Merges `ancestor` and its ancestors into `meta`, nearest first, draining an undecorated base's
|
package/dist/http/handler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
2
|
import { getEntities, getMeta, soleIdOf } from '../entity/index.js';
|
|
3
|
-
import { whereIds } from '../util/dialect.util.js';
|
|
3
|
+
import { whereIds, whereWith } from '../util/dialect.util.js';
|
|
4
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
4
5
|
import { entityPath, matchRoute } from './contract.js';
|
|
5
6
|
import { parseQueryParams } from './query.js';
|
|
6
7
|
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
@@ -16,14 +17,14 @@ export function createRequestHandler(opts) {
|
|
|
16
17
|
entities = entities.filter((entity) => !exclude.includes(entity));
|
|
17
18
|
}
|
|
18
19
|
if (!entities.length) {
|
|
19
|
-
throw new
|
|
20
|
+
throw new UqlUsageError('no entities for the uql middleware');
|
|
20
21
|
}
|
|
21
22
|
// All of them at once, so fixing the first collision does not just reveal the next.
|
|
22
23
|
const byPath = Map.groupBy(entities, pathOf);
|
|
23
24
|
const collisions = [...byPath].filter(([, clashing]) => clashing.length > 1);
|
|
24
25
|
if (collisions.length) {
|
|
25
26
|
const lines = collisions.map(([path, clashing]) => ` /${path} <- ${clashing.map(tableOf).join(', ')}`);
|
|
26
|
-
throw new
|
|
27
|
+
throw new UqlUsageError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
|
|
27
28
|
"A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
|
|
28
29
|
"pass an 'entityPath', or pass only one of them in 'include'.");
|
|
29
30
|
}
|
|
@@ -157,6 +158,6 @@ function ok(body) {
|
|
|
157
158
|
return { status: 200, body };
|
|
158
159
|
}
|
|
159
160
|
function buildIdQuery(meta, id, query) {
|
|
160
|
-
query.$where =
|
|
161
|
+
query.$where = whereWith(soleIdOf(meta, 'the HTTP handler'), id, query.$where);
|
|
161
162
|
return query;
|
|
162
163
|
}
|
package/dist/http/query.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { WireQuery } from '../type/index.js';
|
|
|
3
3
|
* Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
|
|
4
4
|
* Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
5
5
|
*/
|
|
6
|
-
export declare function parseQueryParams(params?: Record<string, unknown>): WireQuery<
|
|
6
|
+
export declare function parseQueryParams<E = unknown>(params?: Record<string, unknown>): WireQuery<E>;
|
|
7
7
|
/**
|
|
8
8
|
* Serialize a UQL query object into a percent-encoded query string where object values
|
|
9
9
|
* are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
|
package/dist/http/query.js
CHANGED
|
@@ -105,11 +105,11 @@ export function wireJson(value) {
|
|
|
105
105
|
return held;
|
|
106
106
|
}
|
|
107
107
|
if (RAW_VALUE in held) {
|
|
108
|
-
throw new
|
|
108
|
+
throw new UqlUsageError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');
|
|
109
109
|
}
|
|
110
110
|
// A blob is a field value, so no type parameter reaches it: this is the only place it is caught.
|
|
111
111
|
if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {
|
|
112
|
-
throw new
|
|
112
|
+
throw new UqlUsageError('binary cannot travel over HTTP: what leaves the browser is JSON');
|
|
113
113
|
}
|
|
114
114
|
return held;
|
|
115
115
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export { withDeleted } from './util/filters.util.js';
|
|
|
8
8
|
export type { HookContext } from './util/hook.util.js';
|
|
9
9
|
export { DefaultLogger } from './util/logger.js';
|
|
10
10
|
export { raw, refs } from './util/raw.js';
|
|
11
|
+
export { deleteFrom, insertInto, updateTable } from './util/triggerWrite.js';
|
|
11
12
|
export * from './util/uqlError.js';
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,5 @@ export * from './type/index.js';
|
|
|
7
7
|
export { withDeleted } from './util/filters.util.js';
|
|
8
8
|
export { DefaultLogger } from './util/logger.js';
|
|
9
9
|
export { raw, refs } from './util/raw.js';
|
|
10
|
+
export { deleteFrom, insertInto, updateTable } from './util/triggerWrite.js';
|
|
10
11
|
export * from './util/uqlError.js';
|
|
@@ -9,8 +9,10 @@ export class MariadbQuerierPool extends AbstractSqlQuerierPool {
|
|
|
9
9
|
constructor(opts, extra) {
|
|
10
10
|
super(new MariaDialect(dialectOptionsFrom(extra)), extra);
|
|
11
11
|
// BIGINT stays the driver's `bigint`, which `MariadbQuerier` decodes by the rule every driver here
|
|
12
|
-
// shares (`decodeWideNumber`) - not `bigIntAsNumber`, which rounds past 2^53 without a word.
|
|
13
|
-
|
|
12
|
+
// shares (`decodeWideNumber`) - not `bigIntAsNumber`, which rounds past 2^53 without a word. A date
|
|
13
|
+
// reads as its UTC text, which hydration decodes, since the connector would take it for local time
|
|
14
|
+
// whatever `timezone` says; that only sets the session's zone, UTC, so `NOW()` agrees.
|
|
15
|
+
this.pool = createPool({ timezone: 'Z', dateStrings: true, ...opts });
|
|
14
16
|
// `mariadb` fires 'error' at runtime without declaring it, hence the cast; this makes it visible.
|
|
15
17
|
attachPoolErrorHandler(this.pool, 'Idle MariaDB pool connection encountered an error', extra?.logger);
|
|
16
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isMongoQuerier, isSqlQuerier, } from '../type/index.js';
|
|
2
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
2
3
|
/**
|
|
3
4
|
* Querier used for schema migrations and the migration journal.
|
|
4
5
|
*
|
|
@@ -34,7 +35,7 @@ export function withMongoQuerierForMigrations(pool, requiredBy, task) {
|
|
|
34
35
|
function withQuerierOfKind(pool, isKind, error, task) {
|
|
35
36
|
return withQuerierForMigrations(pool, (querier) => {
|
|
36
37
|
if (!isKind(querier)) {
|
|
37
|
-
throw new
|
|
38
|
+
throw new UqlUsageError(error);
|
|
38
39
|
}
|
|
39
40
|
return task(querier);
|
|
40
41
|
});
|
|
@@ -1,30 +1,31 @@
|
|
|
1
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
1
2
|
/**
|
|
2
3
|
* Validates shape required for the migrations CLI (real `QuerierPool`, not config stubs).
|
|
3
4
|
*/
|
|
4
5
|
export function assertCliConfig(config) {
|
|
5
6
|
if (config === null || typeof config !== 'object') {
|
|
6
|
-
throw new
|
|
7
|
+
throw new UqlUsageError('Config must be a non-null object');
|
|
7
8
|
}
|
|
8
9
|
const c = config;
|
|
9
10
|
const pool = c['pool'];
|
|
10
11
|
if (pool === null || typeof pool !== 'object') {
|
|
11
|
-
throw new
|
|
12
|
+
throw new UqlUsageError('Config.pool is required and must be an object');
|
|
12
13
|
}
|
|
13
14
|
const p = pool;
|
|
14
15
|
for (const key of ['getQuerier', 'transaction', 'withQuerier']) {
|
|
15
16
|
if (typeof p[key] !== 'function') {
|
|
16
|
-
throw new
|
|
17
|
+
throw new UqlUsageError(`Config.pool.${key} must be a function`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
if (p['end'] !== undefined && typeof p['end'] !== 'function') {
|
|
20
|
-
throw new
|
|
21
|
+
throw new UqlUsageError('Config.pool.end must be a function when provided');
|
|
21
22
|
}
|
|
22
23
|
const dialect = p['dialect'];
|
|
23
24
|
if (dialect === null || typeof dialect !== 'object') {
|
|
24
|
-
throw new
|
|
25
|
+
throw new UqlUsageError('Config.pool.dialect is required and must be an object');
|
|
25
26
|
}
|
|
26
27
|
const dialectName = dialect['dialectName'];
|
|
27
28
|
if (typeof dialectName !== 'string') {
|
|
28
|
-
throw new
|
|
29
|
+
throw new UqlUsageError('Config.pool.dialect.dialectName must be a string');
|
|
29
30
|
}
|
|
30
31
|
}
|
package/dist/migrate/bin.js
CHANGED
|
File without changes
|
|
@@ -13,6 +13,8 @@ export type DialectDefaults = {
|
|
|
13
13
|
readonly expressions: SqlExpressionMap;
|
|
14
14
|
/** Column types whose `DEFAULT` this engine takes only as a parenthesized expression. */
|
|
15
15
|
readonly wrapTypes?: RegExp;
|
|
16
|
+
/** Column types whose `CURRENT_TIMESTAMP` default must repeat their precision, captured by the pattern. */
|
|
17
|
+
readonly preciseTypes?: RegExp;
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* Looked up by name rather than carried on the dialect, which keeps DDL data out of the query
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
1
2
|
const ANSI = {
|
|
2
3
|
now: 'CURRENT_TIMESTAMP',
|
|
3
4
|
currentDate: 'CURRENT_DATE',
|
|
@@ -14,6 +15,8 @@ const MYSQL = {
|
|
|
14
15
|
};
|
|
15
16
|
/** MySQL 8.0.13+ rejects `DEFAULT 'x'` on these but accepts `DEFAULT ('x')`, whatever the value. */
|
|
16
17
|
const MYSQL_LARGE_TYPES = /^\s*(TINY|MEDIUM|LONG)?(TEXT|BLOB)|^\s*(JSON|GEOMETRY)\b/i;
|
|
18
|
+
/** A `DATETIME(3)` or `TIMESTAMP(3)`, whose fractional-second precision is captured. */
|
|
19
|
+
const MYSQL_PRECISE_TYPES = /^\s*(?:DATETIME|TIMESTAMP)\((\d)\)/i;
|
|
17
20
|
/**
|
|
18
21
|
* Looked up by name rather than carried on the dialect, which keeps DDL data out of the query
|
|
19
22
|
* bundle - the same split that keeps `CANONICAL_TO_SQL` in `schema/canonicalType.ts`. `uuidv7()` is
|
|
@@ -23,8 +26,12 @@ const MYSQL_LARGE_TYPES = /^\s*(TINY|MEDIUM|LONG)?(TEXT|BLOB)|^\s*(JSON|GEOMETRY
|
|
|
23
26
|
export const DIALECT_DEFAULTS = {
|
|
24
27
|
postgres: { expressions: { ...PG, uuidv7: 'uuidv7()' } },
|
|
25
28
|
cockroachdb: { expressions: PG },
|
|
26
|
-
mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES },
|
|
27
|
-
mariadb: {
|
|
29
|
+
mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES, preciseTypes: MYSQL_PRECISE_TYPES },
|
|
30
|
+
mariadb: {
|
|
31
|
+
expressions: { ...MYSQL, uuidv7: 'UUID_v7()' },
|
|
32
|
+
wrapTypes: MYSQL_LARGE_TYPES,
|
|
33
|
+
preciseTypes: MYSQL_PRECISE_TYPES,
|
|
34
|
+
},
|
|
28
35
|
sqlite: { expressions: ANSI },
|
|
29
36
|
// `SYSUTCDATETIME()` over `CURRENT_TIMESTAMP`, which is local time in the server's zone. No
|
|
30
37
|
// `uuidv7`: `NEWSEQUENTIALID()` is an ordered v4 GUID, so it carries no readable timestamp and
|
|
@@ -79,7 +86,7 @@ export const expr = {
|
|
|
79
86
|
* the result needs wrapping, which MySQL demands on its large types whatever the value.
|
|
80
87
|
*/
|
|
81
88
|
export function formatDefaultValue(value, dialect, columnType) {
|
|
82
|
-
const sql = defaultLiteral(value, dialect);
|
|
89
|
+
const sql = defaultLiteral(value, dialect, columnType);
|
|
83
90
|
const { wrapTypes } = DIALECT_DEFAULTS[dialect.dialectName];
|
|
84
91
|
return columnType !== undefined && wrapTypes?.test(columnType) ? `(${sql})` : sql;
|
|
85
92
|
}
|
|
@@ -113,23 +120,26 @@ export function sameDefault(desired, current, dialect) {
|
|
|
113
120
|
* cannot serve stay here: a boolean is `1` where booleans are integers, and a plain object or array
|
|
114
121
|
* is JSON rather than the throw and the IN-list `escape` gives them.
|
|
115
122
|
*/
|
|
116
|
-
function defaultLiteral(value, dialect) {
|
|
123
|
+
function defaultLiteral(value, dialect, columnType) {
|
|
117
124
|
if (value === undefined || value === null) {
|
|
118
125
|
return 'NULL';
|
|
119
126
|
}
|
|
120
127
|
if (SqlExpression.isExpression(value)) {
|
|
121
|
-
return expressionSql(value, dialect);
|
|
128
|
+
return expressionSql(value, dialect, columnType);
|
|
122
129
|
}
|
|
123
130
|
if (typeof value === 'boolean') {
|
|
124
131
|
return dialect.booleanLiteral === 'native' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0';
|
|
125
132
|
}
|
|
126
133
|
return dialect.escape(typeof value === 'object' && !(value instanceof Date) ? JSON.stringify(value) : value);
|
|
127
134
|
}
|
|
128
|
-
function expressionSql(expression, dialect) {
|
|
129
|
-
const { expressions } = DIALECT_DEFAULTS[dialect.dialectName];
|
|
130
|
-
const
|
|
135
|
+
function expressionSql(expression, dialect, columnType) {
|
|
136
|
+
const { expressions, preciseTypes } = DIALECT_DEFAULTS[dialect.dialectName];
|
|
137
|
+
const raw = expression.kind === 'raw';
|
|
138
|
+
const sql = raw ? expression.sql : expressions[expression.kind];
|
|
131
139
|
if (sql == null) {
|
|
132
|
-
throw new
|
|
140
|
+
throw new UqlUsageError(`${dialect.dialectName} has no '${expression.kind}' default; pass expr.raw(...) with SQL this engine accepts`);
|
|
133
141
|
}
|
|
134
|
-
|
|
142
|
+
// A raw default is the caller's SQL, never rewritten.
|
|
143
|
+
const precision = raw || columnType === undefined ? undefined : preciseTypes?.exec(columnType)?.[1];
|
|
144
|
+
return precision === undefined ? sql : sql.replaceAll('CURRENT_TIMESTAMP', `CURRENT_TIMESTAMP(${precision})`);
|
|
135
145
|
}
|
|
@@ -144,7 +144,7 @@ export class TableBuilder {
|
|
|
144
144
|
return this.timestampNow('updatedAt');
|
|
145
145
|
}
|
|
146
146
|
timestampNow(name) {
|
|
147
|
-
return this.
|
|
147
|
+
return this.timestamptz(name, { defaultValue: expr.now() });
|
|
148
148
|
}
|
|
149
149
|
timestamps() {
|
|
150
150
|
this.createdAt();
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
4
5
|
/**
|
|
5
6
|
* Loads the config with a plain `import()`, leaving TypeScript to the runtime: uql bundles no transpiler,
|
|
6
7
|
* since only the project knows its decorator spec. Node's type stripping handles no decorators.
|
|
7
8
|
*/
|
|
8
9
|
async function importConfig(path) {
|
|
9
10
|
const mod = (await import(pathToFileURL(path).href).catch((cause) => {
|
|
10
|
-
throw new
|
|
11
|
+
throw new UqlUsageError(`Could not import ${path}: ${cause?.message}\n` +
|
|
11
12
|
'If it reaches entity classes, their decorators need a runtime that transforms TypeScript, not ' +
|
|
12
13
|
'just one that strips its types. Run the CLI with `bun`, or with `node --import tsx` ' +
|
|
13
14
|
'(`npm i -D tsx`). A JavaScript config, or passing the config inline, needs neither.', { cause });
|
|
@@ -21,14 +22,14 @@ export async function loadConfig(customPath) {
|
|
|
21
22
|
.then(() => true)
|
|
22
23
|
.catch(() => false);
|
|
23
24
|
if (!exists) {
|
|
24
|
-
throw new
|
|
25
|
+
throw new UqlUsageError(`Could not find uql configuration file at ${customPath}`);
|
|
25
26
|
}
|
|
26
27
|
try {
|
|
27
28
|
const config = await importConfig(fullPath);
|
|
28
29
|
return config;
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
|
-
throw new
|
|
32
|
+
throw new UqlUsageError(`Could not load configuration file at ${customPath}: ${error.message}`);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
const configPaths = ['uql.config.ts', 'uql.config.js', 'uql.config.mjs', '.uqlrc.ts', '.uqlrc.js'];
|
|
@@ -42,5 +43,5 @@ export async function loadConfig(customPath) {
|
|
|
42
43
|
return config;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
throw new
|
|
46
|
+
throw new UqlUsageError('Could not find uql configuration file. Create a uql.config.ts or uql.config.js file in your project root.');
|
|
46
47
|
}
|
|
@@ -2,6 +2,7 @@ import { jsonTypeMode } from '../../dialect/jsonSql.js';
|
|
|
2
2
|
import { INDEX_TYPES } from '../../schema/types.js';
|
|
3
3
|
import { INDEX_FEATURE_LABELS, } from '../../type/index.js';
|
|
4
4
|
import { fulltextConfig, getKeys } from '../../util/index.js';
|
|
5
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
5
6
|
/**
|
|
6
7
|
* What in an index asks for each feature. A `Record` over the feature union rather than a list, so a
|
|
7
8
|
* feature added to {@link INDEX_FEATURE_LABELS} cannot reach a dialect without the test that decides
|
|
@@ -20,14 +21,14 @@ const INDEX_FEATURE_PROBES = {
|
|
|
20
21
|
/** Refuses an index of a type `types` lacks, `hints` naming what to declare instead. */
|
|
21
22
|
export function assertIndexType(index, types, dialectName, hints = new Map()) {
|
|
22
23
|
if (index.type && !types.has(index.type)) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new UqlUsageError(`${dialectName} has no ${index.type} index (index "${index.name}")` + (hints.get(index.type) ?? ''));
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
/** Refuses an index asking for a feature `features` lacks. */
|
|
27
28
|
export function assertIndexFeatures(index, features, dialectName) {
|
|
28
29
|
for (const feature of getKeys(INDEX_FEATURE_PROBES)) {
|
|
29
30
|
if (INDEX_FEATURE_PROBES[feature](index) && !features.has(feature)) {
|
|
30
|
-
throw new
|
|
31
|
+
throw new UqlUsageError(`${dialectName} does not support ${INDEX_FEATURE_LABELS[feature]} (index "${index.name}")`);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -122,7 +123,7 @@ export class IndexDdl {
|
|
|
122
123
|
* every other dialect refuses `jsonArray` in {@link assertIndexFeatures} and never reaches this.
|
|
123
124
|
*/
|
|
124
125
|
jsonArrayIndexExpr(_escapedColumn, _json) {
|
|
125
|
-
throw new
|
|
126
|
+
throw new UqlUsageError(`${this.dialect.dialectName} has no multi-valued index`);
|
|
126
127
|
}
|
|
127
128
|
/** Postgres-wire dialects put a vector or user-declared operator class here. */
|
|
128
129
|
indexColumnOpsClass(_entry, _index) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsonTypeMode } from '../../dialect/jsonSql.js';
|
|
2
2
|
import { indexDistance, isVectorIndexType, unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
|
|
3
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
3
4
|
import { IndexDdl } from './indexDdl.js';
|
|
4
5
|
/**
|
|
5
6
|
* A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext` is
|
|
@@ -38,14 +39,14 @@ export class MySqlIndexDdl extends MysqlLikeIndexDdl {
|
|
|
38
39
|
jsonPathIndexExpr(escapedColumn, json) {
|
|
39
40
|
const mode = jsonTypeMode(json.type);
|
|
40
41
|
if (mode === 'json') {
|
|
41
|
-
throw new
|
|
42
|
+
throw new UqlUsageError(`mysql cannot index the boolean JSON path '${json.path}', which compares as JSON`);
|
|
42
43
|
}
|
|
43
44
|
const expr = super.jsonPathIndexExpr(escapedColumn, json);
|
|
44
45
|
if (mode === 'numeric') {
|
|
45
46
|
return expr;
|
|
46
47
|
}
|
|
47
48
|
if (!json.length) {
|
|
48
|
-
throw new
|
|
49
|
+
throw new UqlUsageError(`a MySQL index over the string JSON path '${json.path}' needs a length`);
|
|
49
50
|
}
|
|
50
51
|
return `CAST(${expr} AS CHAR(${json.length}) CHARACTER SET utf8mb4) COLLATE utf8mb4_bin`;
|
|
51
52
|
}
|
|
@@ -132,13 +133,13 @@ function arrayCastType(json) {
|
|
|
132
133
|
const type = json.type;
|
|
133
134
|
const cast = ARRAY_CASTS.get(typeof type === 'string' ? type.toLowerCase() : type);
|
|
134
135
|
if (!cast) {
|
|
135
|
-
throw new
|
|
136
|
+
throw new UqlUsageError(`mysql has no array cast for ${typeof type === 'string' ? type : type.name} elements`);
|
|
136
137
|
}
|
|
137
138
|
if (cast !== 'CHAR' && cast !== 'BINARY') {
|
|
138
139
|
return cast;
|
|
139
140
|
}
|
|
140
141
|
if (!json.length) {
|
|
141
|
-
throw new
|
|
142
|
+
throw new UqlUsageError(`a multi-valued index over ${cast === 'CHAR' ? 'string' : 'binary'} elements needs a length`);
|
|
142
143
|
}
|
|
143
144
|
return `${cast}(${json.length})`;
|
|
144
145
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
|
|
2
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
2
3
|
import { IndexDdl } from './indexDdl.js';
|
|
3
4
|
/** `CREATE INDEX ... USING hnsw ("embedding" vector_cosine_ops) WITH (m = ...)`, pgvector's form. */
|
|
4
5
|
export class PgIndexDdl extends IndexDdl {
|
|
@@ -49,7 +50,7 @@ export class PgIndexDdl extends IndexDdl {
|
|
|
49
50
|
const opsClass = `${vectorType}_${metric}_ops`;
|
|
50
51
|
// IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
|
|
51
52
|
if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || distance === 'l1')) {
|
|
52
|
-
throw new
|
|
53
|
+
throw new UqlUsageError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
|
|
53
54
|
}
|
|
54
55
|
return ` ${opsClass}`;
|
|
55
56
|
}
|