uql-orm 0.82.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/dist/cockroachdb/crdbQuerierPool.js +2 -2
- package/dist/dialect/abstractSqlDialect.d.ts +1 -1
- package/dist/dialect/abstractSqlDialect.js +1 -1
- 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/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +13 -4
- 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/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/util/date.d.ts +11 -0
- package/dist/util/date.js +19 -0
- 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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Pool, types } from 'pg';
|
|
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 { CockroachDialect } from './cockroachDialect.js';
|
|
6
6
|
/**
|
|
7
7
|
* QuerierPool for CockroachDB using the `pg` driver Pool.
|
|
8
8
|
*/
|
|
9
9
|
export class CrdbQuerierPool extends AbstractPgQuerierPool {
|
|
10
10
|
constructor(opts, extra) {
|
|
11
|
-
super(new CockroachDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types:
|
|
11
|
+
super(new CockroachDialect(dialectOptionsFrom(extra)), new Pool({ keepAlive: true, types: wireTypes(types), ...opts }), extra);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -698,7 +698,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
698
698
|
private sizeCondition;
|
|
699
699
|
/** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
|
|
700
700
|
private vectorNearCondition;
|
|
701
|
-
/** ANSI-style single-quote escaping.
|
|
701
|
+
/** ANSI-style single-quote escaping. */
|
|
702
702
|
escape(value: unknown): string;
|
|
703
703
|
protected get regexpOp(): string;
|
|
704
704
|
/**
|
|
@@ -1953,7 +1953,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1953
1953
|
const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, near, prefix);
|
|
1954
1954
|
return this.boundConditions(ctx, distance, bounds, (operand, op, val) => (isOrderedOp(op) ? this.operatorCondition(ctx, operand, op, val) : undefined), 'unsupported $near bound');
|
|
1955
1955
|
}
|
|
1956
|
-
/** ANSI-style single-quote escaping.
|
|
1956
|
+
/** ANSI-style single-quote escaping. */
|
|
1957
1957
|
escape(value) {
|
|
1958
1958
|
return escapeAnsiSqlLiteral(value);
|
|
1959
1959
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeDate } from '../util/date.js';
|
|
1
2
|
import { decodeWideNumber } from '../util/wideNumber.js';
|
|
2
3
|
import { decodeFloat32s, parseVectorLiteral } from './vectorCast.js';
|
|
3
4
|
/**
|
|
@@ -31,7 +32,7 @@ const float32Decoder = (value) => {
|
|
|
31
32
|
const DECODERS = {
|
|
32
33
|
// 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
|
|
33
34
|
boolean: (value) => (typeof value === 'boolean' ? value : Boolean(value)),
|
|
34
|
-
date: (value) => (typeof value === 'string' ? (
|
|
35
|
+
date: (value) => (typeof value === 'string' ? decodeDate(value) : value),
|
|
35
36
|
// Only a string can be bytes that crossed JSON: bytes a driver already decoded stay as they are.
|
|
36
37
|
bytes: (value) => typeof value === 'string' && value.startsWith(BYTES_PREFIX) ? hexBytes(value.slice(BYTES_PREFIX.length)) : value,
|
|
37
38
|
// A number too, not just text: `type: BigInt` is BIGINT, which the pg pools decode at the wire.
|
|
@@ -61,17 +62,6 @@ const DECODERS = {
|
|
|
61
62
|
halfvec: vectorDecoder('halfvec'),
|
|
62
63
|
sparsevec: vectorDecoder('sparsevec'),
|
|
63
64
|
};
|
|
64
|
-
/**
|
|
65
|
-
* An ISO 8601 timestamp as a `Date`, its fraction cut to the milliseconds one holds, and a bare date at
|
|
66
|
-
* local midnight, which is how `pg` reads a `date`. `undefined` for text that is neither.
|
|
67
|
-
*/
|
|
68
|
-
function parseDate(text) {
|
|
69
|
-
const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
70
|
-
const date = day
|
|
71
|
-
? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]))
|
|
72
|
-
: new Date(text.replace(/(\.\d{3})\d+/, '$1'));
|
|
73
|
-
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
74
|
-
}
|
|
75
65
|
/**
|
|
76
66
|
* What bytes crossing JSON start with, before two hex digits per byte: Postgres's own text for `bytea`,
|
|
77
67
|
* which every dialect spells, so a string a driver reads from a column on its own is never mistaken.
|
|
@@ -78,6 +78,8 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
78
78
|
/** Bytes as the hex text `decodeColumn` reads back, whole. */
|
|
79
79
|
protected bytesAsText(expr: string): string;
|
|
80
80
|
escape(value: unknown): string;
|
|
81
|
+
/** A date as UTC text, which a `DATETIME` stores as is, where a driver would convert it to its own zone. */
|
|
82
|
+
normalizeValue(value: unknown): unknown;
|
|
81
83
|
/**
|
|
82
84
|
* `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
|
|
83
85
|
* the server answers "Can't find FULLTEXT index matching the column list". Declare it with
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { utcTimestamp } from '../util/date.js';
|
|
2
3
|
import { textSearchFields } from '../util/index.js';
|
|
3
4
|
import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
5
|
import { AbstractSqlDialect, } from './abstractSqlDialect.js';
|
|
@@ -187,6 +188,10 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
187
188
|
escape(value) {
|
|
188
189
|
return escapeMysqlSqlLiteral(value);
|
|
189
190
|
}
|
|
191
|
+
/** A date as UTC text, which a `DATETIME` stores as is, where a driver would convert it to its own zone. */
|
|
192
|
+
normalizeValue(value) {
|
|
193
|
+
return value instanceof Date ? utcTimestamp(value) : super.normalizeValue(value);
|
|
194
|
+
}
|
|
190
195
|
/**
|
|
191
196
|
* `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
|
|
192
197
|
* the server answers "Can't find FULLTEXT index matching the column list". Declare it with
|
|
@@ -105,6 +105,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
105
105
|
* array takes its type from `operand`, so it needs none of the casts `bind` would give each value.
|
|
106
106
|
*/
|
|
107
107
|
protected formatIn(ctx: QueryContext, operand: string, values: unknown[], negate: boolean, bind: (value: unknown) => string): string;
|
|
108
|
+
escape(value: unknown): string;
|
|
108
109
|
protected numericCast(expr: string): string;
|
|
109
110
|
protected appendJsonValue(ctx: QueryContext, value: unknown, type: JsonColumnType): void;
|
|
110
111
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
|
+
import { utcTimestamp } from '../util/date.js';
|
|
2
3
|
import { fulltextConfig, fulltextIndexOver, hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
3
|
-
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
|
+
import { escapePgSqlLiteral, escapeSingleQuotes, PG_UTC } from '../util/sqlLiteral.js';
|
|
4
5
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
6
|
import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
|
|
6
7
|
import { BYTES_PREFIX } from './hydrateColumn.js';
|
|
@@ -128,10 +129,11 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
128
129
|
return statements;
|
|
129
130
|
}
|
|
130
131
|
normalizeValue(value) {
|
|
131
|
-
if (
|
|
132
|
-
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
const values = value.map(utcDate);
|
|
134
|
+
return this.driverCapabilities.nativeArrays ? values : toPgArray(values);
|
|
133
135
|
}
|
|
134
|
-
return super.normalizeValue(value);
|
|
136
|
+
return super.normalizeValue(utcDate(value));
|
|
135
137
|
}
|
|
136
138
|
placeholder(index) {
|
|
137
139
|
return `$${index}`;
|
|
@@ -240,6 +242,9 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
240
242
|
const ph = this.addValue(ctx, values);
|
|
241
243
|
return negate ? `${operand} <> ALL(${ph})` : `${operand} = ANY(${ph})`;
|
|
242
244
|
}
|
|
245
|
+
escape(value) {
|
|
246
|
+
return escapePgSqlLiteral(value);
|
|
247
|
+
}
|
|
243
248
|
numericCast(expr) {
|
|
244
249
|
return `(${expr})::numeric`;
|
|
245
250
|
}
|
|
@@ -306,6 +311,10 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
306
311
|
return this.driverCapabilities.explicitJsonCast ? `(${ph}::text)::${type}` : `${ph}::${type}`;
|
|
307
312
|
}
|
|
308
313
|
}
|
|
314
|
+
/** A date as UTC text, which a zoneless `TIMESTAMP` stores as is, where each driver would pick its own zone. */
|
|
315
|
+
function utcDate(value) {
|
|
316
|
+
return value instanceof Date ? utcTimestamp(value, PG_UTC) : value;
|
|
317
|
+
}
|
|
309
318
|
/**
|
|
310
319
|
* Converts a JS array to a Postgres array literal string: `{"val1","val2"}`.
|
|
311
320
|
* Safely handles nesting and escaping of special characters.
|
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -15,6 +15,8 @@ const MYSQL = {
|
|
|
15
15
|
};
|
|
16
16
|
/** MySQL 8.0.13+ rejects `DEFAULT 'x'` on these but accepts `DEFAULT ('x')`, whatever the value. */
|
|
17
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;
|
|
18
20
|
/**
|
|
19
21
|
* Looked up by name rather than carried on the dialect, which keeps DDL data out of the query
|
|
20
22
|
* bundle - the same split that keeps `CANONICAL_TO_SQL` in `schema/canonicalType.ts`. `uuidv7()` is
|
|
@@ -24,8 +26,12 @@ const MYSQL_LARGE_TYPES = /^\s*(TINY|MEDIUM|LONG)?(TEXT|BLOB)|^\s*(JSON|GEOMETRY
|
|
|
24
26
|
export const DIALECT_DEFAULTS = {
|
|
25
27
|
postgres: { expressions: { ...PG, uuidv7: 'uuidv7()' } },
|
|
26
28
|
cockroachdb: { expressions: PG },
|
|
27
|
-
mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES },
|
|
28
|
-
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
|
+
},
|
|
29
35
|
sqlite: { expressions: ANSI },
|
|
30
36
|
// `SYSUTCDATETIME()` over `CURRENT_TIMESTAMP`, which is local time in the server's zone. No
|
|
31
37
|
// `uuidv7`: `NEWSEQUENTIALID()` is an ordered v4 GUID, so it carries no readable timestamp and
|
|
@@ -80,7 +86,7 @@ export const expr = {
|
|
|
80
86
|
* the result needs wrapping, which MySQL demands on its large types whatever the value.
|
|
81
87
|
*/
|
|
82
88
|
export function formatDefaultValue(value, dialect, columnType) {
|
|
83
|
-
const sql = defaultLiteral(value, dialect);
|
|
89
|
+
const sql = defaultLiteral(value, dialect, columnType);
|
|
84
90
|
const { wrapTypes } = DIALECT_DEFAULTS[dialect.dialectName];
|
|
85
91
|
return columnType !== undefined && wrapTypes?.test(columnType) ? `(${sql})` : sql;
|
|
86
92
|
}
|
|
@@ -114,23 +120,26 @@ export function sameDefault(desired, current, dialect) {
|
|
|
114
120
|
* cannot serve stay here: a boolean is `1` where booleans are integers, and a plain object or array
|
|
115
121
|
* is JSON rather than the throw and the IN-list `escape` gives them.
|
|
116
122
|
*/
|
|
117
|
-
function defaultLiteral(value, dialect) {
|
|
123
|
+
function defaultLiteral(value, dialect, columnType) {
|
|
118
124
|
if (value === undefined || value === null) {
|
|
119
125
|
return 'NULL';
|
|
120
126
|
}
|
|
121
127
|
if (SqlExpression.isExpression(value)) {
|
|
122
|
-
return expressionSql(value, dialect);
|
|
128
|
+
return expressionSql(value, dialect, columnType);
|
|
123
129
|
}
|
|
124
130
|
if (typeof value === 'boolean') {
|
|
125
131
|
return dialect.booleanLiteral === 'native' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0';
|
|
126
132
|
}
|
|
127
133
|
return dialect.escape(typeof value === 'object' && !(value instanceof Date) ? JSON.stringify(value) : value);
|
|
128
134
|
}
|
|
129
|
-
function expressionSql(expression, dialect) {
|
|
130
|
-
const { expressions } = DIALECT_DEFAULTS[dialect.dialectName];
|
|
131
|
-
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];
|
|
132
139
|
if (sql == null) {
|
|
133
140
|
throw new UqlUsageError(`${dialect.dialectName} has no '${expression.kind}' default; pass expr.raw(...) with SQL this engine accepts`);
|
|
134
141
|
}
|
|
135
|
-
|
|
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})`);
|
|
136
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();
|
|
@@ -33,7 +33,7 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
|
|
|
33
33
|
generateDropTable(tableName: string): string;
|
|
34
34
|
/** A collection's indexes: each dropped, then each created, an alter as both. */
|
|
35
35
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
36
|
-
/** MongoDB
|
|
36
|
+
/** MongoDB runs no trigger within a write, and a write to an entity declaring one is refused, so there is none to reconcile. */
|
|
37
37
|
generateTriggers(): string[];
|
|
38
38
|
generateTriggersDown(): string[];
|
|
39
39
|
generateTriggerDrops(): string[];
|
|
@@ -115,7 +115,7 @@ export class MongoSchemaGenerator extends MongoDialect {
|
|
|
115
115
|
...sides(diff.indexes, 'to').map((index) => this.generateCreateIndex(diff.tableName, index)),
|
|
116
116
|
];
|
|
117
117
|
}
|
|
118
|
-
/** MongoDB
|
|
118
|
+
/** MongoDB runs no trigger within a write, and a write to an entity declaring one is refused, so there is none to reconcile. */
|
|
119
119
|
generateTriggers() {
|
|
120
120
|
return [];
|
|
121
121
|
}
|
|
@@ -10,7 +10,7 @@ export declare class MongoSchemaIntrospector implements SchemaIntrospector {
|
|
|
10
10
|
/** `listIndexes` reports keys, uniqueness and text weights; a `partialFilterExpression` is no SQL predicate. */
|
|
11
11
|
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
12
12
|
constructor(pool: QuerierPool);
|
|
13
|
-
/** MongoDB
|
|
13
|
+
/** MongoDB runs no trigger within a write, so uql installs none. */
|
|
14
14
|
ownedTriggers(): Promise<InstalledTriggers>;
|
|
15
15
|
introspect(tables?: readonly string[]): Promise<SchemaAST>;
|
|
16
16
|
getTableSchema(tableName: string): Promise<TableSchema | undefined>;
|
|
@@ -15,7 +15,7 @@ export class MongoSchemaIntrospector {
|
|
|
15
15
|
constructor(pool) {
|
|
16
16
|
this.pool = pool;
|
|
17
17
|
}
|
|
18
|
-
/** MongoDB
|
|
18
|
+
/** MongoDB runs no trigger within a write, so uql installs none. */
|
|
19
19
|
async ownedTriggers() {
|
|
20
20
|
return new Map();
|
|
21
21
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defaultTimestampPrecision } from '../../schema/canonicalType.js';
|
|
1
2
|
import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
|
|
2
3
|
/**
|
|
3
4
|
* SQL Server schema introspector.
|
|
@@ -137,7 +138,7 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
137
138
|
const bytes = this.toNumber(row.max_length);
|
|
138
139
|
return {
|
|
139
140
|
name: row.column_name,
|
|
140
|
-
type:
|
|
141
|
+
type: spelledType(type, bytes, this.toNumber(row.numeric_scale)),
|
|
141
142
|
nullable: Boolean(row.is_nullable),
|
|
142
143
|
defaultValue: this.parseDefaultValue(row.column_default),
|
|
143
144
|
isAutoIncrement: Boolean(row.is_identity),
|
|
@@ -183,6 +184,17 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
183
184
|
/** The types whose `max_length` is a width rather than a fixed storage size. */
|
|
184
185
|
const CHARACTER_TYPES = new Set(['CHAR', 'NCHAR', 'VARCHAR', 'NVARCHAR', 'BINARY', 'VARBINARY']);
|
|
185
186
|
const NUMERIC_TYPES = new Set(['DECIMAL', 'NUMERIC']);
|
|
187
|
+
/** `(MAX)` on an unbounded character type, and a timestamp's fractional digits where not the engine's default. */
|
|
188
|
+
function spelledType(type, bytes, scale) {
|
|
189
|
+
if (bytes === -1 && CHARACTER_TYPES.has(type)) {
|
|
190
|
+
return `${type}(MAX)`;
|
|
191
|
+
}
|
|
192
|
+
return (type === 'DATETIME2' || type === 'DATETIMEOFFSET') &&
|
|
193
|
+
scale !== undefined &&
|
|
194
|
+
scale !== defaultTimestampPrecision('mssql')
|
|
195
|
+
? `${type}(${scale})`
|
|
196
|
+
: type;
|
|
197
|
+
}
|
|
186
198
|
/**
|
|
187
199
|
* A column's declared width from `max_length`, which is bytes: an `N` type holds two a character, a
|
|
188
200
|
* `VECTOR` is an 8-byte header and four a dimension, and `MAX` is `-1`. Read as bytes, every Unicode
|
|
@@ -29,6 +29,7 @@ export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
|
|
|
29
29
|
}
|
|
30
30
|
type MysqlColumnRow = {
|
|
31
31
|
column_name: string;
|
|
32
|
+
data_type: string;
|
|
32
33
|
column_type: string;
|
|
33
34
|
is_nullable: string;
|
|
34
35
|
column_default: string | null;
|
|
@@ -36,6 +37,7 @@ type MysqlColumnRow = {
|
|
|
36
37
|
extra: string;
|
|
37
38
|
character_maximum_length: number | bigint | null;
|
|
38
39
|
numeric_precision: number | bigint | null;
|
|
40
|
+
datetime_precision: number | bigint | null;
|
|
39
41
|
numeric_scale: number | null;
|
|
40
42
|
column_comment: string | null;
|
|
41
43
|
generated_as: string | null;
|
|
@@ -47,6 +47,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
47
47
|
CHARACTER_MAXIMUM_LENGTH as character_maximum_length,
|
|
48
48
|
NUMERIC_PRECISION as numeric_precision,
|
|
49
49
|
NUMERIC_SCALE as numeric_scale,
|
|
50
|
+
DATETIME_PRECISION as datetime_precision,
|
|
50
51
|
COLUMN_KEY as column_key,
|
|
51
52
|
EXTRA as extra,
|
|
52
53
|
CASE WHEN EXTRA LIKE '%STORED GENERATED%' THEN GENERATION_EXPRESSION END as generated_as,
|
|
@@ -113,7 +114,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
113
114
|
isUnique: row.column_key === 'UNI',
|
|
114
115
|
// A `VECTOR`'s is its bytes, four a dimension, which `column_type` already states as dimensions.
|
|
115
116
|
length: /^vector/i.test(row.column_type) ? undefined : this.toNumber(row.character_maximum_length),
|
|
116
|
-
|
|
117
|
+
// A timestamp's fractional digits, stated even when 0, which uql's own unstated `DATETIME(3)` is not.
|
|
118
|
+
precision: this.toNumber(TIMESTAMP_TYPES.has(row.data_type) ? row.datetime_precision : row.numeric_precision),
|
|
117
119
|
scale: this.toNumber(row.numeric_scale),
|
|
118
120
|
comment: row.column_comment || undefined,
|
|
119
121
|
generatedAs: row.generated_as ?? undefined,
|
|
@@ -146,7 +148,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
146
148
|
if (normalized === 'NULL') {
|
|
147
149
|
return null;
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
// Whatever precision it repeats from its column, which the column's own type already states.
|
|
152
|
+
if (/^CURRENT_TIMESTAMP(?:\(\d?\))?$/.test(normalized)) {
|
|
150
153
|
return 'CURRENT_TIMESTAMP';
|
|
151
154
|
}
|
|
152
155
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
|
@@ -158,6 +161,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
158
161
|
return quoted ? unescapeMysqlString(quoted[1]) : literal;
|
|
159
162
|
}
|
|
160
163
|
}
|
|
164
|
+
const TIMESTAMP_TYPES = new Set(['datetime', 'timestamp']);
|
|
161
165
|
/**
|
|
162
166
|
* MariaDB reads out of the same `information_schema` as MySQL, save for one column type it does not
|
|
163
167
|
* have: `JSON` there is an alias for `LONGTEXT` plus a `json_valid()` check constraint named after
|
|
@@ -75,7 +75,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
75
75
|
c.is_identity,
|
|
76
76
|
c.identity_generation,
|
|
77
77
|
CASE WHEN a.attgenerated = 's' THEN c.generation_expression END AS generated_as,
|
|
78
|
-
CASE WHEN c.data_type IN ('USER-DEFINED', 'vector') AND a.atttypmod > -1
|
|
78
|
+
CASE WHEN (c.data_type IN ('USER-DEFINED', 'vector') OR c.data_type LIKE 'timestamp%') AND a.atttypmod > -1
|
|
79
79
|
THEN format_type(a.atttypid, a.atttypmod) END AS formatted_type,
|
|
80
80
|
EXISTS (
|
|
81
81
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
@@ -32,7 +32,7 @@ export class DatabaseMigrationStorage {
|
|
|
32
32
|
async createTableIfNotExists(querier) {
|
|
33
33
|
const table = new TableBuilder(this.tableName);
|
|
34
34
|
table.string('name', { length: 255, primaryKey: true });
|
|
35
|
-
table.
|
|
35
|
+
table.timestamptz('executed_at', { defaultValue: expr.now() });
|
|
36
36
|
const generator = new SqlSchemaGenerator(querier.dialect);
|
|
37
37
|
for (const sql of generator.generateCreateTableFromDefinition(table.build(), { ifNotExists: true })) {
|
|
38
38
|
await querier.run(sql);
|
|
@@ -13,12 +13,13 @@ function asksForNoRows(q) {
|
|
|
13
13
|
return q.$limit === 0;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* MongoDB
|
|
17
|
-
* silently. Refused instead, as a query naming
|
|
16
|
+
* MongoDB runs no trigger within a write (Atlas Database Triggers fire after the commit), so a write to
|
|
17
|
+
* an entity declaring one - a stamp included - would skip it silently. Refused instead, as a query naming
|
|
18
|
+
* SQL is. The why, in `architecture/triggers.md`.
|
|
18
19
|
*/
|
|
19
20
|
function refuseTriggers(entity) {
|
|
20
21
|
if (hasTriggers(getMeta(entity))) {
|
|
21
|
-
throw new UqlUsageError(`'${entity.name}' declares triggers, which MongoDB
|
|
22
|
+
throw new UqlUsageError(`'${entity.name}' declares triggers, which MongoDB cannot run within a write: a write here would skip them. ` +
|
|
22
23
|
'Keep the entity on a SQL engine, or drop its triggers and stamps.');
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DateTime2 } from 'mssql';
|
|
1
2
|
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
2
3
|
import type { QueryUpdateResult, RawRow, TransactionOptions } from '../type/index.js';
|
|
3
4
|
/** What `tedious` hands back for one statement, whichever shape it took. */
|
|
@@ -17,6 +18,7 @@ type MsSqlRowStream = AsyncIterable<unknown> & {
|
|
|
17
18
|
/** The part of an `mssql` `Request` a querier drives. */
|
|
18
19
|
type MsSqlRequest = {
|
|
19
20
|
input(name: string, value: unknown): unknown;
|
|
21
|
+
input(name: string, type: typeof DateTime2, value: unknown): unknown;
|
|
20
22
|
query(command: string): Promise<MsSqlResult>;
|
|
21
23
|
toReadableStream(): MsSqlRowStream;
|
|
22
24
|
cancel(): unknown;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ISOLATION_LEVEL } from 'mssql';
|
|
1
|
+
import { DateTime2, ISOLATION_LEVEL } from 'mssql';
|
|
2
2
|
import { AbstractPoolQuerier } from '../querier/abstractPoolQuerier.js';
|
|
3
3
|
import { decodeWireTypes } from './mssqlWireTypes.js';
|
|
4
4
|
/**
|
|
@@ -9,13 +9,16 @@ import { decodeWireTypes } from './mssqlWireTypes.js';
|
|
|
9
9
|
export class MsSqlQuerier extends AbstractPoolQuerier {
|
|
10
10
|
#transaction;
|
|
11
11
|
/**
|
|
12
|
-
* Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Values bind by name, `@p1` upward, matching {@link MsSqlDialect.placeholder}. `mssql` infers a type
|
|
13
|
+
* from the JS value, right for a `Uint8Array` and harmlessly wrong for a bare `null` (`NVarChar`), but
|
|
14
|
+
* a `Date` it binds as the legacy `DATETIME`, whose 1/300 s steps no `DATETIME2` column compares equal to.
|
|
15
15
|
*/
|
|
16
16
|
#request(values) {
|
|
17
17
|
const request = this.#transaction ? this.#transaction.request() : this.getConn().request();
|
|
18
|
-
values?.forEach((value, index) =>
|
|
18
|
+
values?.forEach((value, index) => {
|
|
19
|
+
const name = `p${index + 1}`;
|
|
20
|
+
return value instanceof Date ? request.input(name, DateTime2, value) : request.input(name, value);
|
|
21
|
+
});
|
|
19
22
|
return request;
|
|
20
23
|
}
|
|
21
24
|
async internalAll(query, values) {
|
|
@@ -4,6 +4,7 @@ import type { ExtraOptions } from '../type/index.js';
|
|
|
4
4
|
import { MySql2Querier } from './mysql2Querier.js';
|
|
5
5
|
import { MySqlDialect } from './mysqlDialect.js';
|
|
6
6
|
export declare class MySql2QuerierPool extends AbstractSqlQuerierPool<MySql2Querier, MySqlDialect> {
|
|
7
|
+
#private;
|
|
7
8
|
readonly pool: Pool;
|
|
8
9
|
constructor(opts: PoolOptions, extra?: ExtraOptions);
|
|
9
10
|
getQuerier(): Promise<MySql2Querier>;
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -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.0",
|
|
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`.
|