uql-orm 0.24.5 → 0.24.7

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 (62) hide show
  1. package/README.md +9 -8
  2. package/dist/cockroachdb/crdbQuerierPool.d.ts +1 -1
  3. package/dist/cockroachdb/crdbQuerierPool.js +5 -4
  4. package/dist/dialect/abstractSqlDialect.d.ts +78 -28
  5. package/dist/dialect/abstractSqlDialect.js +196 -147
  6. package/dist/dialect/hydrateColumn.d.ts +16 -0
  7. package/dist/dialect/hydrateColumn.js +66 -0
  8. package/dist/dialect/jsonSql.d.ts +24 -0
  9. package/dist/dialect/jsonSql.js +39 -0
  10. package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -0
  11. package/dist/dialect/mysqlLikeSqlDialect.js +10 -1
  12. package/dist/dialect/pgLikeSqlDialect.d.ts +3 -7
  13. package/dist/dialect/pgLikeSqlDialect.js +2 -14
  14. package/dist/dialect/vectorCast.d.ts +15 -0
  15. package/dist/dialect/vectorCast.js +58 -0
  16. package/dist/entity/metadata/definition.d.ts +0 -1
  17. package/dist/entity/metadata/definition.js +1 -1
  18. package/dist/maria/mariaDialect.d.ts +3 -2
  19. package/dist/maria/mariaDialect.js +3 -18
  20. package/dist/maria/mariadbQuerierPool.js +6 -1
  21. package/dist/migrate/builder/migrationBuilder.d.ts +12 -16
  22. package/dist/migrate/builder/migrationBuilder.js +24 -59
  23. package/dist/migrate/builder/tableBuilder.js +0 -12
  24. package/dist/migrate/cli.d.ts +0 -1
  25. package/dist/migrate/cli.js +1 -1
  26. package/dist/migrate/codegen/entityCodeGenerator.js +0 -3
  27. package/dist/migrate/drift/driftDetector.js +17 -15
  28. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +8 -2
  29. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +10 -9
  30. package/dist/migrate/introspection/mysqlIntrospector.d.ts +0 -3
  31. package/dist/migrate/introspection/mysqlIntrospector.js +0 -9
  32. package/dist/migrate/introspection/postgresIntrospector.d.ts +0 -3
  33. package/dist/migrate/introspection/postgresIntrospector.js +0 -12
  34. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  35. package/dist/migrate/introspection/sqliteIntrospector.js +1 -9
  36. package/dist/migrate/migrator.d.ts +8 -0
  37. package/dist/migrate/migrator.js +19 -29
  38. package/dist/migrate/schemaGenerator.js +0 -12
  39. package/dist/mongo/mongoDialect.d.ts +7 -0
  40. package/dist/mongo/mongoDialect.js +37 -1
  41. package/dist/mongo/mongodbQuerier.js +8 -3
  42. package/dist/neon/neonQuerierPool.d.ts +1 -1
  43. package/dist/neon/neonQuerierPool.js +6 -4
  44. package/dist/postgres/abstractPgQuerierPool.d.ts +6 -0
  45. package/dist/postgres/abstractPgQuerierPool.js +3 -0
  46. package/dist/postgres/pgNumericTypes.d.ts +41 -0
  47. package/dist/postgres/pgNumericTypes.js +35 -0
  48. package/dist/postgres/pgQuerierPool.d.ts +1 -1
  49. package/dist/postgres/pgQuerierPool.js +5 -4
  50. package/dist/querier/abstractQuerier.d.ts +0 -3
  51. package/dist/querier/abstractQuerier.js +10 -9
  52. package/dist/querier/abstractSqlQuerier.d.ts +9 -2
  53. package/dist/querier/abstractSqlQuerier.js +31 -24
  54. package/dist/schema/canonicalType.js +2 -12
  55. package/dist/schema/schemaAST.js +0 -24
  56. package/dist/schema/schemaASTBuilder.js +0 -3
  57. package/dist/type/query.d.ts +2 -0
  58. package/dist/type/queryAggregate.d.ts +3 -0
  59. package/dist/util/field.util.d.ts +4 -0
  60. package/dist/util/field.util.js +12 -0
  61. package/dist/util/sqlLiteral.js +18 -15
  62. package/package.json +5 -5
@@ -4,9 +4,6 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
4
4
  * Works with both MySQL and MariaDB as they share the same information_schema structure.
