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
|
@@ -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.
|
package/dist/http/handler.js
CHANGED
|
@@ -42,13 +42,12 @@ export function createRequestHandler(opts) {
|
|
|
42
42
|
};
|
|
43
43
|
async function run(entity, { op, method, id }, req) {
|
|
44
44
|
const meta = getMeta(entity);
|
|
45
|
-
// QUERY (RFC 10008) carries the JSON query in the body instead of the query string
|
|
46
|
-
const rawQuery = method === 'QUERY' ? req.body : req.query;
|
|
47
45
|
const hookCtx = {
|
|
48
46
|
meta,
|
|
49
47
|
op,
|
|
50
48
|
method,
|
|
51
|
-
|
|
49
|
+
// QUERY (RFC 10008) carries the JSON query in the body instead of the query string
|
|
50
|
+
query: parseQueryParams(method === 'QUERY' ? req.body : req.query),
|
|
52
51
|
body: req.body,
|
|
53
52
|
context: req.context,
|
|
54
53
|
};
|
|
@@ -69,11 +68,9 @@ export function createRequestHandler(opts) {
|
|
|
69
68
|
else {
|
|
70
69
|
await preFilter?.(hookCtx);
|
|
71
70
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return resp;
|
|
71
|
+
const envelope = await dispatch();
|
|
72
|
+
await post?.(hookCtx, envelope);
|
|
73
|
+
return { status: 200, body: envelope };
|
|
77
74
|
});
|
|
78
75
|
function dispatch() {
|
|
79
76
|
// read post-hooks so both in-place mutation and reassignment of hookCtx.query apply
|
|
@@ -84,59 +81,59 @@ export function createRequestHandler(opts) {
|
|
|
84
81
|
case 'findOne':
|
|
85
82
|
return withQuerier(async (querier) => {
|
|
86
83
|
const data = await querier.findOne(entity, query);
|
|
87
|
-
return
|
|
84
|
+
return { data, count: data ? 1 : 0 };
|
|
88
85
|
});
|
|
89
86
|
case 'count':
|
|
90
87
|
return withQuerier(async (querier) => {
|
|
91
88
|
const count = await querier.count(entity, query);
|
|
92
|
-
return
|
|
89
|
+
return { data: count, count };
|
|
93
90
|
});
|
|
94
91
|
case 'findOneById':
|
|
95
92
|
return withQuerier(async (querier) => {
|
|
96
93
|
const data = await querier.findOne(entity, buildIdQuery(meta, id, query));
|
|
97
|
-
return
|
|
94
|
+
return { data, count: data ? 1 : 0 };
|
|
98
95
|
});
|
|
99
96
|
case 'findMany':
|
|
100
97
|
return withQuerier(async (querier) => {
|
|
101
98
|
const findManyPromise = querier.findMany(entity, query);
|
|
102
99
|
const countPromise = flags.count ? querier.count(entity, query) : undefined;
|
|
103
100
|
const [data, count] = await Promise.all([findManyPromise, countPromise]);
|
|
104
|
-
return
|
|
101
|
+
return { data, count };
|
|
105
102
|
});
|
|
106
103
|
case 'insertOne':
|
|
107
104
|
return withTransaction(async (querier) => {
|
|
108
105
|
const data = await querier.insertOne(entity, hookCtx.body);
|
|
109
|
-
return
|
|
106
|
+
return { data, count: 1 };
|
|
110
107
|
});
|
|
111
108
|
case 'insertMany':
|
|
112
109
|
return withTransaction(async (querier) => {
|
|
113
110
|
const data = await querier.insertMany(entity, hookCtx.body);
|
|
114
|
-
return
|
|
111
|
+
return { data, count: data.length };
|
|
115
112
|
});
|
|
116
113
|
case 'saveOne':
|
|
117
114
|
return withTransaction(async (querier) => {
|
|
118
115
|
const data = await querier.saveOne(entity, hookCtx.body);
|
|
119
|
-
return
|
|
116
|
+
return { data, count: 1 };
|
|
120
117
|
});
|
|
121
118
|
case 'saveMany':
|
|
122
119
|
return withTransaction(async (querier) => {
|
|
123
120
|
const data = await querier.saveMany(entity, hookCtx.body);
|
|
124
|
-
return
|
|
121
|
+
return { data, count: data.length };
|
|
125
122
|
});
|
|
126
123
|
case 'updateOneById':
|
|
127
124
|
return withTransaction(async (querier) => {
|
|
128
125
|
const count = await querier.updateMany(entity, buildIdQuery(meta, id, query), hookCtx.body);
|
|
129
|
-
return
|
|
126
|
+
return { data: id, count };
|
|
130
127
|
});
|
|
131
128
|
case 'updateMany':
|
|
132
129
|
return withTransaction(async (querier) => {
|
|
133
130
|
const count = await querier.updateMany(entity, query, hookCtx.body);
|
|
134
|
-
return
|
|
131
|
+
return { data: count, count };
|
|
135
132
|
});
|
|
136
133
|
case 'deleteOneById':
|
|
137
134
|
return withTransaction(async (querier) => {
|
|
138
135
|
const count = await querier.deleteMany(entity, buildIdQuery(meta, id, query), { hardDelete });
|
|
139
|
-
return
|
|
136
|
+
return { data: id, count };
|
|
140
137
|
});
|
|
141
138
|
case 'deleteMany':
|
|
142
139
|
return withTransaction(async (querier) => {
|
|
@@ -148,15 +145,12 @@ export function createRequestHandler(opts) {
|
|
|
148
145
|
ids = founds.map((found) => found[idKey]);
|
|
149
146
|
count = await querier.deleteMany(entity, { $where: whereIds(meta, ids) }, { hardDelete });
|
|
150
147
|
}
|
|
151
|
-
return
|
|
148
|
+
return { data: ids, count };
|
|
152
149
|
});
|
|
153
150
|
}
|
|
154
151
|
}
|
|
155
152
|
}
|
|
156
153
|
}
|
|
157
|
-
function ok(body) {
|
|
158
|
-
return { status: 200, body };
|
|
159
|
-
}
|
|
160
154
|
function buildIdQuery(meta, id, query) {
|
|
161
155
|
query.$where = whereWith(soleIdOf(meta, 'the HTTP handler'), id, query.$where);
|
|
162
156
|
return query;
|
package/dist/http/query.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { WireQuery } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Parse raw query-string entries (with JSON-stringified values) into a UQL query
|
|
4
|
-
* Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
3
|
+
* Parse raw query-string entries (with JSON-stringified values), or a `QUERY` body, into a UQL query
|
|
4
|
+
* object. Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
5
5
|
*/
|
|
6
|
-
export declare function parseQueryParams<E = unknown>(params?:
|
|
6
|
+
export declare function parseQueryParams<E = unknown>(params?: unknown): WireQuery<E>;
|
|
7
7
|
/**
|
|
8
8
|
* Serialize a UQL query object into a percent-encoded query string where object values
|
|
9
9
|
* are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
|
package/dist/http/query.js
CHANGED
|
@@ -4,7 +4,7 @@ import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUER
|
|
|
4
4
|
// with it, in the browser bundle, which is on a size budget
|
|
5
5
|
import { RAW_VALUE } from '../type/queryRaw.js';
|
|
6
6
|
// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata
|
|
7
|
-
import { getKeys, isWhereMap } from '../util/object.util.js';
|
|
7
|
+
import { getKeys, isRecord, isWhereMap } from '../util/object.util.js';
|
|
8
8
|
// the error class alone, from its own leaf module: `queryError.ts` carries every driver's code map
|
|
9
9
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
10
10
|
/**
|
|
@@ -30,10 +30,13 @@ const ALLOWED_QUERY_KEYS = new Set([
|
|
|
30
30
|
*/
|
|
31
31
|
const REJECTED_QUERY_KEYS = new Set(['$lock']);
|
|
32
32
|
/**
|
|
33
|
-
* Parse raw query-string entries (with JSON-stringified values) into a UQL query
|
|
34
|
-
* Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
33
|
+
* Parse raw query-string entries (with JSON-stringified values), or a `QUERY` body, into a UQL query
|
|
34
|
+
* object. Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
35
35
|
*/
|
|
36
36
|
export function parseQueryParams(params = {}) {
|
|
37
|
+
if (!isRecord(params)) {
|
|
38
|
+
throw new UqlUsageError('the query must be a JSON object');
|
|
39
|
+
}
|
|
37
40
|
const query = {};
|
|
38
41
|
for (const key of getKeys(params)) {
|
|
39
42
|
if (REJECTED_QUERY_KEYS.has(key)) {
|
|
@@ -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);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
3
|
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, nullsSortField, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
|
|
4
|
-
import { betweenBounds, GROUP_OPS, groupClauses, isGroupOp, whereOperators } from '../dialect/operators.js';
|
|
4
|
+
import { betweenBounds, GROUP_OPS, groupClauses, isGroupOp, LIKE_OPS, likeRegex, whereOperators, } from '../dialect/operators.js';
|
|
5
5
|
import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
6
6
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
7
7
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
8
8
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
9
|
-
import { aggregateOf,
|
|
9
|
+
import { aggregateOf, isSelectList, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
|
|
10
10
|
import { UqlUsageError } from '../util/uqlError.js';
|
|
11
11
|
import { decodeBigIntsExcept } from '../util/wideNumber.js';
|
|
12
12
|
import { textLanguage } from './textLanguage.js';
|
|
@@ -71,17 +71,6 @@ function compareCount(count, size) {
|
|
|
71
71
|
}
|
|
72
72
|
return comparisons.length === 1 ? comparisons[0] : { $and: comparisons };
|
|
73
73
|
}
|
|
74
|
-
/** String operators -> { pattern: (v) => regex, caseInsensitive } */
|
|
75
|
-
const REGEX_OP_MAP = new Map([
|
|
76
|
-
['$startsWith', { wrap: (v) => `^${v}`, ci: false }],
|
|
77
|
-
['$istartsWith', { wrap: (v) => `^${v}`, ci: true }],
|
|
78
|
-
['$endsWith', { wrap: (v) => `${v}$`, ci: false }],
|
|
79
|
-
['$iendsWith', { wrap: (v) => `${v}$`, ci: true }],
|
|
80
|
-
['$includes', { wrap: (v) => String(v), ci: false }],
|
|
81
|
-
['$iincludes', { wrap: (v) => String(v), ci: true }],
|
|
82
|
-
['$like', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: false }],
|
|
83
|
-
['$ilike', { wrap: (v) => String(v).replace(/%/g, '.*').replace(/_/g, '.'), ci: true }],
|
|
84
|
-
]);
|
|
85
74
|
/** MongoDB native operators - pass through as-is. */
|
|
86
75
|
const NATIVE_OPS = new Set([
|
|
87
76
|
'$all',
|
|
@@ -361,11 +350,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
361
350
|
result[op] = val;
|
|
362
351
|
continue;
|
|
363
352
|
}
|
|
364
|
-
//
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
result['$regex'] =
|
|
368
|
-
if (
|
|
353
|
+
// The `$like` family, as the regex matching what its `LIKE` pattern matches on SQL.
|
|
354
|
+
const like = LIKE_OPS.get(op);
|
|
355
|
+
if (like) {
|
|
356
|
+
result['$regex'] = likeRegex(like.pattern(String(val)));
|
|
357
|
+
if (like.insensitive)
|
|
369
358
|
result['$options'] = 'i';
|
|
370
359
|
continue;
|
|
371
360
|
}
|
|
@@ -429,17 +418,16 @@ export class MongoDialect extends AbstractDialect {
|
|
|
429
418
|
if (!select && !exclude) {
|
|
430
419
|
return {};
|
|
431
420
|
}
|
|
432
|
-
if (
|
|
421
|
+
if (isSelectList(select)) {
|
|
433
422
|
throw new UqlUsageError('raw $select is not supported on MongoDB');
|
|
434
423
|
}
|
|
435
|
-
const selectMap = asSelectMap(select);
|
|
436
424
|
// Projected by column, not by field key; `normalizeId` maps them back on the way out.
|
|
437
|
-
const projection = normalizeScalarFieldSelection(meta,
|
|
425
|
+
const projection = normalizeScalarFieldSelection(meta, select, exclude).reduce((acc, key) => {
|
|
438
426
|
// A computed field writing SQL leaves the document nothing to project: refused asked for by
|
|
439
427
|
// name, skipped swept in with the rest. A relation aggregate is on it by now, like any column.
|
|
440
428
|
const field = meta.fields[key];
|
|
441
429
|
if (field?.computed && !aggregateOf(field)) {
|
|
442
|
-
if (
|
|
430
|
+
if (select && key in select) {
|
|
443
431
|
assertReadable(meta, key);
|
|
444
432
|
}
|
|
445
433
|
return acc;
|
|
@@ -450,7 +438,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
450
438
|
// MongoDB returns `_id` unless it is explicitly excluded, so subtracting the primary key needs
|
|
451
439
|
// `_id: 0` - the one inclusion/exclusion mix MongoDB allows - or `$exclude: { id: true }` would
|
|
452
440
|
// have no effect at all.
|
|
453
|
-
if (this.subtractsKey(soleIdOf(meta, 'MongoDB'),
|
|
441
|
+
if (this.subtractsKey(soleIdOf(meta, 'MongoDB'), select, exclude)) {
|
|
454
442
|
projection[ID_KEY] = 0;
|
|
455
443
|
}
|
|
456
444
|
return projection;
|
|
@@ -565,7 +553,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
565
553
|
/** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
|
|
566
554
|
aggregateKeys(entity, q) {
|
|
567
555
|
const meta = getMeta(entity);
|
|
568
|
-
const projected = normalizeScalarFieldSelection(meta,
|
|
556
|
+
const projected = normalizeScalarFieldSelection(meta, isSelectList(q.$select) ? undefined : q.$select, q.$exclude);
|
|
569
557
|
return [...projected, ...Object.keys(q.$sort ?? {})].filter((key) => aggregateOf(meta.fields[key]));
|
|
570
558
|
}
|
|
571
559
|
/**
|
|
@@ -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>;
|