uql-orm 0.68.1 → 0.70.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.
Files changed (56) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/type/clientQuerier.d.ts +5 -5
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +6 -6
  5. package/dist/cockroachdb/cockroachDialect.js +2 -2
  6. package/dist/dialect/abstractDialect.d.ts +8 -2
  7. package/dist/dialect/abstractDialect.js +17 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +42 -4
  9. package/dist/dialect/abstractSqlDialect.js +164 -48
  10. package/dist/dialect/aliases.d.ts +15 -4
  11. package/dist/dialect/aliases.js +15 -4
  12. package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
  13. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  14. package/dist/dialect/queryJoins.d.ts +8 -1
  15. package/dist/dialect/queryJoins.js +33 -10
  16. package/dist/entity/decorator/members.d.ts +44 -2
  17. package/dist/entity/decorator/members.js +0 -5
  18. package/dist/entity/metadata/definition.d.ts +8 -3
  19. package/dist/entity/metadata/definition.js +10 -3
  20. package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
  21. package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
  22. package/dist/migrate/introspection/postgresIntrospector.js +7 -1
  23. package/dist/migrate/migrator.d.ts +2 -1
  24. package/dist/migrate/migrator.js +5 -3
  25. package/dist/mongo/mongoDialect.d.ts +49 -19
  26. package/dist/mongo/mongoDialect.js +238 -81
  27. package/dist/mongo/mongodbQuerier.d.ts +2 -4
  28. package/dist/mongo/mongodbQuerier.js +16 -14
  29. package/dist/mssql/mssqlDialect.js +3 -2
  30. package/dist/postgres/postgresDialect.js +2 -2
  31. package/dist/querier/abstractQuerier.d.ts +16 -11
  32. package/dist/querier/abstractQuerier.js +28 -8
  33. package/dist/querier/abstractQuerierPool.d.ts +9 -9
  34. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  35. package/dist/querier/abstractSqlQuerier.js +3 -3
  36. package/dist/sqlite/sqliteDialect.js +1 -0
  37. package/dist/turso/tursoDialect.d.ts +1 -1
  38. package/dist/turso/tursoDialect.js +6 -2
  39. package/dist/type/dialect.d.ts +35 -2
  40. package/dist/type/entity.d.ts +120 -4
  41. package/dist/type/migration.d.ts +7 -0
  42. package/dist/type/query.d.ts +7 -10
  43. package/dist/type/queryAggregate.d.ts +77 -42
  44. package/dist/type/queryAggregate.js +4 -21
  45. package/dist/type/queryRaw.d.ts +19 -1
  46. package/dist/type/queryRaw.js +18 -0
  47. package/dist/type/universalQuerier.d.ts +9 -9
  48. package/dist/util/dialect.util.d.ts +6 -2
  49. package/dist/util/dialect.util.js +21 -7
  50. package/dist/util/field.util.d.ts +15 -1
  51. package/dist/util/field.util.js +18 -1
  52. package/dist/util/object.util.d.ts +1 -4
  53. package/dist/util/object.util.js +0 -3
  54. package/dist/util/raw.d.ts +2 -2
  55. package/dist/util/raw.js +29 -2
  56. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { SOFT_DELETE_FILTER } from '../../type/index.js';
1
+ import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
2
2
  import { isInlinedExpression } from '../../util/field.util.js';
3
3
  import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, memberRefs, normalizeIndexColumn, definedEntries, } from '../../util/index.js';
4
4
  import { ownRegistrations } from '../decorator/bag.js';