5
5
  */
6
6
  export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
7
- // ============================================================================
8
- // SQL Queries (dialect-specific)
9
- // ============================================================================
10
7
  getTableNamesQuery() {
11
8
  return /*sql*/ `
12
9
  SELECT TABLE_NAME as table_name
@@ -95,12 +92,6 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
95
92
  ORDER BY ORDINAL_POSITION
96
93
  `;
97
94
  }
98
- // ============================================================================
99
- // Internal Types
100
- // ============================================================================
101
- mapTableNameRow(row) {
102
- return row.table_name;
103
- }
104
95
  async mapColumnsResult(_read, _tableName, results) {
105
96
  return results.map((row) => ({
106
97
  name: row.column_name,
@@ -11,9 +11,6 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
11
11
  protected getIndexesQuery(_tableName: string): string;
12
12
  protected getForeignKeysQuery(_tableName: string): string;
13
13
  protected getPrimaryKeyQuery(_tableName: string): string;
14
- protected mapTableNameRow(row: {
15
- table_name: string;
16
- }): string;
17
14
  protected mapColumnsResult(_read: TableRowReader, _tableName: string, results: PostgresColumnRow[]): Promise<ColumnSchema[]>;
18
15
  protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: {
19
16
  index_name: string;
@@ -3,9 +3,6 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
3
3
  * PostgreSQL schema introspector
4
4
  */
5
5
  export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
6
- // ============================================================================
7
- // SQL Queries (dialect-specific)
8
- // ============================================================================
9
6
  getTableNamesQuery() {
10
7
  return /*sql*/ `
11
8
  SELECT table_name
@@ -128,12 +125,6 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
128
125
  ORDER BY kcu.ordinal_position
129
126
  `;
130
127
  }
131
- // ============================================================================
132
- // Internal Types
133
- // ============================================================================
134
- mapTableNameRow(row) {
135
- return row.table_name;
136
- }
137
128
  async mapColumnsResult(_read, _tableName, results) {
138
129
  return results.map((row) => ({
139
130
  name: row.column_name,
@@ -166,9 +157,6 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
166
157
  onUpdate: this.normalizeReferentialAction(row.update_rule),
167
158
  }));
168
159
  }
169
- // ============================================================================
170
- // PostgreSQL-specific helpers
171
- // ============================================================================
172
160
  normalizeType(dataType, udtName) {
173
161
  // Handle user-defined types and arrays
174
162
  if (dataType === 'USER-DEFINED') {
@@ -15,6 +15,7 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
15
15
  protected getIndexesParams(_tableName: string): unknown[];
16
16
  protected getForeignKeysParams(_tableName: string): unknown[];
17
17
  protected getPrimaryKeyParams(_tableName: string): unknown[];
18
+ /** `sqlite_master`, not `information_schema`, so the column is `name`. */
18
19
  protected mapTableNameRow(row: {
19
20
  name: string;
20
21
  }): string;
@@ -3,9 +3,6 @@ import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.j
3
3
  * SQLite schema introspector
4
4
  */
5
5
  export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
6
- // ============================================================================
7
- // SQL Queries (dialect-specific)
8
- // ============================================================================
9
6
  getTableNamesQuery() {
10
7
  return /*sql*/ `
11
8
  SELECT name
@@ -55,9 +52,7 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
55
52
  getPrimaryKeyParams(_tableName) {
56
53
  return [];
57
54
  }
58
- // ============================================================================
59
- // Row Mapping (dialect-specific)
60
- // ============================================================================
55
+ /** `sqlite_master`, not `information_schema`, so the column is `name`. */
61
56
  mapTableNameRow(row) {
62
57
  return row.name;
63
58
  }
@@ -128,9 +123,6 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
128
123
  }
129
124
  return pkColumns.map((r) => r.name);
130
125
  }
131
- // ============================================================================
132
- // SQLite-specific helpers
133
- // ============================================================================
134
126
  async getUniqueColumns(read, tableName) {
135
127
  const indexes = await read(this.getIndexesQuery(tableName));
136
128
  const uniqueColumns = new Set();
@@ -53,6 +53,14 @@ export declare class Migrator {
53
53
  to?: string;
54
54
  step?: number;
55
55
  }): Promise<MigrationResult[]>;
56
+ /**
57
+ * Narrow a run list by `to`/`step` and execute it, stopping at the first failure.
58
+ *
59
+ * Both directions do exactly this and differ only in the list they start from: `up` takes the
60
+ * pending migrations, `down` the executed ones reversed. Keeping the selection in one place is what
61
+ * makes `--to` and `--step` mean the same thing whichever way you are going.
62
+ */
63
+ private runInOrder;
56
64
  /**
57
65
  * Run a single migration within a transaction
58
66
  */
@@ -130,27 +130,7 @@ export class Migrator {
130
130
  * Run all pending migrations
131
131
  */
132
132
  async up(options = {}) {
133
- const pendingMigrations = await this.pending();
134
- const results = [];
135
- let migrationsToRun = pendingMigrations;
136
- if (options.to) {
137
- const toIndex = migrationsToRun.findIndex((m) => m.name === options.to);
138
- if (toIndex === -1) {
139
- throw new Error(`Migration '${options.to}' not found`);
140
- }
141
- migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
142
- }
143
- if (options.step !== undefined) {
144
- migrationsToRun = migrationsToRun.slice(0, options.step);
145
- }
146
- for (const migration of migrationsToRun) {
147
- const result = await this.runMigration(migration, 'up');
148
- results.push(result);
149
- if (!result.success) {
150
- break; // Stop on first failure
151
- }
152
- }
153
- return results;
133
+ return this.runInOrder(await this.pending(), 'up', options);
154
134
  }
155
135
  /**
156
136
  * Rollback migrations
@@ -159,23 +139,33 @@ export class Migrator {
159
139
  const [migrations, executed] = await Promise.all([this.getMigrations(), this.storage.executed()]);
160
140
  const executedSet = new Set(executed);
161
141
  const executedMigrations = migrations.filter((m) => executedSet.has(m.name)).reverse(); // Rollback in reverse order
162
- const results = [];
163
- let migrationsToRun = executedMigrations;
142
+ return this.runInOrder(executedMigrations, 'down', options);
143
+ }
144
+ /**
145
+ * Narrow a run list by `to`/`step` and execute it, stopping at the first failure.
146
+ *
147
+ * Both directions do exactly this and differ only in the list they start from: `up` takes the
148
+ * pending migrations, `down` the executed ones reversed. Keeping the selection in one place is what
149
+ * makes `--to` and `--step` mean the same thing whichever way you are going.
150
+ */
151
+ async runInOrder(migrations, direction, options) {
152
+ let selected = migrations;
164
153
  if (options.to) {
165
- const toIndex = migrationsToRun.findIndex((m) => m.name === options.to);
154
+ const toIndex = selected.findIndex((m) => m.name === options.to);
166
155
  if (toIndex === -1) {
167
156
  throw new Error(`Migration '${options.to}' not found`);
168
157
  }
169
- migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
158
+ selected = selected.slice(0, toIndex + 1);
170
159
  }
171
160
  if (options.step !== undefined) {
172
- migrationsToRun = migrationsToRun.slice(0, options.step);
161
+ selected = selected.slice(0, options.step);
173
162
  }
174
- for (const migration of migrationsToRun) {
175
- const result = await this.runMigration(migration, 'down');
163
+ const results = [];
164
+ for (const migration of selected) {
165
+ const result = await this.runMigration(migration, direction);
176
166
  results.push(result);
177
167
  if (!result.success) {
178
- break; // Stop on first failure
168
+ break;
179
169
  }
180
170
  }
181
171
  return results;
@@ -40,9 +40,6 @@ export class SqlSchemaGenerator {
40
40
  get serialPrimaryKeyType() {
41
41
  return this.dialect.serialPrimaryKey;
42
42
  }
43
- // ============================================================================
44
- // CanonicalType Integration (Unified Type System)
45
- // ============================================================================
46
43
  /**
47
44
  * Convert FieldOptions to CanonicalType using the unified type system.
48
45
  */
@@ -52,9 +49,6 @@ export class SqlSchemaGenerator {
52
49
  canonicalTypeToSql(type) {
53
50
  return canonicalToSql(type, this.dialect);
54
51
  }
55
- // ============================================================================
56
- // SchemaGenerator Implementation
57
- // ============================================================================
58
52
  generateCreateTable(entity, options = {}) {
59
53
  const builder = new SchemaASTBuilder(this.dialect.namingStrategy);
60
54
  const ast = builder.fromEntities([entity]);
@@ -390,9 +384,6 @@ export class SqlSchemaGenerator {
390
384
  };
391
385
  return normalize(current) === normalize(desired);
392
386
  }
393
- // ============================================================================
394
- // SchemaAST Support Methods
395
- // ============================================================================
396
387
  generateCreateTableFromNode(table, options = {}) {
397
388
  const columns = [];
398
389
  const constraints = [];
@@ -468,9 +459,6 @@ export class SqlSchemaGenerator {
468
459
  generateCreateIndexFromNode(index, options = { ifNotExists: false }) {
469
460
  return this.generateCreateIndex(index.table.name, indexNodeToSchema(index), options);
470
461
  }
471
- // ============================================================================
472
- // Phase 3: Builder Operation Methods (Moved forward for unification)
473
- // ============================================================================
474
462
  generateCreateTableFromDefinition(table, options = {}) {
475
463
  const tableNode = this.tableDefinitionToNode(table);
476
464
  return this.generateCreateTableFromNode(tableNode, options);
@@ -103,6 +103,13 @@ export declare class MongoDialect extends AbstractDialect {
103
103
  */
104
104
  private pathOf;
105
105
  aggregationPipeline<E extends Document>(entity: Type<E>, q: Query<E>, relationSummary?: RelationRequestSummary<E>, opts?: QueryOptions): MongoAggregationPipelineEntry<E>[];
106
+ /**
107
+ * The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
108
+ * the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
109
+ * lookups have read the join keys - projecting any earlier is what used to leave `$populate`
110
+ * empty, and is why the pipeline emitted no projection at all and returned every column.
111
+ */
112
+ pipelineProjection<E extends Document>(entity: Type<E>, q: Query<E>, relationSummary?: RelationRequestSummary<E>): Record<string, 0 | 1> | undefined;
106
113
  /**
107
114
  * `$lookup`/`$unwind` stages for the joinable relations a query populates. Shared by the plain
108
115
  * aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
@@ -414,8 +414,33 @@ export class MongoDialect extends AbstractDialect {
414
414
  // does after an INNER JOIN. Otherwise paging first is equivalent and spares the lookups.
415
415
  const dropsParents = relStages.some((stage) => stage.$unwind?.preserveNullAndEmptyArrays === false);
416
416
  pipeline.push(...(dropsParents ? [...relStages, ...pager] : [...pager, ...relStages]));
417
+ const projection = this.pipelineProjection(entity, q, relationSummary);
418
+ if (projection) {
419
+ pipeline.push({ $project: projection });
420
+ }
417
421
  return pipeline;
418
422
  }
423
+ /**
424
+ * The scalar projection a narrowing query asks for, widened by what the pipeline itself produced:
425
+ * the joined documents, and the `_id` a to-many fill groups children by. It goes last, after the
426
+ * lookups have read the join keys - projecting any earlier is what used to leave `$populate`
427
+ * empty, and is why the pipeline emitted no projection at all and returned every column.
428
+ */
429
+ pipelineProjection(entity, q, relationSummary) {
430
+ if (!q.$select && !q.$exclude) {
431
+ return undefined;
432
+ }
433
+ const projection = this.select(entity, q.$select, q.$exclude);
434
+ const summary = relationSummary ?? getRelationRequestSummary(getMeta(entity), q.$populate);
435
+ for (const relKey of summary.joinableKeys) {
436
+ projection[relKey] = 1;
437
+ }
438
+ // Only ever undoes an exclusion: a relation cannot be filled onto a parent with no key.
439
+ if (summary.requestedKeys.length && projection[MongoDialect.ID_KEY] === 0) {
440
+ delete projection[MongoDialect.ID_KEY];
441
+ }
442
+ return projection;
443
+ }
419
444
  /**
420
445
  * `$lookup`/`$unwind` stages for the joinable relations a query populates. Shared by the plain
421
446
  * aggregation pipeline and the `$vectorSearch` one, so relations load the same way in both.
@@ -439,11 +464,22 @@ export class MongoDialect extends AbstractDialect {
439
464
  // filters (in particular `security: true` ones) must apply even to a bare
440
465
  // `$populate: { rel: true }`, exactly like the SQL dialects' JOIN ON-clause filters.
441
466
  const relationFilter = this.where(relEntity, relQuery.$where ?? {}, opts);
467
+ // The relation's own projection runs inside the lookup, where its keys resolve against the
468
+ // related entity. Left out, `$populate: { rel: { $select } }` returned all of `rel`'s columns.
469
+ const relationProjection = this.pipelineProjection(relEntity, relQuery);
470
+ // MongoDB returns `_id` unless a projection subtracts it, so dropping the key from the map is
471
+ // how a joined document keeps its own id - as it does on the SQL dialects, and as a nested
472
+ // to-many fill needs.
473
+ delete relationProjection?.[MongoDialect.ID_KEY];
474
+ const lookupPipeline = [
475
+ ...(hasKeys(relationFilter) ? [{ $match: relationFilter }] : []),
476
+ ...(relationProjection ? [{ $project: relationProjection }] : []),
477
+ ];
442
478
  pipeline.push({
443
479
  $lookup: {
444
480
  from: this.resolveTableName(relEntity, relMeta),
445
481
  ...this.joinKeys(meta, relMeta, relOpts),
446
- ...(hasKeys(relationFilter) ? { pipeline: [{ $match: relationFilter }] } : {}),
482
+ ...(lookupPipeline.length ? { pipeline: lookupPipeline } : {}),
447
483
  as: relKey,
448
484
  },
449
485
  });
@@ -109,14 +109,19 @@ export class MongodbQuerier extends AbstractQuerier {
109
109
  const meta = getMeta(entity);
110
110
  const relationSummary = getRelationRequestSummary(meta, q.$populate);
111
111
  const scoreAlias = vectorSort.vectorSearch.$project;
112
- // With relations, the score is captured with `$addFields` before the lookups and no scalar
113
- // `$project` is emitted: projecting here would drop both the join keys and the joined documents,
114
- // which is why `$populate` used to come back empty under a vector sort.
112
+ // With relations the score is captured with `$addFields` before the lookups, and the scalar
113
+ // projection waits until after them: projecting any earlier drops the join keys and the joined
114
+ // documents, which is why `$populate` used to come back empty under a vector sort.
115
115
  if (relationSummary.requestedKeys.length) {
116
116
  if (scoreAlias) {
117
117
  pipeline.push({ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } });
118
118
  }
119
119
  pipeline.push(...this.dialect.relationStages(entity, q, relationSummary));
120
+ const projection = this.dialect.pipelineProjection(entity, q, relationSummary);
121
+ if (projection) {
122
+ // `$addFields` already made the score a real field, so it projects like any other.
123
+ pipeline.push({ $project: scoreAlias ? { ...projection, [scoreAlias]: 1 } : projection });
124
+ }
120
125
  }
121
126
  else if (scoreAlias) {
122
127
  const select = q.$select || q.$exclude ? this.buildScalarProjection(entity, q) : {};
@@ -6,5 +6,5 @@ import { NeonQuerier } from './neonQuerier.js';
6
6
  export declare class NeonQuerierPool extends AbstractPgQuerierPool<PoolClient, NeonQuerier, NeonDialect> {
7
7
  readonly pool: Pool;
8
8
  constructor(opts: PoolConfig, extra?: ExtraOptions);
9
- getQuerier(): Promise<NeonQuerier>;
9
+ protected buildQuerier(connect: () => Promise<PoolClient>): NeonQuerier;
10
10
  }
@@ -1,12 +1,14 @@
1
- import { Pool } from '@neondatabase/serverless';
1
+ import { Pool, types } from '@neondatabase/serverless';
2
2
  import { AbstractPgQuerierPool } from '../postgres/abstractPgQuerierPool.js';
3
+ import { numericTypes } from '../postgres/pgNumericTypes.js';
3
4
  import { NeonDialect } from './neonDialect.js';
4
5
  import { NeonQuerier } from './neonQuerier.js';
5
6
  export class NeonQuerierPool extends AbstractPgQuerierPool {
6
7
  constructor(opts, extra) {
7
- super(new NeonDialect({ namingStrategy: extra?.namingStrategy }), new Pool(opts), extra);
8
+ // Neon's own `types`, not `pg`'s: this entry has to load on an edge runtime where `pg` is absent.
9
+ super(new NeonDialect({ namingStrategy: extra?.namingStrategy }), new Pool({ types: numericTypes(types), ...opts }), extra);
8
10
  }
9
- async getQuerier() {
10
- return new NeonQuerier(() => this.pool.connect(), this.dialect, this.extra);
11
+ buildQuerier(connect) {
12
+ return new NeonQuerier(connect, this.dialect, this.extra);
11
13
  }
12
14
  }
@@ -17,5 +17,11 @@ export interface PgAnyPool<C extends PgAnyClient> extends ErrorEmittingPool {
17
17
  export declare abstract class AbstractPgQuerierPool<C extends PgAnyClient, Q extends AbstractPgQuerier<C, D>, D extends AbstractSqlDialect> extends AbstractSqlQuerierPool<Q, D> {
18
18
  readonly pool: PgAnyPool<C>;
19
19
  constructor(dialect: D, pool: PgAnyPool<C>, extra?: ExtraOptions);
20
+ /**
21
+ * Every pg-compatible pool acquires a client the same way, so only the querier class varies.
22
+ * Subclasses name that instead of restating the lazy `connect` the querier expects.
23
+ */
24
+ protected abstract buildQuerier(connect: () => Promise<C>): Q;
25
+ getQuerier(): Promise<Q>;
20
26
  end(): Promise<void>;
21
27
  }
@@ -14,6 +14,9 @@ export class AbstractPgQuerierPool extends AbstractSqlQuerierPool {
14
14
  this.pool = pool;
15
15
  attachPoolErrorHandler(pool, 'Idle Postgres pool client encountered an error');
16
16
  }
17
+ async getQuerier() {
18
+ return this.buildQuerier(() => this.pool.connect());
19
+ }
17
20
  async end() {
18
21
  await this.pool.end();
19
22
  }
@@ -0,0 +1,41 @@
1
+ import type { CustomTypesConfig } from 'pg';
2
+ /**
3
+ * The shape every pg-family driver exposes as `types`: `pg`'s own, and `@neondatabase/serverless`'s
4
+ * reimplementation of it. Taken as a parameter rather than imported, because `uql-orm/neon` must not
5
+ * pull `pg` into an edge bundle that has no such peer installed - the same reason
6
+ * `abstractPgQuerierPool.ts` keeps its `pg` imports type-only.
7
+ */
8
+ type PgTypes = {
9
+ readonly builtins: Readonly<Record<string, number>>;
10
+ getTypeParser(oid: number, format?: 'text' | 'binary'): (value: string) => unknown;
11
+ };
12
+ /**
13
+ * Decode `INT8` and `FLOAT8` as JS numbers, leaving every other type to the driver.
14
+ *
15
+ * uql owes this to the caller because uql picks the column: `type: Number` maps to BIGINT (see
16
+ * `schema/canonicalType.ts`), so without it a field declared `number` read back as `'9'` - including
17
+ * every auto-increment primary key, on every entity. `FLOAT8` is a float64, which is exactly what a
18
+ * JS number is, so decoding it loses nothing at all.
19
+ *
20
+ * At the driver because everything crosses the wire decoder exactly once - entity reads, `RETURNING
21
+ * id`, raw SQL, counts, aggregates - while the ORM's hydration only ever sees entity reads. Which
22
+ * types belong here and which need the entity's declaration is settled in `hydratableFields`.
23
+ *
24
+ * `NUMERIC` is deliberately absent, and decoded in hydration instead: `type: BigInt` also maps to
25
+ * BIGINT, so a blanket decode here is already as far as a driver can go without the declaration. That
26
+ * split also covers mysql2, which returns DECIMAL as text and has no equivalent hook.
27
+ *
28
+ * Per pool, never global, which is the whole reason this takes `types` as an argument. TypeORM does
29
+ * the same job by assigning `postgres.defaults.parseInt8`, a module-wide flag every pool in the
30
+ * process then shares; MikroORM passes a per-pool `TypeOverrides`, as here. Two globals of exactly
31
+ * that shape have already been deleted from this repo - `test/pgTypeParsers.util.ts` and the
32
+ * `types.setTypeParser` calls in `neon/neonQuerier.test.ts` - and both made the suite pass on
33
+ * behaviour the library never shipped. Do not reintroduce one.
34
+ *
35
+ * Exact to 2^53, which covers any auto-increment id. A caller who needs more passes their own
36
+ * `types` in the pool options: it is spread after this one and therefore wins. For a decimal, the
37
+ * lighter escape hatch is the declaration itself: `@Field({ type: String, columnType: 'decimal' })`
38
+ * keeps the column DECIMAL while leaving the value as the exact text the driver returned.
39
+ */
40
+ export declare function numericTypes(types: PgTypes): CustomTypesConfig;
41
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Decode `INT8` and `FLOAT8` as JS numbers, leaving every other type to the driver.
3
+ *
4
+ * uql owes this to the caller because uql picks the column: `type: Number` maps to BIGINT (see
5
+ * `schema/canonicalType.ts`), so without it a field declared `number` read back as `'9'` - including
6
+ * every auto-increment primary key, on every entity. `FLOAT8` is a float64, which is exactly what a
7
+ * JS number is, so decoding it loses nothing at all.
8
+ *
9
+ * At the driver because everything crosses the wire decoder exactly once - entity reads, `RETURNING
10
+ * id`, raw SQL, counts, aggregates - while the ORM's hydration only ever sees entity reads. Which
11
+ * types belong here and which need the entity's declaration is settled in `hydratableFields`.
12
+ *
13
+ * `NUMERIC` is deliberately absent, and decoded in hydration instead: `type: BigInt` also maps to
14
+ * BIGINT, so a blanket decode here is already as far as a driver can go without the declaration. That
15
+ * split also covers mysql2, which returns DECIMAL as text and has no equivalent hook.
16
+ *
17
+ * Per pool, never global, which is the whole reason this takes `types` as an argument. TypeORM does
18
+ * the same job by assigning `postgres.defaults.parseInt8`, a module-wide flag every pool in the
19
+ * process then shares; MikroORM passes a per-pool `TypeOverrides`, as here. Two globals of exactly
20
+ * that shape have already been deleted from this repo - `test/pgTypeParsers.util.ts` and the
21
+ * `types.setTypeParser` calls in `neon/neonQuerier.test.ts` - and both made the suite pass on
22
+ * behaviour the library never shipped. Do not reintroduce one.
23
+ *
24
+ * Exact to 2^53, which covers any auto-increment id. A caller who needs more passes their own
25
+ * `types` in the pool options: it is spread after this one and therefore wins. For a decimal, the
26
+ * lighter escape hatch is the declaration itself: `@Field({ type: String, columnType: 'decimal' })`
27
+ * keeps the column DECIMAL while leaving the value as the exact text the driver returned.
28
+ */
29
+ export function numericTypes(types) {
30
+ // Text only: in binary mode an INT8 arrives as an 8-byte Buffer, and `Number(buffer)` is `NaN`.
31
+ const textNumeric = new Set([types.builtins['INT8'], types.builtins['FLOAT8']]);
32
+ return {
33
+ getTypeParser: (oid, format) => format === 'text' && textNumeric.has(oid) ? Number : types.getTypeParser(oid, format),
34
+ };
35
+ }
@@ -6,5 +6,5 @@ import { PgQuerier } from './pgQuerier.js';
6
6
  export declare class PgQuerierPool extends AbstractPgQuerierPool<PoolClient, PgQuerier, PgDialect> {
7
7
  readonly pool: Pool;
8
8
  constructor(opts: PoolConfig, extra?: ExtraOptions);
9
- getQuerier(): Promise<PgQuerier>;
9
+ protected buildQuerier(connect: () => Promise<PoolClient>): PgQuerier;
10
10
  }
@@ -1,14 +1,15 @@
1
- import { Pool } from 'pg';
1
+ import { Pool, types } from 'pg';
2
2
  import { AbstractPgQuerierPool } from './abstractPgQuerierPool.js';
3
3
  import { PgDialect } from './pgDialect.js';
4
+ import { numericTypes } from './pgNumericTypes.js';
4
5
  import { PgQuerier } from './pgQuerier.js';
5
6
  export class PgQuerierPool extends AbstractPgQuerierPool {
6
7
  constructor(opts, extra) {
7
8
  // keepAlive reduces (but can't eliminate) idle connections being silently
8
9
  // dropped by NATs/firewalls on long-lived remote connections.
9
- super(new PgDialect({ namingStrategy: extra?.namingStrategy }), new Pool({ keepAlive: true, ...opts }), extra);
10
+ super(new PgDialect({ namingStrategy: extra?.namingStrategy }), new Pool({ keepAlive: true, types: numericTypes(types), ...opts }), extra);
10
11
  }
11
- async getQuerier() {
12
- return new PgQuerier(() => this.pool.connect(), this.dialect, this.extra);
12
+ buildQuerier(connect) {
13
+ return new PgQuerier(connect, this.dialect, this.extra);
13
14
  }
14
15
  }
@@ -6,9 +6,6 @@ import { LoggerWrapper } from '../util/index.js';
6
6
  */
7
7
  export declare abstract class AbstractQuerier implements Querier {
8
8
  readonly extra?: ExtraOptions | undefined;
9
- private static readonly emittedWarnings;
10
- /** Clears process-wide warning deduplication. For tests only. */
11
- static clearEmittedWarningsForTests(): void;
12
9
  /**
13
10
  * Internal promise used to queue database operations.
14
11
  * This ensures that each operation is executed serially, preventing race conditions
@@ -7,11 +7,6 @@ import { enrichError } from './queryError.js';
7
7
  */
8
8
  export class AbstractQuerier {
9
9
  extra;
10
- static emittedWarnings = new Set();
11
- /** Clears process-wide warning deduplication. For tests only. */
12
- static clearEmittedWarningsForTests() {
13
- AbstractQuerier.emittedWarnings.clear();
14
- }
15
10
  /**
16
11
  * Internal promise used to queue database operations.
17
12
  * This ensures that each operation is executed serially, preventing race conditions
@@ -207,8 +202,13 @@ export class AbstractQuerier {
207
202
  const throughMeta = getMeta(throughEntity);
208
203
  const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts.references[1].local));
209
204
  const ids = payload.map((it) => it[meta.id]);
205
+ // A relation query names the target's columns, not the join table's, so the projection and the
206
+ // filter belong on the populate below - resolved there against the entity that has them. Spread
207
+ // onto the through query they asked `ItemTag` for `Tag`'s columns: `$where`/`$sort` failed with
208
+ // "no such column", and `$exclude` collided with the `$select` this builds.
209
+ const { $select: _select, $exclude: _exclude, $where: _where, ...throughQuery } = relationQuery;
210
210
  const throughFounds = await this.findMany(throughEntity, {
211
- ...relationQuery,
211
+ ...throughQuery,
212
212
  $select: {
213
213
  [localField]: true,
214
214
  },
@@ -219,7 +219,6 @@ export class AbstractQuerier {
219
219
  },
220
220
  },
221
221
  $where: {
222
- ...relationQuery.$where,
223
222
  [localField]: ids,
224
223
  },
225
224
  });
@@ -231,12 +230,14 @@ export class AbstractQuerier {
231
230
  }
232
231
  async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
233
232
  const foreignField = relOpts.references[0].foreign;
234
- // Ensure the FK column is selected so putChildrenInParents can group by it; skips the raw-array
235
- // `$select` form (nothing to augment). Mutates the same object asSelectMap returns.
233
+ // The FK is what putChildrenInParents groups on, so it outlives the relation's projection
234
+ // either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
235
+ // dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
236
236
  const select = asSelectMap(relationQuery.$select);
237
237
  if (select && !select[foreignField]) {
238
238
  select[foreignField] = true;
239
239
  }
240
+ delete relationQuery.$exclude?.[foreignField];
240
241
  const ids = payload.map((it) => it[meta.id]);
241
242
  relationQuery.$where = { ...relationQuery.$where, [foreignField]: ids };
242
243
  const founds = await this.findMany(relEntity, relationQuery);
@@ -41,8 +41,15 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
41
41
  * Drivers with native cursor/streaming APIs (SQLite, Pg) should override this.
42
42
  */
43
43
  protected internalStream<T>(query: string, values?: unknown[]): AsyncIterable<T>;
44
- private hydrateJsonFields;
45
- private hydrateJsonFieldsRecursive;
44
+ /**
45
+ * Turn what a driver returned back into the types the entity declares, for the row and everything
46
+ * populated under it. Which columns, and as what, is `hydratableFields`; the per-cell decode is
47
+ * `decodeColumn`. Both live with the dialect, because a `sparsevec` is only sparse on Postgres.
48
+ *
49
+ * `visited` guards a populated graph that points back at itself; it defaults rather than living in
50
+ * a separate entry-point wrapper, because the wrapper's whole body was seeding it.
51
+ */
52
+ private hydrateFields;
46
53
  protected internalCount<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
47
54
  protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
48
55
  internalInsertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;