uql-orm 0.77.1 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +4 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
- package/dist/migrate/introspection/mysqlIntrospector.js +2 -0
- package/dist/migrate/introspection/postgresIntrospector.d.ts +4 -0
- package/dist/migrate/introspection/postgresIntrospector.js +9 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +9 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +77 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,6 @@ Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/ma
|
|
|
73
73
|
|
|
74
74
|
## ⭐ Like what we're doing? Give us a star
|
|
75
75
|
|
|
76
|
-
It
|
|
76
|
+
It will help other people find the project.
|
|
77
77
|
|
|
78
78
|
[](https://github.com/rogerpadilla/uql)
|
|
@@ -46,12 +46,14 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
46
46
|
c.is_identity as is_identity,
|
|
47
47
|
d.definition as column_default,
|
|
48
48
|
f.is_primary_key,
|
|
49
|
-
f.is_unique
|
|
49
|
+
f.is_unique,
|
|
50
|
+
CASE WHEN cc.is_persisted = 1 THEN cc.definition END as generated_as
|
|
50
51
|
FROM sys.columns c
|
|
51
52
|
JOIN sys.objects o ON o.object_id = c.object_id
|
|
52
53
|
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
53
54
|
JOIN sys.types t ON t.user_type_id = c.user_type_id
|
|
54
55
|
LEFT JOIN sys.default_constraints d ON d.object_id = c.default_object_id
|
|
56
|
+
LEFT JOIN sys.computed_columns cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
|
|
55
57
|
OUTER APPLY (
|
|
56
58
|
SELECT
|
|
57
59
|
MAX(CAST(i.is_primary_key AS INT)) as is_primary_key,
|
|
@@ -135,6 +137,7 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
135
137
|
length: widthOf(type, bytes),
|
|
136
138
|
precision: NUMERIC_TYPES.has(type) ? this.toNumber(row.numeric_precision) : undefined,
|
|
137
139
|
scale: NUMERIC_TYPES.has(type) ? this.toNumber(row.numeric_scale) : undefined,
|
|
140
|
+
generatedAs: row.generated_as ?? undefined,
|
|
138
141
|
};
|
|
139
142
|
});
|
|
140
143
|
}
|
|
@@ -40,6 +40,7 @@ type MysqlColumnRow = {
|
|
|
40
40
|
numeric_precision: number | bigint | null;
|
|
41
41
|
numeric_scale: number | null;
|
|
42
42
|
column_comment: string | null;
|
|
43
|
+
generated_as: string | null;
|
|
43
44
|
};
|
|
44
45
|
/**
|
|
45
46
|
* MariaDB reads out of the same `information_schema` as MySQL, save for one column type it does not
|
|
@@ -40,6 +40,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
40
40
|
NUMERIC_SCALE as numeric_scale,
|
|
41
41
|
COLUMN_KEY as column_key,
|
|
42
42
|
EXTRA as extra,
|
|
43
|
+
CASE WHEN EXTRA LIKE '%STORED GENERATED%' THEN GENERATION_EXPRESSION END as generated_as,
|
|
43
44
|
COLUMN_COMMENT as column_comment
|
|
44
45
|
FROM information_schema.COLUMNS
|
|
45
46
|
WHERE TABLE_SCHEMA = ${this.schemaExpr}
|
|
@@ -104,6 +105,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
104
105
|
precision: this.toNumber(row.numeric_precision),
|
|
105
106
|
scale: this.toNumber(row.numeric_scale),
|
|
106
107
|
comment: row.column_comment || undefined,
|
|
108
|
+
generatedAs: row.generated_as ?? undefined,
|
|
107
109
|
}));
|
|
108
110
|
}
|
|
109
111
|
async mapIndexesResult(_read, _tableName, results) {
|
|
@@ -19,6 +19,9 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
|
|
|
19
19
|
* the live catalogue while `information_schema` answers from this statement's snapshot, so a table
|
|
20
20
|
* another connection has just dropped is still listed here and the cast would raise on it. Whole
|
|
21
21
|
* database scans meet that table every time something else is migrating.
|
|
22
|
+
*
|
|
23
|
+
* `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
|
|
24
|
+
* virtual one Postgres 18 added and uql never declares. CockroachDB states it too.
|
|
22
25
|
*/
|
|
23
26
|
protected getColumnsQuery(_tableName: string): string;
|
|
24
27
|
/**
|
|
@@ -116,5 +119,6 @@ type PostgresColumnRow = {
|
|
|
116
119
|
numeric_precision: number | null;
|
|
117
120
|
numeric_scale: number | null;
|
|
118
121
|
column_comment: string | null;
|
|
122
|
+
generated_as: string | null;
|
|
119
123
|
};
|
|
120
124
|
export {};
|
|
@@ -42,6 +42,9 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
42
42
|
* the live catalogue while `information_schema` answers from this statement's snapshot, so a table
|
|
43
43
|
* another connection has just dropped is still listed here and the cast would raise on it. Whole
|
|
44
44
|
* database scans meet that table every time something else is migrating.
|
|
45
|
+
*
|
|
46
|
+
* `attgenerated` rather than `is_generated`, which cannot part a stored generated column from the
|
|
47
|
+
* virtual one Postgres 18 added and uql never declares. CockroachDB states it too.
|
|
45
48
|
*/
|
|
46
49
|
getColumnsQuery(_tableName) {
|
|
47
50
|
return /*sql*/ `
|
|
@@ -56,6 +59,11 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
56
59
|
c.numeric_scale,
|
|
57
60
|
c.is_identity,
|
|
58
61
|
c.identity_generation,
|
|
62
|
+
CASE WHEN (
|
|
63
|
+
SELECT a.attgenerated FROM pg_catalog.pg_attribute a
|
|
64
|
+
WHERE a.attrelid = to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
|
|
65
|
+
AND a.attname = c.column_name
|
|
66
|
+
) = 's' THEN c.generation_expression END AS generated_as,
|
|
59
67
|
EXISTS (
|
|
60
68
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
61
69
|
JOIN information_schema.key_column_usage kcu USING (constraint_schema, constraint_name)
|
|
@@ -186,6 +194,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
186
194
|
precision: row.numeric_precision ?? undefined,
|
|
187
195
|
scale: row.numeric_scale ?? undefined,
|
|
188
196
|
comment: row.column_comment ?? undefined,
|
|
197
|
+
generatedAs: row.generated_as ?? undefined,
|
|
189
198
|
}));
|
|
190
199
|
}
|
|
191
200
|
async mapIndexesResult(_read, _tableName, results) {
|
|
@@ -35,11 +35,18 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
|
|
|
35
35
|
private getUniqueColumns;
|
|
36
36
|
/** libSQL's `libsql_vector_idx(col, 'metric=...')`, read back from the statement that created it. */
|
|
37
37
|
private getVectorIndex;
|
|
38
|
+
/** The statement that created the table, which is where SQLite keeps every expression it was given. */
|
|
39
|
+
private getTableDdl;
|
|
38
40
|
private getIndexColumns;
|
|
39
41
|
protected normalizeType(type: string): string;
|
|
40
42
|
protected extractLength(type: string): number | undefined;
|
|
41
43
|
protected parseDefaultValue(defaultValue: string | null): unknown;
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The expression a generated column is computed from, read out of the `CREATE TABLE` itself: no PRAGMA
|
|
47
|
+
* reports one, and SQLite keeps the statement's text exactly as it was given.
|
|
48
|
+
*/
|
|
49
|
+
export declare function generatedExpression(ddl: string, column: string): string | undefined;
|
|
43
50
|
type SqliteCountRow = {
|
|
44
51
|
count: number | bigint;
|
|
45
52
|
};
|
|
@@ -49,6 +56,8 @@ type SqliteColumnRow = {
|
|
|
49
56
|
notnull: number;
|
|
50
57
|
dflt_value: string | null;
|
|
51
58
|
pk: number;
|
|
59
|
+
/** `PRAGMA table_xinfo`'s flag: 0 ordinary, 1 a hidden `VIRTUAL` table column, 2 virtual, 3 stored. Absent from `table_info`. */
|
|
60
|
+
hidden?: number;
|
|
52
61
|
};
|
|
53
62
|
type SqliteIndexRow = {
|
|
54
63
|
seq: number;
|
|
@@ -66,6 +66,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
66
66
|
const uniqueColumns = await this.getUniqueColumns(read, tableName);
|
|
67
67
|
// Only a sole `INTEGER PRIMARY KEY` is the rowid, which is what numbers itself.
|
|
68
68
|
const soleKey = results.filter((row) => row.pk > 0).length === 1;
|
|
69
|
+
const ddl = results.some((row) => row.hidden === STORED_GENERATED) ? await this.getTableDdl(read, tableName) : '';
|
|
69
70
|
return results.map((row) => ({
|
|
70
71
|
name: row.name,
|
|
71
72
|
type: this.normalizeType(row.type),
|
|
@@ -78,6 +79,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
78
79
|
precision: undefined,
|
|
79
80
|
scale: undefined,
|
|
80
81
|
comment: undefined, // SQLite doesn't support column comments
|
|
82
|
+
generatedAs: row.hidden === STORED_GENERATED ? generatedExpression(ddl, row.name) : undefined,
|
|
81
83
|
}));
|
|
82
84
|
}
|
|
83
85
|
async mapIndexesResult(read, _tableName, results) {
|
|
@@ -169,6 +171,12 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
169
171
|
const distances = new Map([...this.dialect.vectorMetrics].map(([distance, { index }]) => [index, distance]));
|
|
170
172
|
return { name: indexName, entries: [{ column }], unique: false, type: 'vector', distance: distances.get(metric) };
|
|
171
173
|
}
|
|
174
|
+
/** The statement that created the table, which is where SQLite keeps every expression it was given. */
|
|
175
|
+
async getTableDdl(read, tableName) {
|
|
176
|
+
const [row] = await read(
|
|
177
|
+
/*sql*/ `SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`, [tableName]);
|
|
178
|
+
return row.sql;
|
|
179
|
+
}
|
|
172
180
|
getIndexColumns(read, indexName) {
|
|
173
181
|
return read(/*sql*/ `PRAGMA index_info(${this.escapeId(indexName)})`);
|
|
174
182
|
}
|
|
@@ -208,3 +216,72 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
208
216
|
return defaultValue;
|
|
209
217
|
}
|
|
210
218
|
}
|
|
219
|
+
/** `PRAGMA table_xinfo`'s `hidden` for a column the engine stores rather than recomputes on each read. */
|
|
220
|
+
const STORED_GENERATED = 3;
|
|
221
|
+
const GENERATED_AS = /\b(?:GENERATED\s+ALWAYS\s+)?AS\s*\(/i;
|
|
222
|
+
/**
|
|
223
|
+
* The expression a generated column is computed from, read out of the `CREATE TABLE` itself: no PRAGMA
|
|
224
|
+
* reports one, and SQLite keeps the statement's text exactly as it was given.
|
|
225
|
+
*/
|
|
226
|
+
export function generatedExpression(ddl, column) {
|
|
227
|
+
const entry = tableEntries(ddl).find((it) => leadingIdentifier(it) === column);
|
|
228
|
+
if (entry === undefined) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const at = GENERATED_AS.exec(entry);
|
|
232
|
+
return at === null ? undefined : parenthesized(entry.slice(at.index + at[0].length - 1));
|
|
233
|
+
}
|
|
234
|
+
/** A `CREATE TABLE` body split at each comma outside any parentheses or quotes: one entry per column or constraint. */
|
|
235
|
+
function tableEntries(ddl) {
|
|
236
|
+
const body = ddl.slice(ddl.indexOf('(') + 1, ddl.lastIndexOf(')'));
|
|
237
|
+
const entries = [];
|
|
238
|
+
let start = 0;
|
|
239
|
+
scan(body, (char, index, depth) => {
|
|
240
|
+
if (char === ',' && depth === 0) {
|
|
241
|
+
entries.push(body.slice(start, index));
|
|
242
|
+
start = index + 1;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return [...entries, body.slice(start)];
|
|
246
|
+
}
|
|
247
|
+
/** What a leading `(` encloses, its own nesting and quoting respected. */
|
|
248
|
+
function parenthesized(text) {
|
|
249
|
+
let end = text.length;
|
|
250
|
+
scan(text, (char, index, depth) => {
|
|
251
|
+
const closes = char === ')' && depth === 0;
|
|
252
|
+
if (closes) {
|
|
253
|
+
end = index;
|
|
254
|
+
}
|
|
255
|
+
return closes;
|
|
256
|
+
});
|
|
257
|
+
return text.slice(1, end).trim();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Walk SQL, reporting each character outside a string or a quoted identifier along with the nesting
|
|
261
|
+
* depth that follows it. A truthy `visit` stops the walk.
|
|
262
|
+
*/
|
|
263
|
+
function scan(sql, visit) {
|
|
264
|
+
let depth = 0;
|
|
265
|
+
let quote = '';
|
|
266
|
+
for (let index = 0; index < sql.length; index++) {
|
|
267
|
+
const char = sql[index];
|
|
268
|
+
if (quote !== '') {
|
|
269
|
+
quote = char === quote ? '' : quote;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
273
|
+
quote = char;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
depth += char === '(' || char === '[' ? 1 : 0;
|
|
277
|
+
depth -= char === ')' || char === ']' ? 1 : 0;
|
|
278
|
+
if (visit(char, index, depth)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** The name a column definition opens with, however it was quoted. */
|
|
284
|
+
function leadingIdentifier(entry) {
|
|
285
|
+
const [token = ''] = /^\s*(?:"[^"]*"|`[^`]*`|\[[^\]]*\]|[^\s(]+)/.exec(entry) ?? [];
|
|
286
|
+
return token.trim().replace(/^["`[]|["`\]]$/g, '');
|
|
287
|
+
}
|
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.78.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|