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 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 is how other people find the project.
76
+ It will help other people find the project.
77
77
 
78
78
  [![Star UQL on GitHub](https://img.shields.io/github/stars/rogerpadilla/uql?style=flat&label=stars&color=3282b5)](https://github.com/rogerpadilla/uql)
@@ -49,5 +49,6 @@ type MsSqlColumnRow = {
49
49
  column_default: string | null;
50
50
  is_primary_key: number | null;
51
51
  is_unique: number | null;
52
+ generated_as: string | null;
52
53
  };
53
54
  export {};
@@ -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.77.1",
6
+ "version": "0.78.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"