uql-orm 0.82.0 → 0.83.1
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 +116 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/cockroachdb/crdbQuerierPool.js +2 -2
- package/dist/dialect/abstractSqlDialect.d.ts +1 -2
- package/dist/dialect/abstractSqlDialect.js +15 -10
- 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 +16 -0
- package/dist/dialect/operators.js +47 -4
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +13 -4
- package/dist/http/handler.js +17 -23
- package/dist/http/query.d.ts +3 -3
- package/dist/http/query.js +6 -3
- package/dist/maria/mariadbQuerierPool.js +4 -2
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +18 -9
- package/dist/migrate/builder/tableBuilder.js +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +1 -1
- 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/storage/databaseStorage.js +1 -1
- package/dist/mongo/mongoDialect.js +12 -24
- package/dist/mongo/mongodbQuerier.js +4 -3
- 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/schema/canonicalType.d.ts +3 -0
- package/dist/schema/canonicalType.js +31 -9
- 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/entity.d.ts +1 -1
- package/dist/type/queryWhere.d.ts +10 -8
- package/dist/util/date.d.ts +11 -0
- package/dist/util/date.js +19 -0
- package/dist/util/dialect.util.d.ts +2 -5
- package/dist/util/dialect.util.js +3 -6
- package/dist/util/fieldOption.util.d.ts +5 -3
- package/dist/util/fieldOption.util.js +6 -5
- package/dist/util/sqlLiteral.d.ts +8 -1
- package/dist/util/sqlLiteral.js +11 -7
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +1 -1
|
@@ -5,14 +5,32 @@ import { MySql2Querier } from './mysql2Querier.js';
|
|
|
5
5
|
import { MySqlDialect } from './mysqlDialect.js';
|
|
6
6
|
export class MySql2QuerierPool extends AbstractSqlQuerierPool {
|
|
7
7
|
pool;
|
|
8
|
+
#utcSessions = new WeakSet();
|
|
8
9
|
constructor(opts, extra) {
|
|
9
10
|
super(new MySqlDialect(dialectOptionsFrom(extra)), extra);
|
|
10
11
|
// A BIGINT past 2^53 as its exact text rather than a rounded number, the rule every driver here
|
|
11
12
|
// decodes by (`decodeWideNumber`); within that range it stays a number, and DECIMAL is untouched.
|
|
12
|
-
|
|
13
|
+
// A date reads as the UTC it holds, whichever zone the process runs in.
|
|
14
|
+
this.pool = createPool({ supportBigNumbers: true, timezone: 'Z', ...opts });
|
|
13
15
|
}
|
|
14
16
|
async getQuerier() {
|
|
15
|
-
return new MySql2Querier(() => this
|
|
17
|
+
return new MySql2Querier(() => this.#connection(), this.dialect, this.extra);
|
|
18
|
+
}
|
|
19
|
+
/** A connection whose session is UTC too, so `NOW()` agrees with a bound date: set once per connection. */
|
|
20
|
+
async #connection() {
|
|
21
|
+
const connection = await this.pool.getConnection();
|
|
22
|
+
if (this.#utcSessions.has(connection.connection)) {
|
|
23
|
+
return connection;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await connection.query("SET time_zone = '+00:00'");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
connection.release();
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
this.#utcSessions.add(connection.connection);
|
|
33
|
+
return connection;
|
|
16
34
|
}
|
|
17
35
|
async end() {
|
|
18
36
|
await this.pool.end();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Pool, types } from '@neondatabase/serverless';
|
|
2
2
|
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { AbstractPgQuerierPool } from '../postgres/abstractPgQuerierPool.js';
|
|
4
|
-
import {
|
|
4
|
+
import { wireTypes } from '../postgres/pgWireTypes.js';
|
|
5
5
|
import { PostgresDialect } from '../postgres/postgresDialect.js';
|
|
6
6
|
export class NeonQuerierPool extends AbstractPgQuerierPool {
|
|
7
7
|
constructor(opts, extra) {
|
|
8
8
|
// Neon's own `types`, not `pg`'s: this entry has to load on an edge runtime where `pg` is absent.
|
|
9
|
-
super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ types:
|
|
9
|
+
super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ types: wireTypes(types), ...opts }), extra);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
2
2
|
import { PostgresDialect } from '../postgres/postgresDialect.js';
|
|
3
3
|
import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
|
|
4
|
+
import { decodeDate } from '../util/date.js';
|
|
4
5
|
import { decodeWideNumber } from '../util/wideNumber.js';
|
|
5
6
|
import { PgliteQuerier } from './pgliteQuerier.js';
|
|
6
7
|
/**
|
|
@@ -18,12 +19,17 @@ export class PgliteQuerierPool extends AbstractSharedHandleQuerierPool {
|
|
|
18
19
|
}
|
|
19
20
|
async openDb() {
|
|
20
21
|
const { PGlite, types } = await import('@electric-sql/pglite');
|
|
21
|
-
// INT8 by the one wide-integer rule, where PGlite's own answers a `bigint` past 2^53
|
|
22
|
-
//
|
|
23
|
-
// real driver, so no cast is needed
|
|
22
|
+
// INT8 by the one wide-integer rule, where PGlite's own answers a `bigint` past 2^53, and a zoneless
|
|
23
|
+
// TIMESTAMP or a DATE as UTC, as every pool reads one; a caller's own `parsers` still win. The declared
|
|
24
|
+
// return type is what checks {@link PgliteDatabase} against the real driver, so no cast is needed below it.
|
|
24
25
|
return PGlite.create(this.dataDir, {
|
|
25
26
|
...this.opts,
|
|
26
|
-
parsers: {
|
|
27
|
+
parsers: {
|
|
28
|
+
[types.INT8]: decodeWideNumber,
|
|
29
|
+
[types.TIMESTAMP]: decodeDate,
|
|
30
|
+
[types.DATE]: decodeDate,
|
|
31
|
+
...this.opts?.parsers,
|
|
32
|
+
},
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
buildQuerier(db) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Pool, types } from 'pg';
|
|
2
2
|
import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { AbstractPgQuerierPool } from './abstractPgQuerierPool.js';
|
|
4
|
-
import {
|
|
4
|
+
import { wireTypes } from './pgWireTypes.js';
|
|
5
5
|
import { PostgresDialect } from './postgresDialect.js';
|
|
6
6
|
export class PgQuerierPool extends AbstractPgQuerierPool {
|
|
7
7
|
constructor(opts, extra) {
|
|
8
8
|
// keepAlive reduces (but can't eliminate) idle connections being silently
|
|
9
9
|
// dropped by NATs/firewalls on long-lived remote connections.
|
|
10
|
-
super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types:
|
|
10
|
+
super(new PostgresDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: wireTypes(types), ...opts }), extra);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -11,8 +11,9 @@ type PgTypes = {
|
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
13
|
* Decodes `INT8` by `decodeWideNumber` and `FLOAT8` as the float64 it is, since `type: Number` maps to
|
|
14
|
-
* BIGINT
|
|
15
|
-
*
|
|
14
|
+
* BIGINT, and a zoneless `TIMESTAMP` or a `DATE` as UTC, where `pg` reads both in the process's zone. At
|
|
15
|
+
* the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field. Per pool,
|
|
16
|
+
* never a global parser, and a caller's own `types` win.
|
|
16
17
|
*/
|
|
17
|
-
export declare function
|
|
18
|
+
export declare function wireTypes(types: PgTypes): CustomTypesConfig;
|
|
18
19
|
export {};
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { decodeDate } from '../util/date.js';
|
|
1
2
|
import { decodeWideNumber } from '../util/wideNumber.js';
|
|
2
3
|
/**
|
|
3
4
|
* Decodes `INT8` by `decodeWideNumber` and `FLOAT8` as the float64 it is, since `type: Number` maps to
|
|
4
|
-
* BIGINT
|
|
5
|
-
*
|
|
5
|
+
* BIGINT, and a zoneless `TIMESTAMP` or a `DATE` as UTC, where `pg` reads both in the process's zone. At
|
|
6
|
+
* the wire, which every result crosses; `NUMERIC` is left to hydration, which knows the field. Per pool,
|
|
7
|
+
* never a global parser, and a caller's own `types` win.
|
|
6
8
|
*/
|
|
7
|
-
export function
|
|
9
|
+
export function wireTypes(types) {
|
|
8
10
|
// Text only: in binary mode an INT8 arrives as an 8-byte Buffer, and `Number(buffer)` is `NaN`.
|
|
9
11
|
const decoders = new Map([
|
|
10
12
|
[types.builtins['INT8'], decodeWideNumber],
|
|
11
13
|
[types.builtins['FLOAT8'], Number],
|
|
14
|
+
[types.builtins['TIMESTAMP'], decodeDate],
|
|
15
|
+
[types.builtins['DATE'], decodeDate],
|
|
12
16
|
]);
|
|
13
17
|
return {
|
|
14
18
|
getTypeParser: (oid, format) => (format === 'text' && decoders.get(oid)) || types.getTypeParser(oid, format),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
2
2
|
import type { VectorCast } from '../dialect/vectorCast.js';
|
|
3
3
|
import type { ColumnType, EntityGetter, FieldMeta, FieldOptions } from '../type/entity.js';
|
|
4
|
+
import { type DialectName } from '../type/index.js';
|
|
4
5
|
import type { CanonicalType, TypeCategory } from './types.js';
|
|
5
6
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
6
7
|
export declare function isVectorCategory(category: TypeCategory | undefined): category is VectorCast;
|
|
@@ -29,6 +30,8 @@ export declare function canonicalToSql(type: CanonicalType, dialect: AbstractDia
|
|
|
29
30
|
* Convert a canonical type to a TypeScript type string.
|
|
30
31
|
*/
|
|
31
32
|
export declare function canonicalToTypeScript(type: CanonicalType): string;
|
|
33
|
+
/** The fractional-second digits an engine's timestamp holds when its type states none; `undefined` where it counts none. */
|
|
34
|
+
export declare function defaultTimestampPrecision(dialectName: DialectName): number | undefined;
|
|
32
35
|
/**
|
|
33
36
|
* A type as `dialect` stores it, rendered and read back: several types share one storage type, and only
|
|
34
37
|
* the engine settles an unstated bound. Migrations and drift both compare through it.
|
|
@@ -120,7 +120,8 @@ const MYSQL_SCALAR_MAP = {
|
|
|
120
120
|
boolean: 'TINYINT(1)',
|
|
121
121
|
date: 'DATE',
|
|
122
122
|
time: 'TIME',
|
|
123
|
-
|
|
123
|
+
// The milliseconds a `Date` holds, which a bare `DATETIME` rounds away.
|
|
124
|
+
timestamp: 'DATETIME(3)',
|
|
124
125
|
json: 'JSON',
|
|
125
126
|
uuid: 'CHAR(36)',
|
|
126
127
|
blob: 'BLOB',
|
|
@@ -193,25 +194,28 @@ const ENGINE_TYPES = {
|
|
|
193
194
|
postgres: {
|
|
194
195
|
scalars: { ...PG_SCALAR_MAP, vector: 'VECTOR', halfvec: 'HALFVEC', sparsevec: 'SPARSEVEC' },
|
|
195
196
|
sizes: PG_SIZES,
|
|
197
|
+
timestampPrecision: 6,
|
|
196
198
|
},
|
|
197
199
|
// CockroachDB's VECTOR is native, no extension needed.
|
|
198
|
-
cockroachdb: { scalars: withVectorType(PG_SCALAR_MAP, 'VECTOR'), sizes: PG_SIZES },
|
|
200
|
+
cockroachdb: { scalars: withVectorType(PG_SCALAR_MAP, 'VECTOR'), sizes: PG_SIZES, timestampPrecision: 6 },
|
|
199
201
|
// MySQL does have a `VECTOR` type (26.7), but no distance function outside HeatWave and no vector
|
|
200
202
|
// index, so JSON keeps the column queryable with the JSON operators and needs no conversion.
|
|
201
203
|
mysql: {
|
|
202
204
|
scalars: withVectorType(MYSQL_SCALAR_MAP, 'JSON'),
|
|
203
205
|
sizes: MYSQL_SIZES,
|
|
204
206
|
decimal: { precision: 10, scale: 2 },
|
|
207
|
+
timestampPrecision: 0,
|
|
205
208
|
},
|
|
206
209
|
mariadb: {
|
|
207
210
|
scalars: withVectorType(MYSQL_SCALAR_MAP, 'VECTOR'),
|
|
208
211
|
sizes: MYSQL_SIZES,
|
|
209
212
|
decimal: { precision: 10, scale: 2 },
|
|
213
|
+
timestampPrecision: 0,
|
|
210
214
|
},
|
|
211
215
|
// SQLite uses affinity, so no size variants. `F32_BLOB` is libSQL's vector type; elsewhere just a name of BLOB affinity.
|
|
212
216
|
sqlite: { scalars: withVectorType(SQLITE_SCALAR_MAP, 'F32_BLOB') },
|
|
213
217
|
// 2025 and up; below that the server refuses the type rather than storing it as text.
|
|
214
|
-
mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'VECTOR'), sizes: MSSQL_SIZES },
|
|
218
|
+
mssql: { scalars: withVectorType(MSSQL_SCALAR_MAP, 'VECTOR'), sizes: MSSQL_SIZES, timestampPrecision: 7 },
|
|
215
219
|
mongodb: { scalars: withVectorType(MONGO_SCALAR_MAP, 'array') },
|
|
216
220
|
};
|
|
217
221
|
/**
|
|
@@ -241,8 +245,11 @@ export function sqlToCanonical(sqlType) {
|
|
|
241
245
|
const normalized = sqlType.toLowerCase().trim();
|
|
242
246
|
const unsigned = normalized.includes('unsigned');
|
|
243
247
|
const withoutUnsigned = normalized.replace(/\s*unsigned\s*/i, ' ').trim();
|
|
244
|
-
// Extract base type and parameters: "VARCHAR(255)" -> ["varchar", "255"]
|
|
245
|
-
|
|
248
|
+
// Extract base type and parameters: "VARCHAR(255)" -> ["varchar", "255"], and Postgres's
|
|
249
|
+
// "timestamp(3) with time zone", whose parameter sits inside the name, as "timestamp with time zone(3)".
|
|
250
|
+
const match = withoutUnsigned
|
|
251
|
+
.replace(/^(\w+)\((\d+)\)\s+(.+)$/, '$1 $3($2)')
|
|
252
|
+
.match(/^([a-z][a-z0-9_ ]*?)(?:\(([^)]+)\))?$/);
|
|
246
253
|
const base = match ? SQL_TO_CANONICAL[match[1]] : undefined;
|
|
247
254
|
if (!match || !base) {
|
|
248
255
|
return { category: 'string', raw: sqlType };
|
|
@@ -260,7 +267,7 @@ export function sqlToCanonical(sqlType) {
|
|
|
260
267
|
size: measured && params[0] === 'max' ? 'small' : base.size,
|
|
261
268
|
withTimezone: base.withTimezone,
|
|
262
269
|
length: measured ? first : undefined,
|
|
263
|
-
precision: decimal ? first : undefined,
|
|
270
|
+
precision: decimal || base.category === 'timestamp' ? first : undefined,
|
|
264
271
|
scale: decimal ? second : undefined,
|
|
265
272
|
unsigned: unsigned || undefined,
|
|
266
273
|
};
|
|
@@ -317,6 +324,9 @@ export function canonicalToSql(type, dialect) {
|
|
|
317
324
|
if (type.category === 'timestamp' && type.withTimezone && features.supportsTimestamptz) {
|
|
318
325
|
sqlType = 'TIMESTAMPTZ';
|
|
319
326
|
}
|
|
327
|
+
if (type.category === 'timestamp' && type.precision !== undefined && engine.timestampPrecision !== undefined) {
|
|
328
|
+
sqlType = `${sqlType.replace(/\(\d+\)$/, '')}(${type.precision})`;
|
|
329
|
+
}
|
|
320
330
|
return type.unsigned && features.supportsUnsigned ? `${sqlType} UNSIGNED` : sqlType;
|
|
321
331
|
}
|
|
322
332
|
/** See {@link DialectFeatures.stringSizing} for what each mode means. */
|
|
@@ -343,12 +353,22 @@ function formatDecimalSqlType(type, fallback, baseType) {
|
|
|
343
353
|
export function canonicalToTypeScript(type) {
|
|
344
354
|
return CANONICAL_TO_TS[type.category];
|
|
345
355
|
}
|
|
356
|
+
/** The fractional-second digits an engine's timestamp holds when its type states none; `undefined` where it counts none. */
|
|
357
|
+
export function defaultTimestampPrecision(dialectName) {
|
|
358
|
+
return ENGINE_TYPES[dialectName].timestampPrecision;
|
|
359
|
+
}
|
|
346
360
|
/**
|
|
347
361
|
* A type as `dialect` stores it, rendered and read back: several types share one storage type, and only
|
|
348
362
|
* the engine settles an unstated bound. Migrations and drift both compare through it.
|
|
349
363
|
*/
|
|
350
364
|
export function engineType(dialect) {
|
|
351
|
-
|
|
365
|
+
const timestampPrecision = defaultTimestampPrecision(dialect.dialectName);
|
|
366
|
+
return (type) => {
|
|
367
|
+
const stored = sqlToCanonical(canonicalToSql(type, dialect));
|
|
368
|
+
return stored.category === 'timestamp' && stored.precision === undefined
|
|
369
|
+
? { ...stored, precision: timestampPrecision }
|
|
370
|
+
: stored;
|
|
371
|
+
};
|
|
352
372
|
}
|
|
353
373
|
/**
|
|
354
374
|
* Convert UQL FieldOptions to a canonical type.
|
|
@@ -363,14 +383,16 @@ export function fieldOptionsToCanonical(options) {
|
|
|
363
383
|
switch (columnFamily(options.type)) {
|
|
364
384
|
case 'numeric':
|
|
365
385
|
// BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
|
|
366
|
-
// to happen, and the pools decode it back to a JS number at the wire (see `
|
|
386
|
+
// to happen, and the pools decode it back to a JS number at the wire (see `pgWireTypes`).
|
|
367
387
|
return isIntegerColumn(options)
|
|
368
388
|
? { category: 'integer', size: 'big' }
|
|
369
389
|
: { category: 'decimal', precision: options.precision, scale: options.scale };
|
|
370
390
|
case 'boolean':
|
|
371
391
|
return { category: 'boolean' };
|
|
392
|
+
// An instant: uql reads a zoneless timestamp as UTC, but the database's own clock and every other
|
|
393
|
+
// client read one in the session's zone.
|
|
372
394
|
case 'date':
|
|
373
|
-
return { category: 'timestamp' };
|
|
395
|
+
return { category: 'timestamp', withTimezone: true, precision: options.precision };
|
|
374
396
|
// `String`, and anything a reflected type left unrecognised.
|
|
375
397
|
default:
|
|
376
398
|
return { category: 'string', length: options.length };
|
|
@@ -124,7 +124,9 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
124
124
|
// inconsistently (`BIGINT(20)`, SQLite's `notnull: 0` rowid), so neither is compared.
|
|
125
125
|
const generatedType = source.isAutoIncrement && target.isAutoIncrement;
|
|
126
126
|
const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
|
|
127
|
-
const
|
|
127
|
+
const expectedType = opts.normalizeType(source.type);
|
|
128
|
+
const actualType = opts.normalizeType(target.type);
|
|
129
|
+
const typeChanged = !generatedType && !areTypesEqual(expectedType, actualType);
|
|
128
130
|
if (typeChanged) {
|
|
129
131
|
differences.push(`type: ${formatType(source.type)} -> ${formatType(target.type)}`);
|
|
130
132
|
}
|
|
@@ -158,7 +160,7 @@ function diffColumn(tableName, source, target, opts) {
|
|
|
158
160
|
// Only the type this diff actually reports: a column altered for its default carries no data loss,
|
|
159
161
|
// and a generated key's type - never compared above - reads as unsigned against an entity that
|
|
160
162
|
// cannot say so.
|
|
161
|
-
isBreaking: (typeChanged || signednessChanged) && isBreakingTypeChange(
|
|
163
|
+
isBreaking: (typeChanged || signednessChanged) && isBreakingTypeChange(actualType, expectedType),
|
|
162
164
|
description: differences.join(', '),
|
|
163
165
|
};
|
|
164
166
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractSqlDialect, type DerivedRelation, type
|
|
1
|
+
import { AbstractSqlDialect, type DerivedRelation, type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
2
2
|
import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
|
|
3
3
|
import { type EntityMeta, type FieldOptions, type Query, type QueryContext, type QueryPager, type QueryTextSearchOptions, type QueryWhere, type SqlDialectFeatures, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
4
4
|
/** What SQLite and the engines derived from it have. */
|
|
@@ -60,8 +60,6 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
60
60
|
vector: (expr: string) => string;
|
|
61
61
|
};
|
|
62
62
|
private bytesAsText;
|
|
63
|
-
/** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
|
|
64
|
-
protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
|
|
65
63
|
/**
|
|
66
64
|
* FTS5 matches the table itself, so this works only where the table *is* an FTS5 virtual table (UQL does
|
|
67
65
|
* not create those; declare it outside your entities). The whole query is bound, column filter and all.
|
|
@@ -3,9 +3,10 @@ import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
|
3
3
|
import { chainedCall, groupsPerCall, jsonSetCall, jsonPath, jsonArraySlotArgs, jsonSlotArgs, jsonRemoveCall, jsonSetTarget, } from '../dialect/jsonSql.js';
|
|
4
4
|
import { QueryRaw, } from '../type/index.js';
|
|
5
5
|
import { indexDistance, isVectorIndexType } from '../type/vector.js';
|
|
6
|
+
import { utcTimestamp } from '../util/date.js';
|
|
6
7
|
import { declaredIndexName } from '../util/ddlExpression.util.js';
|
|
7
8
|
import { findVectorIndex, findVectorSort, textSearchFields, vectorCandidates } from '../util/dialect.util.js';
|
|
8
|
-
import {
|
|
9
|
+
import { isIntegerColumn } from '../util/field.util.js';
|
|
9
10
|
/**
|
|
10
11
|
* An FTS5 query over `columns` for what a person typed: each word a quoted string, which FTS5 reads as a
|
|
11
12
|
* term to match and never as syntax, and every one required, as the other engines read plain words.
|
|
@@ -128,7 +129,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
128
129
|
}
|
|
129
130
|
normalizeValue(value) {
|
|
130
131
|
if (value instanceof Date)
|
|
131
|
-
return value
|
|
132
|
+
return utcTimestamp(value);
|
|
132
133
|
return super.normalizeValue(value);
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
@@ -169,10 +170,6 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
169
170
|
bytesAsText(expr) {
|
|
170
171
|
return `${this.escape(BYTES_PREFIX)} || hex(${expr})`;
|
|
171
172
|
}
|
|
172
|
-
/** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
|
|
173
|
-
hydrateKind(field) {
|
|
174
|
-
return columnFamily(field?.type) === 'date' ? undefined : super.hydrateKind(field);
|
|
175
|
-
}
|
|
176
173
|
/**
|
|
177
174
|
* FTS5 matches the table itself, so this works only where the table *is* an FTS5 virtual table (UQL does
|
|
178
175
|
* not create those; declare it outside your entities). The whole query is bound, column filter and all.
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -288,7 +288,7 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
|
|
|
288
288
|
readonly columnType?: ColumnType | QueryRaw;
|
|
289
289
|
/** A string column's length. */
|
|
290
290
|
readonly length?: number;
|
|
291
|
-
/** A decimal column's
|
|
291
|
+
/** A decimal column's digits, or a timestamp's fractional-second digits. */
|
|
292
292
|
readonly precision?: number;
|
|
293
293
|
/** A decimal column's scale. */
|
|
294
294
|
readonly scale?: number;
|
|
@@ -138,35 +138,37 @@ export type QueryWhereFieldOperatorMap<T, Raw = QueryRaw> = {
|
|
|
138
138
|
*/
|
|
139
139
|
$between?: readonly [ExpandScalar<T>, ExpandScalar<T>];
|
|
140
140
|
/**
|
|
141
|
-
* whether a string begins with the given
|
|
141
|
+
* whether a string begins with the given text, taken literally (case sensitive).
|
|
142
142
|
*/
|
|
143
143
|
$startsWith?: string;
|
|
144
144
|
/**
|
|
145
|
-
* whether a string begins with the given
|
|
145
|
+
* whether a string begins with the given text, taken literally (case insensitive).
|
|
146
146
|
*/
|
|
147
147
|
$istartsWith?: string;
|
|
148
148
|
/**
|
|
149
|
-
* whether a string ends with the given
|
|
149
|
+
* whether a string ends with the given text, taken literally (case sensitive).
|
|
150
150
|
*/
|
|
151
151
|
$endsWith?: string;
|
|
152
152
|
/**
|
|
153
|
-
* whether a string ends with the given
|
|
153
|
+
* whether a string ends with the given text, taken literally (case insensitive).
|
|
154
154
|
*/
|
|
155
155
|
$iendsWith?: string;
|
|
156
156
|
/**
|
|
157
|
-
* whether a string
|
|
157
|
+
* whether a string contains the given text, taken literally (case sensitive).
|
|
158
158
|
*/
|
|
159
159
|
$includes?: string;
|
|
160
160
|
/**
|
|
161
|
-
* whether a string
|
|
161
|
+
* whether a string contains the given text, taken literally (case insensitive).
|
|
162
162
|
*/
|
|
163
163
|
$iincludes?: string;
|
|
164
164
|
/**
|
|
165
|
-
* whether a string
|
|
165
|
+
* whether a whole string matches the given pattern, the same on every engine: `%` is any run of
|
|
166
|
+
* characters, `_` any one, and `\` makes the next one literal; a last `\` with nothing after it to
|
|
167
|
+
* escape, `'John\'`, is refused (case sensitive).
|
|
166
168
|
*/
|
|
167
169
|
$like?: string;
|
|
168
170
|
/**
|
|
169
|
-
* whether a string
|
|
171
|
+
* whether a whole string matches the given pattern, as `$like` reads it (case insensitive).
|
|
170
172
|
*/
|
|
171
173
|
$ilike?: string;
|
|
172
174
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `YYYY-MM-DD HH:mm:ss.SSS` in UTC, then `zone`: how a date is written, whichever machine writes it. Not
|
|
3
|
+
* `toISOString` as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
|
|
4
|
+
*/
|
|
5
|
+
export declare function utcTimestamp(date: Date, zone?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* A timestamp's text as the `Date` it names, the one rule every driver and hydration read dates by: UTC
|
|
8
|
+
* where it names no zone, its own offset where it does, a bare day at UTC midnight, and the fraction cut
|
|
9
|
+
* to the milliseconds a `Date` holds. Text that is none of these, such as `infinity`, stays text.
|
|
10
|
+
*/
|
|
11
|
+
export declare function decodeDate(text: string): Date | string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `YYYY-MM-DD HH:mm:ss.SSS` in UTC, then `zone`: how a date is written, whichever machine writes it. Not
|
|
3
|
+
* `toISOString` as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
|
|
4
|
+
*/
|
|
5
|
+
export function utcTimestamp(date, zone = '') {
|
|
6
|
+
return date.toISOString().replace('T', ' ').replace('Z', zone);
|
|
7
|
+
}
|
|
8
|
+
/** A time of day that names no zone, which `Date` would read in the process's own. */
|
|
9
|
+
const ZONELESS_TIME = /T[\d:.]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* A timestamp's text as the `Date` it names, the one rule every driver and hydration read dates by: UTC
|
|
12
|
+
* where it names no zone, its own offset where it does, a bare day at UTC midnight, and the fraction cut
|
|
13
|
+
* to the milliseconds a `Date` holds. Text that is none of these, such as `infinity`, stays text.
|
|
14
|
+
*/
|
|
15
|
+
export function decodeDate(text) {
|
|
16
|
+
const iso = text.replace(' ', 'T').replace(/(\.\d{3})\d+/, '$1');
|
|
17
|
+
const date = new Date(ZONELESS_TIME.test(iso) ? `${iso}Z` : iso);
|
|
18
|
+
return Number.isNaN(date.getTime()) ? text : date;
|
|
19
|
+
}
|
|
@@ -63,11 +63,8 @@ export declare function whereEach<E>(keys: readonly FieldKey<E>[], valueOf: (key
|
|
|
63
63
|
export declare function whereAnyOf<E>(clauses: QueryWhereArray<E>): QueryWhere<E>;
|
|
64
64
|
/** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
|
|
65
65
|
export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
* narrowing cast: `Array.isArray` does not narrow `readonly` arrays out of a union.
|
|
69
|
-
*/
|
|
70
|
-
export declare function asSelectMap<E>(select: QuerySelectValue<E> | undefined): QuerySelect<E> | undefined;
|
|
66
|
+
/** Whether `select` is the list form `raw()` fills, narrowing both ways, which `Array.isArray` does not for a `readonly` array. */
|
|
67
|
+
export declare function isSelectList<E>(select: QuerySelectValue<E> | undefined): select is readonly QueryRaw[];
|
|
71
68
|
export declare function normalizeScalarFieldSelection<E>(meta: EntityMeta<E>, select?: QuerySelect<E>, exclude?: QueryExclude<E>): FieldKey<E>[];
|
|
72
69
|
/** Type guard: checks whether a sort value is a vector similarity search. */
|
|
73
70
|
export declare function isVectorSearch(value: unknown): value is QueryVectorSearch;
|
|
@@ -138,12 +138,9 @@ export function whereAnyOf(clauses) {
|
|
|
138
138
|
export function idOnlyQuery(meta, q) {
|
|
139
139
|
return { ...q, $select: keySet(meta.ids) };
|
|
140
140
|
}
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*/
|
|
145
|
-
export function asSelectMap(select) {
|
|
146
|
-
return Array.isArray(select) ? undefined : select;
|
|
141
|
+
/** Whether `select` is the list form `raw()` fills, narrowing both ways, which `Array.isArray` does not for a `readonly` array. */
|
|
142
|
+
export function isSelectList(select) {
|
|
143
|
+
return Array.isArray(select);
|
|
147
144
|
}
|
|
148
145
|
export function normalizeScalarFieldSelection(meta, select, exclude) {
|
|
149
146
|
// A positive `$select` (the common case) wins outright and returns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ColumnFamily, type FamilyOf, type FieldOptions, QueryRaw, type StampEvent } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* The column
|
|
3
|
+
* The column families each field option means anything on, or `'*'` where it applies to every column.
|
|
4
4
|
* Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
|
|
5
5
|
* discipline `INDEX_FEATURE_LABELS` uses for index features.
|
|
6
6
|
*/
|
|
@@ -23,7 +23,7 @@ declare const FIELD_OPTION_FAMILY: {
|
|
|
23
23
|
readonly version: 'numeric';
|
|
24
24
|
readonly columnType: '*';
|
|
25
25
|
readonly length: 'string';
|
|
26
|
-
readonly precision:
|
|
26
|
+
readonly precision: readonly ["numeric", "date"];
|
|
27
27
|
readonly scale: 'numeric';
|
|
28
28
|
readonly nullable: '*';
|
|
29
29
|
readonly unique: '*';
|
|
@@ -91,8 +91,10 @@ type DeadOptions<O> = (O extends {
|
|
|
91
91
|
readonly nullable: true;
|
|
92
92
|
} ? 'nullable' : never);
|
|
93
93
|
type Given<O> = Extract<keyof O, keyof FieldOptions>;
|
|
94
|
+
/** The families option `K` applies to, `'*'` for every one. */
|
|
95
|
+
type OptionFamilies<K extends keyof FieldOptions> = (typeof FIELD_OPTION_FAMILY)[K] extends readonly (infer F)[] ? F : (typeof FIELD_OPTION_FAMILY)[K];
|
|
94
96
|
type Offending<O> = {
|
|
95
|
-
[K in Given<O>]:
|
|
97
|
+
[K in Given<O>]: [Extract<OptionFamilies<K>, OptionsFamily<O> | '*'>] extends [never] ? K : K extends DeadOptions<O> ? K : never;
|
|
96
98
|
}[Given<O>];
|
|
97
99
|
/**
|
|
98
100
|
* Every option `O` states but cannot use, mapped to `never`, so one that would be ignored does not compile.
|
|
@@ -3,7 +3,7 @@ import { columnFamily, isInlinedExpression } from './field.util.js';
|
|
|
3
3
|
import { getKeys } from './object.util.js';
|
|
4
4
|
import { constantSql } from './raw.js';
|
|
5
5
|
/**
|
|
6
|
-
* The column
|
|
6
|
+
* The column families each field option means anything on, or `'*'` where it applies to every column.
|
|
7
7
|
* Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
|
|
8
8
|
* discipline `INDEX_FEATURE_LABELS` uses for index features.
|
|
9
9
|
*/
|
|
@@ -26,7 +26,8 @@ const FIELD_OPTION_FAMILY = {
|
|
|
26
26
|
version: 'numeric',
|
|
27
27
|
columnType: '*',
|
|
28
28
|
length: 'string',
|
|
29
|
-
|
|
29
|
+
// A decimal's digits, or a timestamp's fractional-second digits.
|
|
30
|
+
precision: ['numeric', 'date'],
|
|
30
31
|
scale: 'numeric',
|
|
31
32
|
nullable: '*',
|
|
32
33
|
unique: '*',
|
|
@@ -116,11 +117,11 @@ export function fieldOptionConflict(opts) {
|
|
|
116
117
|
// conflicts always reports the same one. An option no rule knows is a typo, which `@Field`'s own check
|
|
117
118
|
// reports where it can still be spelled right.
|
|
118
119
|
for (const key of getKeys(FIELD_OPTION_FAMILY)) {
|
|
119
|
-
const applies = FIELD_OPTION_FAMILY[key];
|
|
120
|
+
const applies = [FIELD_OPTION_FAMILY[key]].flat();
|
|
120
121
|
if (opts[key] === undefined)
|
|
121
122
|
continue;
|
|
122
|
-
if (family && applies
|
|
123
|
-
return `cannot use '${key}': it applies to a ${applies} column, not to a ${family} one`;
|
|
123
|
+
if (family && !applies.includes('*') && !applies.includes(family)) {
|
|
124
|
+
return `cannot use '${key}': it applies to a ${applies.join(' or ')} column, not to a ${family} one`;
|
|
124
125
|
}
|
|
125
126
|
const dead = deadOn(opts, key);
|
|
126
127
|
if (dead) {
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
export declare function escapeSingleQuotes(val: string): string;
|
|
3
3
|
/** The text a MySQL string literal's body stands for: its backslash escapes and doubled quotes undone. */
|
|
4
4
|
export declare function unescapeMysqlString(body: string): string;
|
|
5
|
-
/**
|
|
5
|
+
/** How a Postgres timestamp says it is UTC. */
|
|
6
|
+
export declare const PG_UTC = "+00";
|
|
7
|
+
/** Escape `value` for SQLite and SQL Server (single-quote doubling). */
|
|
6
8
|
export declare const escapeAnsiSqlLiteral: (value: unknown) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
|
|
11
|
+
* in the session's zone.
|
|
12
|
+
*/
|
|
13
|
+
export declare const escapePgSqlLiteral: (value: unknown) => string;
|
|
7
14
|
/** Escape `value` for MySQL and MariaDB (backslash escaping). */
|
|
8
15
|
export declare const escapeMysqlSqlLiteral: (value: unknown) => string;
|
package/dist/util/sqlLiteral.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SQL literal escaping for `Dialect.escape`: ANSI quote doubling, or MySQL's backslashes. UQL binds
|
|
2
2
|
// values instead, so this is the hand-written-SQL hatch, and inline MySQL literals break under
|
|
3
3
|
// `NO_BACKSLASH_ESCAPES` or a GBK-like charset: prefer bound parameters. Postgres arrays are separate.
|
|
4
|
+
import { utcTimestamp } from './date.js';
|
|
4
5
|
import { UqlUsageError } from './uqlError.js';
|
|
5
6
|
const SINGLE_QUOTE = /'/g;
|
|
6
7
|
/** Doubles every single quote in `val`, the ANSI escaping shared by string literals and JSON path keys. */
|
|
@@ -43,12 +44,8 @@ function bytesToHexLiteral(bytes) {
|
|
|
43
44
|
* A factory, not a function taking `escapeString` as an argument: threading it through every call
|
|
44
45
|
* measured 1.1-1.5x slower. Rejects unsupported types rather than stringifying them into SQL.
|
|
45
46
|
*/
|
|
46
|
-
function createEscaper(escapeString) {
|
|
47
|
-
|
|
48
|
-
* `YYYY-MM-DD HH:mm:ss.SSS` in UTC, so the SQL is the same whichever machine wrote it. Not `toISOString`
|
|
49
|
-
* as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
|
|
50
|
-
*/
|
|
51
|
-
const dateLiteral = (date) => escapeString(date.toISOString().replace('T', ' ').replace('Z', ''));
|
|
47
|
+
function createEscaper(escapeString, zone = '') {
|
|
48
|
+
const dateLiteral = (date) => escapeString(utcTimestamp(date, zone));
|
|
52
49
|
const sqlList = (arr) => {
|
|
53
50
|
let sql = '';
|
|
54
51
|
for (let i = 0; i < arr.length; i++) {
|
|
@@ -98,7 +95,14 @@ function createEscaper(escapeString) {
|
|
|
98
95
|
};
|
|
99
96
|
return escapeValue;
|
|
100
97
|
}
|
|
101
|
-
/**
|
|
98
|
+
/** How a Postgres timestamp says it is UTC. */
|
|
99
|
+
export const PG_UTC = '+00';
|
|
100
|
+
/** Escape `value` for SQLite and SQL Server (single-quote doubling). */
|
|
102
101
|
export const escapeAnsiSqlLiteral = createEscaper(ansiStringLiteral);
|
|
102
|
+
/**
|
|
103
|
+
* Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
|
|
104
|
+
* in the session's zone.
|
|
105
|
+
*/
|
|
106
|
+
export const escapePgSqlLiteral = createEscaper(ansiStringLiteral, PG_UTC);
|
|
103
107
|
/** Escape `value` for MySQL and MariaDB (backslash escaping). */
|
|
104
108
|
export const escapeMysqlSqlLiteral = createEscaper(mysqlStringLiteral);
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.83.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
package/skills/uql-orm/SKILL.md
CHANGED
|
@@ -74,7 +74,7 @@ export class Post {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
- Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date
|
|
77
|
+
- Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date` (an instant, bound and read as UTC on every engine; `TIMESTAMPTZ` on Postgres and CockroachDB, `DATETIME(3)` on MySQL and MariaDB; `precision` sets its fractional-second digits), `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
|
|
78
78
|
- A column is nullable unless it says `nullable: false`, and its property must admit `null` to match: `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
|
|
79
79
|
- An engine's own column type is a `raw` constant, ``columnType: raw`tsvector` ``, rendered verbatim and carrying its own `length`/`precision`: never a bare string.
|
|
80
80
|
- Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
|