@@ -17,6 +17,14 @@ function globalMap(key) {
17
17
  const metas = globalMap('uql-orm/entity/metadata/v1');
18
18
  export function defineField(entity, key, opts = {}) {
19
19
  const meta = ensureWritableMeta(entity);
20
+ const { computed, ...rest } = opts;
21
+ const sql = computed === undefined ? undefined : entitySql(computed);
22
+ // A relation aggregate reads as a correlated subquery, which no engine accepts in a generated column:
23
+ // keeping one on the row takes the triggers a write fires, which are not built yet.
24
+ if (opts.stored && sql instanceof RelationAggregate) {
25
+ throw new TypeError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
26
+ "engine keeps in a generated column. Drop 'stored' to have it read on each query.");
27
+ }
20
28
  // A stored computed column is a real column and still needs a type; only an inlined one is exempt,
21
29
  // its expression being spliced in rather than declared.
22
30
  if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
@@ -31,13 +39,12 @@ export function defineField(entity, key, opts = {}) {
31
39
  // Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
32
40
  // column from the referenced primary key (picking up its `columnType`, length and chained keys)
33
41
  // instead of treating whatever ends up in `type` as deliberate.
34
- const { computed, ...rest } = opts;
35
42
  const resolved = rest.type ? rest : { ...rest, typeFromReference: true };
36
43
  meta.fields[fieldKey] = {
37
44
  ...meta.fields[fieldKey],
38
45
  name: key,
39
46
  ...resolved,
40
- ...(computed && { computed: entitySql(computed) }),
47
+ ...(sql && { computed: sql }),
41
48
  };
42
49
  return meta;
43
50
  }
@@ -156,9 +156,11 @@ export class EntityCodeGenerator {
156
156
  const fieldOptions = this.buildFieldOptions(col, propertyName);
157
157
  lines.push(` @Field(${fieldOptions})`);
158
158
  }
159
- // Property
159
+ // Property. A generated column is the database's to write, so it is `readonly`: a write payload
160
+ // leaves those out, and one naming it would be dropped rather than persisted.
160
161
  const nullable = col.nullable ? '?' : '';
161
- lines.push(` ${propertyName}${nullable}: ${tsType};`);
162
+ const written = col.generatedAs === undefined ? '' : 'readonly ';
163
+ lines.push(` ${written}${propertyName}${nullable}: ${tsType};`);
162
164
  return lines.join('\n');
163
165
  }
164
166
  /**
@@ -14,6 +14,12 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
14
14
  protected getTableNamesQuery(): string;
15
15
  protected tableExistsQuery(): string;
16
16
  protected parseTableExistsResult([row]: RawRow[]): boolean;
17
+ /**
18
+ * The comment reads through `to_regclass` rather than a `::regclass` cast: a name resolves against
19
+ * the live catalogue while `information_schema` answers from this statement's snapshot, so a table
20
+ * another connection has just dropped is still listed here and the cast would raise on it. Whole
21
+ * database scans meet that table every time something else is migrating.
22
+ */
17
23
  protected getColumnsQuery(_tableName: string): string;
18
24
  /**
19
25
  * `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
@@ -37,6 +37,12 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
37
37
  parseTableExistsResult([row]) {
38
38
  return row['exists'] === true;
39
39
  }
40
+ /**
41
+ * The comment reads through `to_regclass` rather than a `::regclass` cast: a name resolves against
42
+ * the live catalogue while `information_schema` answers from this statement's snapshot, so a table
43
+ * another connection has just dropped is still listed here and the cast would raise on it. Whole
44
+ * database scans meet that table every time something else is migrating.
45
+ */
40
46
  getColumnsQuery(_tableName) {
41
47
  return /*sql*/ `
42
48
  SELECT
@@ -68,7 +74,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
68
74
  HAVING COUNT(*) = 1 AND MIN(kcu.column_name) = c.column_name
69
75
  ) AS is_unique,
70
76
  pg_catalog.col_description(
71
- (quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass,
77
+ to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name)),
72
78
  c.ordinal_position
73
79
  ) AS column_comment
74
80
  FROM information_schema.columns c
@@ -49,7 +49,8 @@ export declare class Migrator {
49
49
  /** Runs the list narrowed by `to`/`step`, stopping at the first failure: `up` over the pending, `down` over the executed reversed. */
50
50
  private runInOrder;
51
51
  /**
52
- * Run a single migration, within a transaction where the dialect has one for it
52
+ * Run a single migration, in a transaction where the dialect has one for it and the migration has not
53
+ * declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
53
54
  */
54
55
  runMigration(migration: Migration<Querier>, direction: 'up' | 'down'): Promise<MigrationResult>;
55
56
  /**
@@ -112,14 +112,15 @@ export class Migrator {
112
112
  return results;
113
113
  }
114
114
  /**
115
- * Run a single migration, within a transaction where the dialect has one for it
115
+ * Run a single migration, in a transaction where the dialect has one for it and the migration has not
116
+ * declared `transaction: false` - the opt-out a statement an engine refuses inside one needs.
116
117
  */
117
118
  async runMigration(migration, direction) {
118
119
  const startTime = Date.now();
119
120
  return this.target.withSession(async ({ querier, transaction }) => {
120
121
  try {
121
122
  this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
122
- await transaction(async () => {
123
+ const work = async () => {
123
124
  if (direction === 'up') {
124
125
  await migration.up(querier);
125
126
  await this.storage.logWithQuerier(querier, migration.name);
@@ -128,7 +129,8 @@ export class Migrator {
128
129
  await migration.down(querier);
129
130
  await this.storage.unlogWithQuerier(querier, migration.name);
130
131
  }
131
- });
132
+ };
133
+ await (migration.transaction === false ? work() : transaction(work));
132
134
  const duration = Date.now() - startTime;
133
135
  this.logger.logMigration(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
134
136
  return {
@@ -33,20 +33,15 @@ export declare class MongoDialect extends AbstractDialect {
33
33
  columnOf<E>(meta: EntityMeta<E>, key: string): string;
34
34
  where<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions): Filter<E>;
35
35
  /**
36
- * A `$where` that may constrain relations, as the `$lookup` stages it needs and the `$match` reading
37
- * them: each condition a lookup into a temporary field (`unset` names them), so `$or` keeps its meaning.
36
+ * The stages every pipeline starts with: the `$lookup` each relation condition needs, into a temporary
37
+ * field so `$or` keeps its meaning, the `$match` reading them, and the temporaries taken back out. Each
38
+ * relation aggregate the `$where` or `named` reads is left on the document, once, under its column.
38
39
  */
39
- whereWithRelations<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions): {
40
- readonly stages: MongoAggregationPipelineEntry<Document>[];
41
- readonly filter: Filter<E>;
42
- readonly unset: string[];
43
- };
44
- /** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
45
- constrainsRelations<E extends Document>(entity: Type<E>, where: QueryWhere<E> | undefined): boolean;
40
+ matchStages<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, opts?: QueryOptions, named?: readonly string[]): MongoAggregationPipelineEntry<Document>[];
46
41
  /**
47
42
  * Renders a `$where` tree without applying entity filters (used for same-scope group-operator
48
- * recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
49
- * given - a plain `find`/`updateMany` filter has nowhere to put them.
43
+ * recursion). A relation, or a relation aggregate, needs `$lookup` stages, so it is only accepted
44
+ * when `lookups` is given - a plain `find`/`updateMany` filter has nowhere to put them.
50
45
  */
51
46
  protected renderFilter<E extends Document>(entity: Type<E>, where?: QueryWhere<E>, lookups?: RelationLookups): Filter<E>;
52
47
  /**
@@ -139,9 +134,29 @@ export declare class MongoDialect extends AbstractDialect {
139
134
  readonly stages: MongoAggregationPipelineEntry<Document>[];
140
135
  readonly fields: string[];
141
136
  };
142
- /** The correlated lookup counting a relation's rows, which `where` narrows, into `temp`. */
143
- private tallyLookup;
144
- /** The tally a lookup left in `temp`, which holds no row at all where nothing matched: a zero. */
137
+ /** Whether a read answers with a relation aggregate, which only the pipeline can build. */
138
+ readsAggregates<E extends Document>(entity: Type<E>, q: Query<E>): boolean;
139
+ /** The relation aggregates a read projects or sorts by; its `$where` puts its own on the document. */
140
+ private aggregateKeys;
141
+ /**
142
+ * The relation aggregate `key` computes, put on the document under its column by the stages
143
+ * {@link aggregateStages} builds from the same spec SQL renders as a subquery. Once however many
144
+ * clauses read it; a key computing none adds nothing.
145
+ */
146
+ private appendAggregateField;
147
+ /**
148
+ * One relation aggregate on the document under `field`: the correlated lookup that reads the related
149
+ * rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
150
+ * the `$addFields` reading that back, `0` or `null` where the lookup matched nothing.
151
+ *
152
+ * Every aggregate MongoDB answers is built here: a `$count` a query asks for, an ordering by one, and
153
+ * a field a `computed` declares, which is the same spec the SQL dialects render as one subquery.
154
+ */
155
+ private aggregateStages;
156
+ /**
157
+ * The value a lookup left in `temp`, which holds no row at all where nothing matched: `0` for the
158
+ * aggregates that count something, and `null` for the ones with no value to report.
159
+ */
145
160
  private tally;
146
161
  /**
147
162
  * The lookups reading each to-many a query populates, and the tally of each `$count`, onto the fields
@@ -232,13 +247,28 @@ export declare class MongoDialect extends AbstractDialect {
232
247
  /**
233
248
  * Build MongoDB aggregation pipeline stages from a QueryAggregate.
234
249
  */
235
- buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Record<string, unknown>[];
250
+ buildAggregateStages<E extends Document, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): MongoAggregationPipelineEntry<Document>[];
236
251
  /**
237
- * Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage.
238
- * `distinctReducers` maps each DISTINCT alias (collected via `$addToSet`) to the `$project`
239
- * operator that reduces its set: `$size` for `$count`, `$sum`/`$avg` for the numeric ops.
252
+ * Resolve parsed group entries into the `_id` keys and accumulators of a `$group` stage, and the
253
+ * `columns` its `$project` reads each result from: a group key out of `_id`, a DISTINCT set by its
254
+ * size, and a `$sum` as null where it read no value, as SQL answers it. `named` is every field it reads,
255
+ * filters included, so each relation aggregate among them is put on the document first.
240
256
  */
241
257
  private buildGroupSpec;
258
+ /** `1` where `ref` holds a value, `0` where it is null or missing, which an expression tells apart. */
259
+ private static countOf;
260
+ /** Whether `ref` is null or missing: an expression compares a missing field as neither. */
261
+ private static isNullExpr;
262
+ /** The field a grouped path reads, on the document or on the joined one its lookup unwound. */
263
+ private groupedPath;
264
+ /**
265
+ * An aggregate's own `$where` as the expression a `$cond` tests, which a query filter is not: the
266
+ * comparisons and the logical operators translate, anything else is refused by name. `named` gathers
267
+ * the fields it reads, so a relation aggregate among them is on the document first.
268
+ */
269
+ private whereExpression;
270
+ /** One field's condition as an expression: a value it equals, a list it is in, or a map of comparisons. */
271
+ private fieldExpression;
242
272
  private buildHavingFilter;
243
273
  /**
244
274
  * Separate vector sort entries from regular sort entries.
@@ -249,7 +279,7 @@ export declare class MongoDialect extends AbstractDialect {
249
279
  * Build a `$vectorSearch` aggregation pipeline stage.
250
280
  * Merges `$where` into `$vectorSearch.filter` for optimal pre-filtering.
251
281
  */
252
- buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number): Record<string, unknown>;
282
+ buildVectorSearchStage<E extends Document>(entity: Type<E>, key: string, search: QueryVectorSearch, where: QueryWhere<E> | undefined, limit: number, opts?: QueryOptions, candidates?: number): MongoAggregationPipelineEntry<Document>;
253
283
  }
254
284
  export type MongoAggregationPipelineEntry<E extends Document> = {
255
285
  $lookup?: MongoAggregationLookup;