linkgress-orm 0.0.2 → 0.1.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 (72) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +196 -196
  3. package/dist/entity/db-column.d.ts +38 -1
  4. package/dist/entity/db-column.d.ts.map +1 -1
  5. package/dist/entity/db-column.js.map +1 -1
  6. package/dist/entity/db-context.d.ts +429 -50
  7. package/dist/entity/db-context.d.ts.map +1 -1
  8. package/dist/entity/db-context.js +884 -203
  9. package/dist/entity/db-context.js.map +1 -1
  10. package/dist/entity/entity-base.d.ts +8 -0
  11. package/dist/entity/entity-base.d.ts.map +1 -1
  12. package/dist/entity/entity-base.js.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/migration/db-schema-manager.js +77 -77
  18. package/dist/migration/enum-migrator.js +6 -6
  19. package/dist/query/collection-strategy.factory.d.ts.map +1 -1
  20. package/dist/query/collection-strategy.factory.js +7 -3
  21. package/dist/query/collection-strategy.factory.js.map +1 -1
  22. package/dist/query/collection-strategy.interface.d.ts +12 -6
  23. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  24. package/dist/query/conditions.d.ts +178 -24
  25. package/dist/query/conditions.d.ts.map +1 -1
  26. package/dist/query/conditions.js +165 -4
  27. package/dist/query/conditions.js.map +1 -1
  28. package/dist/query/cte-builder.d.ts +21 -5
  29. package/dist/query/cte-builder.d.ts.map +1 -1
  30. package/dist/query/cte-builder.js +31 -7
  31. package/dist/query/cte-builder.js.map +1 -1
  32. package/dist/query/grouped-query.d.ts +185 -8
  33. package/dist/query/grouped-query.d.ts.map +1 -1
  34. package/dist/query/grouped-query.js +516 -30
  35. package/dist/query/grouped-query.js.map +1 -1
  36. package/dist/query/join-builder.d.ts +5 -4
  37. package/dist/query/join-builder.d.ts.map +1 -1
  38. package/dist/query/join-builder.js +11 -33
  39. package/dist/query/join-builder.js.map +1 -1
  40. package/dist/query/query-builder.d.ts +89 -20
  41. package/dist/query/query-builder.d.ts.map +1 -1
  42. package/dist/query/query-builder.js +317 -168
  43. package/dist/query/query-builder.js.map +1 -1
  44. package/dist/query/query-utils.d.ts +45 -0
  45. package/dist/query/query-utils.d.ts.map +1 -0
  46. package/dist/query/query-utils.js +103 -0
  47. package/dist/query/query-utils.js.map +1 -0
  48. package/dist/query/sql-utils.d.ts +83 -0
  49. package/dist/query/sql-utils.d.ts.map +1 -0
  50. package/dist/query/sql-utils.js +218 -0
  51. package/dist/query/sql-utils.js.map +1 -0
  52. package/dist/query/strategies/cte-collection-strategy.d.ts +85 -0
  53. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -0
  54. package/dist/query/strategies/cte-collection-strategy.js +338 -0
  55. package/dist/query/strategies/cte-collection-strategy.js.map +1 -0
  56. package/dist/query/strategies/lateral-collection-strategy.d.ts +59 -0
  57. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -0
  58. package/dist/query/strategies/lateral-collection-strategy.js +243 -0
  59. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -0
  60. package/dist/query/strategies/temptable-collection-strategy.d.ts +21 -0
  61. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  62. package/dist/query/strategies/temptable-collection-strategy.js +216 -94
  63. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  64. package/dist/query/subquery.d.ts +24 -1
  65. package/dist/query/subquery.d.ts.map +1 -1
  66. package/dist/query/subquery.js +38 -2
  67. package/dist/query/subquery.js.map +1 -1
  68. package/package.json +1 -1
  69. package/dist/query/strategies/jsonb-collection-strategy.d.ts +0 -51
  70. package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +0 -1
  71. package/dist/query/strategies/jsonb-collection-strategy.js +0 -210
  72. package/dist/query/strategies/jsonb-collection-strategy.js.map +0 -1
@@ -7,6 +7,7 @@ const conditions_1 = require("../query/conditions");
7
7
  const query_builder_1 = require("../query/query-builder");
8
8
  const db_schema_manager_1 = require("../migration/db-schema-manager");
9
9
  const sequence_builder_1 = require("../schema/sequence-builder");
10
+ const sql_utils_1 = require("../query/sql-utils");
10
11
  /**
11
12
  * Query executor with optional logging
12
13
  */
@@ -134,14 +135,6 @@ class InsertBuilder {
134
135
  this.conflictAction = 'nothing';
135
136
  this.overridingSystemValue = false;
136
137
  }
137
- /**
138
- * Get qualified table name with schema prefix if specified
139
- */
140
- getQualifiedTableName() {
141
- return this.schema.schema
142
- ? `"${this.schema.schema}"."${this.schema.name}"`
143
- : `"${this.schema.name}"`;
144
- }
145
138
  /**
146
139
  * Set the values to insert (single row or multiple rows)
147
140
  */
@@ -241,15 +234,9 @@ class InsertBuilder {
241
234
  }
242
235
  valuePlaceholders.push(`(${rowPlaceholders.join(', ')})`);
243
236
  }
244
- const columnNames = columns.map(key => {
245
- const column = this.schema.columns[key];
246
- const config = column.build();
247
- return `"${config.name}"`;
248
- });
249
- const returningColumns = Object.entries(this.schema.columns)
250
- .map(([_, col]) => `"${col.build().name}"`)
251
- .join(', ');
252
- const qualifiedTableName = this.getQualifiedTableName();
237
+ const columnNames = (0, sql_utils_1.buildColumnNamesList)(this.schema, columns);
238
+ const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
239
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
253
240
  let sql = `INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})`;
254
241
  // Add OVERRIDING SYSTEM VALUE if specified
255
242
  if (this.overridingSystemValue) {
@@ -354,6 +341,7 @@ class TableAccessor {
354
341
  }
355
342
  /**
356
343
  * Start a select query with automatic type inference
344
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
357
345
  */
358
346
  select(selector) {
359
347
  return new query_builder_1.SelectQueryBuilder(this.schema, this.client, selector, undefined, undefined, undefined, undefined, this.executor, undefined, undefined, undefined, this.schemaRegistry, undefined, this.collectionStrategy);
@@ -365,6 +353,13 @@ class TableAccessor {
365
353
  const qb = new query_builder_1.QueryBuilder(this.schema, this.client, undefined, undefined, undefined, undefined, this.executor, undefined, undefined, this.collectionStrategy);
366
354
  return qb.where(condition);
367
355
  }
356
+ /**
357
+ * Add CTEs (Common Table Expressions) to the query
358
+ */
359
+ with(...ctes) {
360
+ const qb = new query_builder_1.QueryBuilder(this.schema, this.client, undefined, undefined, undefined, undefined, this.executor, undefined, undefined, this.collectionStrategy);
361
+ return qb.with(...ctes);
362
+ }
368
363
  /**
369
364
  * Left join with another table and selector
370
365
  */
@@ -397,14 +392,6 @@ class TableAccessor {
397
392
  getTableName() {
398
393
  return this.schema.name;
399
394
  }
400
- /**
401
- * Get qualified table name with schema prefix if specified
402
- */
403
- getQualifiedTableName() {
404
- return this.schema.schema
405
- ? `"${this.schema.schema}"."${this.schema.name}"`
406
- : `"${this.schema.name}"`;
407
- }
408
395
  /**
409
396
  * Insert a row
410
397
  */
@@ -428,10 +415,8 @@ class TableAccessor {
428
415
  }
429
416
  }
430
417
  }
431
- const returningColumns = Object.entries(this.schema.columns)
432
- .map(([_, col]) => `"${col.build().name}"`)
433
- .join(', ');
434
- const qualifiedTableName = this.getQualifiedTableName();
418
+ const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
419
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
435
420
  const sql = `
436
421
  INSERT INTO ${qualifiedTableName} (${columns.join(', ')})
437
422
  VALUES (${placeholders.join(', ')})
@@ -451,15 +436,10 @@ class TableAccessor {
451
436
  return [];
452
437
  }
453
438
  // Calculate chunk size based on max rows per batch
454
- let chunkSize = insertConfig?.chunkSize;
455
- if (chunkSize == null && dataArray.length > 0) {
456
- const POSTGRES_MAX_PARAMS = 65535; // PostgreSQL parameter limit
457
- const columnCount = Object.keys(dataArray[0]).length;
458
- const maxRowsPerBatch = Math.floor(POSTGRES_MAX_PARAMS / columnCount);
459
- chunkSize = Math.floor(maxRowsPerBatch * 0.6); // Use 60% of max to be safe
460
- }
439
+ const columnCount = Object.keys(dataArray[0]).length;
440
+ const chunkSize = (0, sql_utils_1.calculateOptimalChunkSize)(columnCount, insertConfig?.chunkSize);
461
441
  // Check if we need to chunk
462
- if (chunkSize && dataArray.length > chunkSize) {
442
+ if (dataArray.length > chunkSize) {
463
443
  const results = [];
464
444
  for (let i = 0; i < dataArray.length; i += chunkSize) {
465
445
  const chunk = dataArray.slice(i, i + chunkSize);
@@ -480,56 +460,28 @@ class TableAccessor {
480
460
  return [];
481
461
  }
482
462
  // Extract all unique column names from all data objects
483
- const columnSet = new Set();
484
- for (const data of dataArray) {
485
- for (const key of Object.keys(data)) {
486
- const column = this.schema.columns[key];
487
- if (column) {
488
- const config = column.build();
489
- if (!config.autoIncrement || insertConfig?.overridingSystemValue) {
490
- columnSet.add(key);
491
- }
492
- }
493
- }
494
- }
495
- const columns = Array.from(columnSet);
496
- const values = [];
497
- const valuePlaceholders = [];
498
- let paramIndex = 1;
499
- // Build placeholders for each row
500
- for (const data of dataArray) {
501
- const rowPlaceholders = [];
502
- for (const key of columns) {
503
- const value = data[key];
504
- const column = this.schema.columns[key];
505
- const config = column.build();
506
- // Apply toDriver mapper if present
507
- const mappedValue = config.mapper
508
- ? config.mapper.toDriver(value !== undefined ? value : null)
509
- : (value !== undefined ? value : null);
510
- values.push(mappedValue);
511
- rowPlaceholders.push(`$${paramIndex++}`);
512
- }
513
- valuePlaceholders.push(`(${rowPlaceholders.join(', ')})`);
463
+ const columns = (0, sql_utils_1.extractUniqueColumnKeys)(dataArray, this.schema, insertConfig?.overridingSystemValue);
464
+ if (columns.length === 0) {
465
+ return [];
514
466
  }
515
- const columnNames = columns.map(key => {
516
- const column = this.schema.columns[key];
517
- const config = column.build();
518
- return `"${config.name}"`;
519
- });
520
- const returningColumns = Object.entries(this.schema.columns)
521
- .map(([_, col]) => `"${col.build().name}"`)
522
- .join(', ');
523
- const qualifiedTableName = this.getQualifiedTableName();
524
- let sql = `INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})`;
467
+ const columnConfigs = (0, sql_utils_1.buildColumnConfigs)(this.schema, columns, insertConfig?.overridingSystemValue);
468
+ const { valueClauses, params } = (0, sql_utils_1.buildValuesClause)(dataArray, columnConfigs);
469
+ const columnNames = columnConfigs.map(c => `"${c.dbName}"`);
470
+ const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
471
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
472
+ let sql = `
473
+ INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})`;
474
+ // Add OVERRIDING SYSTEM VALUE if specified
525
475
  if (insertConfig?.overridingSystemValue) {
526
476
  sql += '\n OVERRIDING SYSTEM VALUE';
527
477
  }
528
- sql += `\n VALUES ${valuePlaceholders.join(', ')}
529
- RETURNING ${returningColumns}`;
478
+ sql += `
479
+ VALUES ${valueClauses.join(', ')}
480
+ RETURNING ${returningColumns}
481
+ `;
530
482
  const result = this.executor
531
- ? await this.executor.query(sql, values)
532
- : await this.client.query(sql, values);
483
+ ? await this.executor.query(sql, params)
484
+ : await this.client.query(sql, params);
533
485
  return result.rows;
534
486
  }
535
487
  /**
@@ -541,33 +493,12 @@ class TableAccessor {
541
493
  }
542
494
  const referenceItem = config?.referenceItem || values[0];
543
495
  // Determine primary keys
544
- let primaryKeys = [];
545
- if (config?.primaryKey) {
546
- primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
547
- }
548
- else {
549
- // Auto-detect from schema
550
- for (const [key, colBuilder] of Object.entries(this.schema.columns)) {
551
- const colConfig = colBuilder.build();
552
- if (colConfig.primaryKey) {
553
- primaryKeys.push(key);
554
- }
555
- }
556
- }
496
+ const primaryKeys = config?.primaryKey
497
+ ? (Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey])
498
+ : (0, sql_utils_1.detectPrimaryKeys)(this.schema);
557
499
  // Auto-detect overridingSystemValue
558
- let overridingSystemValue = config?.overridingSystemValue;
559
- if (overridingSystemValue == null) {
560
- for (const key of Object.keys(referenceItem)) {
561
- const column = this.schema.columns[key];
562
- if (column) {
563
- const colConfig = column.build();
564
- if (colConfig.primaryKey && colConfig.autoIncrement) {
565
- overridingSystemValue = true;
566
- break;
567
- }
568
- }
569
- }
570
- }
500
+ const overridingSystemValue = config?.overridingSystemValue ??
501
+ (0, sql_utils_1.hasAutoIncrementPrimaryKey)(this.schema, Object.keys(referenceItem));
571
502
  // Determine which columns to update
572
503
  let updateColumnFilter = config?.updateColumnFilter;
573
504
  if (updateColumnFilter == null && config?.updateColumns) {
@@ -578,15 +509,10 @@ class TableAccessor {
578
509
  updateColumnFilter = (colId) => !primaryKeys.includes(colId);
579
510
  }
580
511
  // Calculate chunk size based on max rows per batch
581
- let chunkSize = config?.chunkSize;
582
- if (chunkSize == null && values.length > 0) {
583
- const POSTGRES_MAX_PARAMS = 65535;
584
- const columnCount = Object.keys(values[0]).length;
585
- const maxRowsPerBatch = Math.floor(POSTGRES_MAX_PARAMS / columnCount);
586
- chunkSize = Math.floor(maxRowsPerBatch * 0.6); // Use 60% of max to be safe
587
- }
512
+ const columnCount = Object.keys(values[0]).length;
513
+ const chunkSize = (0, sql_utils_1.calculateOptimalChunkSize)(columnCount, config?.chunkSize);
588
514
  // Check if we need to chunk
589
- if (chunkSize && values.length > chunkSize) {
515
+ if (values.length > chunkSize) {
590
516
  const results = [];
591
517
  for (let i = 0; i < values.length; i += chunkSize) {
592
518
  const chunk = values.slice(i, i + chunkSize);
@@ -661,7 +587,7 @@ class TableAccessor {
661
587
  const returningColumns = Object.entries(this.schema.columns)
662
588
  .map(([_, col]) => `"${col.build().name}"`)
663
589
  .join(', ');
664
- const qualifiedTableName = this.getQualifiedTableName();
590
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
665
591
  const sql = `
666
592
  INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})
667
593
  VALUES ${valuePlaceholders.join(', ')}
@@ -725,7 +651,7 @@ class TableAccessor {
725
651
  const returningColumns = Object.entries(this.schema.columns)
726
652
  .map(([_, col]) => `"${col.build().name}"`)
727
653
  .join(', ');
728
- const qualifiedTableName = this.getQualifiedTableName();
654
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
729
655
  const sql = `
730
656
  UPDATE ${qualifiedTableName}
731
657
  SET ${setClauses.join(', ')}
@@ -753,7 +679,7 @@ class TableAccessor {
753
679
  if (!pkColumnName) {
754
680
  throw new Error(`Table ${this.schema.name} has no primary key`);
755
681
  }
756
- const qualifiedTableName = this.getQualifiedTableName();
682
+ const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
757
683
  const sql = `DELETE FROM ${qualifiedTableName} WHERE "${pkColumnName}" = $1`;
758
684
  const result = this.executor
759
685
  ? await this.executor.query(sql, [id])
@@ -1007,6 +933,53 @@ class DbEntityTable {
1007
933
  : await client.query(sql, []);
1008
934
  return this.mapResultsToEntities(result.rows);
1009
935
  }
936
+ /**
937
+ * Get first record
938
+ * @throws Error if no records exist
939
+ */
940
+ async first() {
941
+ const queryBuilder = this.context.getTable(this.tableName);
942
+ const schema = this._getSchema();
943
+ // Build SELECT with all columns and LIMIT 1
944
+ const columns = Object.entries(schema.columns)
945
+ .map(([_, col]) => `"${col.build().name}"`)
946
+ .join(', ');
947
+ const qualifiedTableName = this._getQualifiedTableName();
948
+ const sql = `SELECT ${columns} FROM ${qualifiedTableName} LIMIT 1`;
949
+ const executor = this._getExecutor();
950
+ const client = this._getClient();
951
+ const result = executor
952
+ ? await executor.query(sql, [])
953
+ : await client.query(sql, []);
954
+ if (result.rows.length === 0) {
955
+ throw new Error('Sequence contains no elements');
956
+ }
957
+ const mapped = this.mapResultsToEntities(result.rows);
958
+ return mapped[0];
959
+ }
960
+ /**
961
+ * Get first record or null if none exist
962
+ */
963
+ async firstOrDefault() {
964
+ const queryBuilder = this.context.getTable(this.tableName);
965
+ const schema = this._getSchema();
966
+ // Build SELECT with all columns and LIMIT 1
967
+ const columns = Object.entries(schema.columns)
968
+ .map(([_, col]) => `"${col.build().name}"`)
969
+ .join(', ');
970
+ const qualifiedTableName = this._getQualifiedTableName();
971
+ const sql = `SELECT ${columns} FROM ${qualifiedTableName} LIMIT 1`;
972
+ const executor = this._getExecutor();
973
+ const client = this._getClient();
974
+ const result = executor
975
+ ? await executor.query(sql, [])
976
+ : await client.query(sql, []);
977
+ if (result.rows.length === 0) {
978
+ return null;
979
+ }
980
+ const mapped = this.mapResultsToEntities(result.rows);
981
+ return mapped[0];
982
+ }
1010
983
  /**
1011
984
  * Count all records
1012
985
  */
@@ -1065,6 +1038,7 @@ class DbEntityTable {
1065
1038
  }
1066
1039
  /**
1067
1040
  * Select query
1041
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
1068
1042
  */
1069
1043
  select(selector) {
1070
1044
  const queryBuilder = this.context.getTable(this.tableName).select(selector);
@@ -1092,19 +1066,16 @@ class DbEntityTable {
1092
1066
  for (const colName of Object.keys(schema.columns)) {
1093
1067
  result[colName] = e[colName];
1094
1068
  }
1095
- // Add navigation properties
1069
+ // Add navigation properties as enumerable getters
1070
+ // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1071
+ // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1072
+ // When the selection is analyzed, the builder is detected and handled appropriately
1096
1073
  for (const relName of Object.keys(schema.relations)) {
1097
- const relConfig = schema.relations[relName];
1098
- if (relConfig.type === 'many') {
1099
- // For collections, use empty array placeholder
1100
- // (prevents CollectionQueryBuilder evaluation)
1101
- result[relName] = [];
1102
- }
1103
- else {
1104
- // For references, copy the ReferenceQueryBuilder mock
1105
- // This allows accessing fields like o.user!.username in chained selectors
1106
- result[relName] = e[relName];
1107
- }
1074
+ Object.defineProperty(result, relName, {
1075
+ get: () => e[relName],
1076
+ enumerable: true, // Enumerable so it's included in Object.entries
1077
+ configurable: true,
1078
+ });
1108
1079
  }
1109
1080
  return result;
1110
1081
  };
@@ -1114,85 +1085,472 @@ class DbEntityTable {
1114
1085
  return queryBuilder;
1115
1086
  }
1116
1087
  /**
1117
- * Left join with another table or subquery and selector
1088
+ * Add CTEs (Common Table Expressions) to the query
1118
1089
  */
1090
+ with(...ctes) {
1091
+ const schema = this._getSchema();
1092
+ // Create a selector that selects all columns with navigation property access
1093
+ const allColumnsSelector = (e) => {
1094
+ const result = {};
1095
+ // Copy all column properties
1096
+ for (const colName of Object.keys(schema.columns)) {
1097
+ result[colName] = e[colName];
1098
+ }
1099
+ // Add navigation properties as enumerable getters
1100
+ // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1101
+ // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1102
+ // When the selection is analyzed, the builder is detected and handled appropriately
1103
+ for (const relName of Object.keys(schema.relations)) {
1104
+ Object.defineProperty(result, relName, {
1105
+ get: () => e[relName],
1106
+ enumerable: true, // Enumerable so it's included in Object.entries
1107
+ configurable: true,
1108
+ });
1109
+ }
1110
+ return result;
1111
+ };
1112
+ const queryBuilder = this.context.getTable(this.tableName)
1113
+ .with(...ctes)
1114
+ .select(allColumnsSelector);
1115
+ return queryBuilder;
1116
+ }
1119
1117
  leftJoin(rightTable, condition, selector, alias) {
1120
1118
  const queryBuilder = this.context.getTable(this.tableName).leftJoin(rightTable, condition, selector, alias);
1121
1119
  return queryBuilder;
1122
1120
  }
1123
- /**
1124
- * Inner join with another table or subquery and selector
1125
- */
1126
1121
  innerJoin(rightTable, condition, selector, alias) {
1127
1122
  const queryBuilder = this.context.getTable(this.tableName).innerJoin(rightTable, condition, selector, alias);
1128
1123
  return queryBuilder;
1129
1124
  }
1130
1125
  /**
1131
1126
  * Insert - accepts only DbColumn properties (excludes navigation properties)
1127
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * // No returning (default) - returns void
1132
+ * await db.users.insert({ username: 'alice', email: 'alice@test.com' });
1133
+ *
1134
+ * // With returning() - returns full entity
1135
+ * const user = await db.users.insert({ username: 'alice' }).returning();
1136
+ *
1137
+ * // With returning(selector) - returns selected columns
1138
+ * const { id } = await db.users.insert({ username: 'alice' }).returning(u => ({ id: u.id }));
1139
+ * ```
1132
1140
  */
1133
- async insert(data) {
1134
- const result = await this.context.getTable(this.tableName).insert(data);
1135
- return this.mapResultToEntity(result);
1141
+ insert(data) {
1142
+ const table = this;
1143
+ const executeInsert = async (returning) => {
1144
+ const schema = table._getSchema();
1145
+ const executor = table._getExecutor();
1146
+ const client = table._getClient();
1147
+ // Build INSERT columns and values
1148
+ const columns = [];
1149
+ const values = [];
1150
+ const placeholders = [];
1151
+ let paramIndex = 1;
1152
+ for (const [key, value] of Object.entries(data)) {
1153
+ const column = schema.columns[key];
1154
+ if (column) {
1155
+ const config = column.build();
1156
+ if (!config.autoIncrement) {
1157
+ columns.push(`"${config.name}"`);
1158
+ const mappedValue = config.mapper ? config.mapper.toDriver(value) : value;
1159
+ values.push(mappedValue);
1160
+ placeholders.push(`$${paramIndex++}`);
1161
+ }
1162
+ }
1163
+ }
1164
+ // Build RETURNING clause
1165
+ const returningClause = table.buildReturningClause(returning);
1166
+ const qualifiedTableName = table._getQualifiedTableName();
1167
+ let sql = `INSERT INTO ${qualifiedTableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
1168
+ if (returningClause) {
1169
+ sql += ` RETURNING ${returningClause.sql}`;
1170
+ }
1171
+ const result = executor
1172
+ ? await executor.query(sql, values)
1173
+ : await client.query(sql, values);
1174
+ if (!returningClause) {
1175
+ return undefined;
1176
+ }
1177
+ const mappedResults = table.mapReturningResults(result.rows, returning);
1178
+ return mappedResults[0];
1179
+ };
1180
+ return {
1181
+ then(onfulfilled, onrejected) {
1182
+ return executeInsert(undefined).then(onfulfilled, onrejected);
1183
+ },
1184
+ returning(selector) {
1185
+ const returningConfig = selector ?? true;
1186
+ return {
1187
+ then(onfulfilled, onrejected) {
1188
+ return executeInsert(returningConfig).then(onfulfilled, onrejected);
1189
+ }
1190
+ };
1191
+ }
1192
+ };
1136
1193
  }
1137
1194
  /**
1138
1195
  * Insert multiple records
1196
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1197
+ *
1198
+ * @example
1199
+ * ```typescript
1200
+ * // No returning (default) - returns void
1201
+ * await db.users.insertMany([{ username: 'alice' }, { username: 'bob' }]);
1202
+ *
1203
+ * // With returning() - returns full entities
1204
+ * const users = await db.users.insertMany([{ username: 'alice' }]).returning();
1205
+ *
1206
+ * // With returning(selector) - returns selected columns
1207
+ * const results = await db.users.insertMany([{ username: 'alice' }]).returning(u => ({ id: u.id }));
1208
+ * ```
1139
1209
  */
1140
- async insertMany(data) {
1210
+ insertMany(data) {
1141
1211
  return this.insertBulk(data);
1142
1212
  }
1143
1213
  /**
1144
1214
  * Upsert (insert or update on conflict)
1215
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * // No returning (default) - returns void
1220
+ * await db.users.upsert([{ username: 'alice' }], { primaryKey: 'username' });
1221
+ *
1222
+ * // With returning() - returns full entities
1223
+ * const users = await db.users.upsert([{ username: 'alice' }], { primaryKey: 'username' }).returning();
1224
+ * ```
1145
1225
  */
1146
- async upsert(data, config) {
1226
+ upsert(data, config) {
1147
1227
  return this.upsertBulk(data, config);
1148
1228
  }
1149
1229
  /**
1150
1230
  * Bulk insert with advanced configuration
1151
1231
  * Supports automatic chunking for large datasets
1232
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1233
+ *
1234
+ * @example
1235
+ * ```typescript
1236
+ * // No returning (default) - returns void
1237
+ * await db.users.insertBulk([{ username: 'alice' }]);
1238
+ *
1239
+ * // With returning() - returns full entities
1240
+ * const users = await db.users.insertBulk([{ username: 'alice' }]).returning();
1241
+ *
1242
+ * // With returning(selector) - returns selected columns
1243
+ * const results = await db.users.insertBulk([{ username: 'alice' }]).returning(u => ({ id: u.id }));
1244
+ *
1245
+ * // With options
1246
+ * await db.users.insertBulk([{ username: 'alice' }], { chunkSize: 100 });
1247
+ * ```
1152
1248
  */
1153
- async insertBulk(value, insertConfig) {
1154
- const results = await this.context.getTable(this.tableName).insertBulk(value, insertConfig);
1155
- return this.mapResultsToEntities(results);
1249
+ insertBulk(value, options) {
1250
+ const table = this;
1251
+ const dataArray = Array.isArray(value) ? value : [value];
1252
+ const executeInsertBulk = async (returning) => {
1253
+ if (dataArray.length === 0) {
1254
+ return returning === undefined ? undefined : [];
1255
+ }
1256
+ // Calculate chunk size
1257
+ let chunkSize = options?.chunkSize;
1258
+ if (chunkSize == null) {
1259
+ const POSTGRES_MAX_PARAMS = 65535;
1260
+ const columnCount = Object.keys(dataArray[0]).length;
1261
+ const maxRowsPerBatch = Math.floor(POSTGRES_MAX_PARAMS / columnCount);
1262
+ chunkSize = Math.floor(maxRowsPerBatch * 0.6);
1263
+ }
1264
+ // Process in chunks if needed
1265
+ if (dataArray.length > chunkSize) {
1266
+ const allResults = [];
1267
+ for (let i = 0; i < dataArray.length; i += chunkSize) {
1268
+ const chunk = dataArray.slice(i, i + chunkSize);
1269
+ const chunkResults = await table.insertBulkSingle(chunk, returning, options?.overridingSystemValue);
1270
+ if (chunkResults)
1271
+ allResults.push(...chunkResults);
1272
+ }
1273
+ return returning === undefined ? undefined : allResults;
1274
+ }
1275
+ return table.insertBulkSingle(dataArray, returning, options?.overridingSystemValue);
1276
+ };
1277
+ return {
1278
+ then(onfulfilled, onrejected) {
1279
+ return executeInsertBulk(undefined).then(onfulfilled, onrejected);
1280
+ },
1281
+ returning(selector) {
1282
+ const returningConfig = selector ?? true;
1283
+ return {
1284
+ then(onfulfilled, onrejected) {
1285
+ return executeInsertBulk(returningConfig).then(onfulfilled, onrejected);
1286
+ }
1287
+ };
1288
+ }
1289
+ };
1290
+ }
1291
+ /**
1292
+ * Execute a single bulk insert batch
1293
+ * @internal
1294
+ */
1295
+ async insertBulkSingle(data, returning, overridingSystemValue) {
1296
+ const schema = this._getSchema();
1297
+ const executor = this._getExecutor();
1298
+ const client = this._getClient();
1299
+ const qualifiedTableName = this._getQualifiedTableName();
1300
+ // Get columns from first row
1301
+ const referenceItem = data[0];
1302
+ const columnConfigs = [];
1303
+ for (const [propName, colBuilder] of Object.entries(schema.columns)) {
1304
+ if (propName in referenceItem) {
1305
+ const config = colBuilder.build();
1306
+ if (!config.autoIncrement || overridingSystemValue) {
1307
+ columnConfigs.push({
1308
+ propName,
1309
+ dbName: config.name,
1310
+ mapper: config.mapper,
1311
+ });
1312
+ }
1313
+ }
1314
+ }
1315
+ // Build VALUES clauses
1316
+ const valuesClauses = [];
1317
+ const params = [];
1318
+ let paramIndex = 1;
1319
+ for (const record of data) {
1320
+ const rowValues = [];
1321
+ for (const col of columnConfigs) {
1322
+ const value = record[col.propName];
1323
+ const mappedValue = col.mapper ? col.mapper.toDriver(value) : value;
1324
+ rowValues.push(`$${paramIndex++}`);
1325
+ params.push(mappedValue);
1326
+ }
1327
+ valuesClauses.push(`(${rowValues.join(', ')})`);
1328
+ }
1329
+ const columnList = columnConfigs.map(c => `"${c.dbName}"`).join(', ');
1330
+ const returningClause = this.buildReturningClause(returning);
1331
+ let sql = `INSERT INTO ${qualifiedTableName} (${columnList})`;
1332
+ if (overridingSystemValue) {
1333
+ sql += ' OVERRIDING SYSTEM VALUE';
1334
+ }
1335
+ sql += ` VALUES ${valuesClauses.join(', ')}`;
1336
+ if (returningClause) {
1337
+ sql += ` RETURNING ${returningClause.sql}`;
1338
+ }
1339
+ const result = executor
1340
+ ? await executor.query(sql, params)
1341
+ : await client.query(sql, params);
1342
+ if (!returningClause) {
1343
+ return undefined;
1344
+ }
1345
+ return this.mapReturningResults(result.rows, returning);
1156
1346
  }
1157
1347
  /**
1158
1348
  * Upsert with advanced configuration
1159
1349
  * Auto-detects primary keys and supports chunking
1350
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1351
+ *
1352
+ * @example
1353
+ * ```typescript
1354
+ * // No returning (default) - returns void
1355
+ * await db.users.upsertBulk([{ id: 1, username: 'alice' }], { primaryKey: 'id' });
1356
+ *
1357
+ * // With returning() - returns full entities
1358
+ * const users = await db.users.upsertBulk([{ id: 1, username: 'alice' }]).returning();
1359
+ *
1360
+ * // With returning(selector) - returns selected columns
1361
+ * const results = await db.users.upsertBulk([{ id: 1, username: 'alice' }])
1362
+ * .returning(u => ({ id: u.id }));
1363
+ * ```
1160
1364
  */
1161
- async upsertBulk(values, config) {
1162
- // Convert typed config to base config
1163
- const baseConfig = {
1164
- chunkSize: config?.chunkSize,
1165
- overridingSystemValue: config?.overridingSystemValue,
1166
- targetWhere: config?.targetWhere,
1167
- setWhere: config?.setWhere,
1168
- referenceItem: config?.referenceItem,
1169
- updateColumnFilter: config?.updateColumnFilter,
1170
- };
1171
- // Handle primaryKey (can be lambda, string, or array)
1172
- if (config?.primaryKey) {
1173
- if (typeof config.primaryKey === 'function') {
1174
- // Lambda selector - extract property names
1175
- const pkProps = this.extractPropertyNames(config.primaryKey);
1176
- baseConfig.primaryKey = pkProps.length === 1 ? pkProps[0] : pkProps;
1365
+ upsertBulk(values, config) {
1366
+ const table = this;
1367
+ const executeUpsertBulk = async (returning) => {
1368
+ if (values.length === 0) {
1369
+ return returning === undefined ? undefined : [];
1370
+ }
1371
+ const schema = table._getSchema();
1372
+ // Handle primaryKey (can be lambda, string, or array)
1373
+ let primaryKeys = [];
1374
+ if (config?.primaryKey) {
1375
+ if (typeof config.primaryKey === 'function') {
1376
+ const pkProps = table.extractPropertyNames(config.primaryKey);
1377
+ primaryKeys = pkProps;
1378
+ }
1379
+ else if (Array.isArray(config.primaryKey)) {
1380
+ primaryKeys = config.primaryKey;
1381
+ }
1382
+ else {
1383
+ primaryKeys = [config.primaryKey];
1384
+ }
1177
1385
  }
1178
1386
  else {
1179
- // Direct string or array
1180
- baseConfig.primaryKey = config.primaryKey;
1387
+ // Auto-detect from schema
1388
+ for (const [key, colBuilder] of Object.entries(schema.columns)) {
1389
+ const colConfig = colBuilder.build();
1390
+ if (colConfig.primaryKey) {
1391
+ primaryKeys.push(key);
1392
+ }
1393
+ }
1394
+ }
1395
+ // Handle updateColumns (can be lambda or array)
1396
+ let updateColumns;
1397
+ if (config?.updateColumns) {
1398
+ if (typeof config.updateColumns === 'function') {
1399
+ updateColumns = table.extractPropertyNames(config.updateColumns);
1400
+ }
1401
+ else {
1402
+ updateColumns = config.updateColumns;
1403
+ }
1404
+ }
1405
+ // Auto-detect overridingSystemValue
1406
+ let overridingSystemValue = config?.overridingSystemValue;
1407
+ if (overridingSystemValue == null) {
1408
+ const referenceItem = config?.referenceItem || values[0];
1409
+ for (const key of Object.keys(referenceItem)) {
1410
+ const column = schema.columns[key];
1411
+ if (column) {
1412
+ const colConfig = column.build();
1413
+ if (colConfig.primaryKey && colConfig.autoIncrement) {
1414
+ overridingSystemValue = true;
1415
+ break;
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ // Calculate chunk size
1421
+ let chunkSize = config?.chunkSize;
1422
+ if (chunkSize == null) {
1423
+ const POSTGRES_MAX_PARAMS = 65535;
1424
+ const columnCount = Object.keys(values[0]).length;
1425
+ const maxRowsPerBatch = Math.floor(POSTGRES_MAX_PARAMS / columnCount);
1426
+ chunkSize = Math.floor(maxRowsPerBatch * 0.6);
1427
+ }
1428
+ // Process in chunks if needed
1429
+ if (values.length > chunkSize) {
1430
+ const allResults = [];
1431
+ for (let i = 0; i < values.length; i += chunkSize) {
1432
+ const chunk = values.slice(i, i + chunkSize);
1433
+ const chunkResults = await table.upsertBulkSingle(chunk, primaryKeys, updateColumns, config?.updateColumnFilter, overridingSystemValue || false, config?.targetWhere, config?.setWhere, returning);
1434
+ if (chunkResults)
1435
+ allResults.push(...chunkResults);
1436
+ }
1437
+ return returning === undefined ? undefined : allResults;
1438
+ }
1439
+ return table.upsertBulkSingle(values, primaryKeys, updateColumns, config?.updateColumnFilter, overridingSystemValue || false, config?.targetWhere, config?.setWhere, returning);
1440
+ };
1441
+ return {
1442
+ then(onfulfilled, onrejected) {
1443
+ return executeUpsertBulk(undefined).then(onfulfilled, onrejected);
1444
+ },
1445
+ returning(selector) {
1446
+ const returningConfig = selector ?? true;
1447
+ return {
1448
+ then(onfulfilled, onrejected) {
1449
+ return executeUpsertBulk(returningConfig).then(onfulfilled, onrejected);
1450
+ }
1451
+ };
1452
+ }
1453
+ };
1454
+ }
1455
+ /**
1456
+ * Execute a single upsert batch
1457
+ * @internal
1458
+ */
1459
+ async upsertBulkSingle(values, primaryKeys, updateColumns, updateColumnFilter, overridingSystemValue, targetWhere, setWhere, returning) {
1460
+ const schema = this._getSchema();
1461
+ const executor = this._getExecutor();
1462
+ const client = this._getClient();
1463
+ const qualifiedTableName = this._getQualifiedTableName();
1464
+ // Extract all unique column names from all data objects
1465
+ const columnConfigs = [];
1466
+ const columnSet = new Set();
1467
+ for (const data of values) {
1468
+ for (const key of Object.keys(data)) {
1469
+ if (!columnSet.has(key)) {
1470
+ const column = schema.columns[key];
1471
+ if (column) {
1472
+ const config = column.build();
1473
+ if (!config.autoIncrement || overridingSystemValue) {
1474
+ columnSet.add(key);
1475
+ columnConfigs.push({
1476
+ propName: key,
1477
+ dbName: config.name,
1478
+ mapper: config.mapper,
1479
+ });
1480
+ }
1481
+ }
1482
+ }
1181
1483
  }
1182
1484
  }
1183
- // Handle updateColumns (can be lambda or array)
1184
- if (config?.updateColumns) {
1185
- if (typeof config.updateColumns === 'function') {
1186
- // Lambda selector - extract property names
1187
- baseConfig.updateColumns = this.extractPropertyNames(config.updateColumns);
1485
+ // Build VALUES clauses
1486
+ const valuesClauses = [];
1487
+ const params = [];
1488
+ let paramIndex = 1;
1489
+ for (const record of values) {
1490
+ const rowValues = [];
1491
+ for (const col of columnConfigs) {
1492
+ const value = record[col.propName];
1493
+ const mappedValue = col.mapper
1494
+ ? col.mapper.toDriver(value !== undefined ? value : null)
1495
+ : (value !== undefined ? value : null);
1496
+ rowValues.push(`$${paramIndex++}`);
1497
+ params.push(mappedValue);
1188
1498
  }
1189
- else {
1190
- // Direct array
1191
- baseConfig.updateColumns = config.updateColumns;
1499
+ valuesClauses.push(`(${rowValues.join(', ')})`);
1500
+ }
1501
+ const columnList = columnConfigs.map(c => `"${c.dbName}"`).join(', ');
1502
+ // Build SQL
1503
+ let sql = `INSERT INTO ${qualifiedTableName} (${columnList})`;
1504
+ if (overridingSystemValue) {
1505
+ sql += ' OVERRIDING SYSTEM VALUE';
1506
+ }
1507
+ sql += ` VALUES ${valuesClauses.join(', ')}`;
1508
+ // Add ON CONFLICT clause
1509
+ const conflictCols = primaryKeys.map(pk => {
1510
+ const col = columnConfigs.find(c => c.propName === pk);
1511
+ return col ? `"${col.dbName}"` : `"${pk}"`;
1512
+ }).join(', ');
1513
+ sql += ` ON CONFLICT (${conflictCols})`;
1514
+ if (targetWhere) {
1515
+ sql += ` WHERE ${targetWhere}`;
1516
+ }
1517
+ // Determine columns to update
1518
+ let columnsToUpdate;
1519
+ if (updateColumns) {
1520
+ columnsToUpdate = updateColumns;
1521
+ }
1522
+ else if (updateColumnFilter) {
1523
+ columnsToUpdate = Array.from(columnSet).filter(updateColumnFilter);
1524
+ }
1525
+ else {
1526
+ columnsToUpdate = Array.from(columnSet).filter(key => !primaryKeys.includes(key));
1527
+ }
1528
+ if (columnsToUpdate.length === 0) {
1529
+ sql += ' DO NOTHING';
1530
+ }
1531
+ else {
1532
+ const updateSetClauses = columnsToUpdate.map(propName => {
1533
+ const col = columnConfigs.find(c => c.propName === propName);
1534
+ const dbName = col ? col.dbName : propName;
1535
+ return `"${dbName}" = EXCLUDED."${dbName}"`;
1536
+ });
1537
+ sql += ` DO UPDATE SET ${updateSetClauses.join(', ')}`;
1538
+ if (setWhere) {
1539
+ sql += ` WHERE ${setWhere}`;
1192
1540
  }
1193
1541
  }
1194
- const results = await this.context.getTable(this.tableName).upsertBulk(values, baseConfig);
1195
- return this.mapResultsToEntities(results);
1542
+ // Add RETURNING clause
1543
+ const returningClause = this.buildReturningClause(returning);
1544
+ if (returningClause) {
1545
+ sql += ` RETURNING ${returningClause.sql}`;
1546
+ }
1547
+ const result = executor
1548
+ ? await executor.query(sql, params)
1549
+ : await client.query(sql, params);
1550
+ if (!returningClause) {
1551
+ return undefined;
1552
+ }
1553
+ return this.mapReturningResults(result.rows, returning);
1196
1554
  }
1197
1555
  /**
1198
1556
  * Map database column names back to property names
@@ -1244,48 +1602,268 @@ class DbEntityTable {
1244
1602
  }
1245
1603
  /**
1246
1604
  * Update records matching condition
1247
- * Usage: db.users.update({ age: 30 }, u => eq(u.id, 1))
1605
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1606
+ *
1607
+ * @example
1608
+ * ```typescript
1609
+ * // No returning (default) - returns void
1610
+ * await db.users.update({ age: 30 }, u => eq(u.id, 1));
1611
+ *
1612
+ * // With returning() - returns full entities
1613
+ * const users = await db.users.update({ age: 30 }, u => eq(u.id, 1)).returning();
1614
+ *
1615
+ * // With returning(selector) - returns selected columns
1616
+ * const results = await db.users.update({ age: 30 }, u => eq(u.id, 1))
1617
+ * .returning(u => ({ id: u.id, age: u.age }));
1618
+ * ```
1619
+ */
1620
+ update(data, condition) {
1621
+ const table = this;
1622
+ const executeUpdate = async (returning) => {
1623
+ const schema = table._getSchema();
1624
+ const executor = table._getExecutor();
1625
+ const client = table._getClient();
1626
+ // Build SET clause
1627
+ const setClauses = [];
1628
+ const values = [];
1629
+ let paramIndex = 1;
1630
+ for (const [key, value] of Object.entries(data)) {
1631
+ const column = schema.columns[key];
1632
+ if (column) {
1633
+ const config = column.build();
1634
+ setClauses.push(`"${config.name}" = $${paramIndex++}`);
1635
+ values.push(value);
1636
+ }
1637
+ }
1638
+ if (setClauses.length === 0) {
1639
+ throw new Error('No valid columns to update');
1640
+ }
1641
+ // Build WHERE clause
1642
+ const mockEntity = table.createMockEntity();
1643
+ const whereCondition = condition(mockEntity);
1644
+ const condBuilder = new conditions_1.ConditionBuilder();
1645
+ const { sql: whereSql, params: whereParams } = condBuilder.build(whereCondition, paramIndex);
1646
+ values.push(...whereParams);
1647
+ // Build RETURNING clause
1648
+ const returningClause = table.buildReturningClause(returning);
1649
+ const qualifiedTableName = table._getQualifiedTableName();
1650
+ let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
1651
+ if (returningClause) {
1652
+ sql += ` RETURNING ${returningClause.sql}`;
1653
+ }
1654
+ const result = executor
1655
+ ? await executor.query(sql, values)
1656
+ : await client.query(sql, values);
1657
+ if (!returningClause) {
1658
+ return undefined;
1659
+ }
1660
+ return table.mapReturningResults(result.rows, returning);
1661
+ };
1662
+ return {
1663
+ then(onfulfilled, onrejected) {
1664
+ return executeUpdate(undefined).then(onfulfilled, onrejected);
1665
+ },
1666
+ returning(selector) {
1667
+ const returningConfig = selector ?? true;
1668
+ return {
1669
+ then(onfulfilled, onrejected) {
1670
+ return executeUpdate(returningConfig).then(onfulfilled, onrejected);
1671
+ }
1672
+ };
1673
+ }
1674
+ };
1675
+ }
1676
+ /**
1677
+ * Bulk update multiple records efficiently using PostgreSQL VALUES clause
1678
+ * Updates records matching primary key(s) with provided data
1679
+ * Returns a fluent builder that can be awaited directly or chained with .returning()
1680
+ *
1681
+ * @param data Array of objects with primary key(s) and columns to update
1682
+ * @param config Optional configuration for the bulk update
1683
+ *
1684
+ * @example
1685
+ * ```typescript
1686
+ * // No returning (default) - returns void
1687
+ * await db.users.bulkUpdate([
1688
+ * { id: 1, age: 30 },
1689
+ * { id: 2, age: 25 },
1690
+ * ]);
1691
+ *
1692
+ * // With returning() - returns full entities
1693
+ * const updated = await db.users.bulkUpdate([{ id: 1, age: 30 }]).returning();
1694
+ *
1695
+ * // With returning(selector) - returns selected columns
1696
+ * const results = await db.users.bulkUpdate([{ id: 1, age: 30 }])
1697
+ * .returning(u => ({ id: u.id, age: u.age }));
1698
+ *
1699
+ * // With custom primary key
1700
+ * await db.users.bulkUpdate(
1701
+ * [{ username: 'alice', age: 31 }],
1702
+ * { primaryKey: 'username' }
1703
+ * );
1704
+ * ```
1248
1705
  */
1249
- async update(data, condition) {
1706
+ bulkUpdate(data, config) {
1707
+ const table = this;
1708
+ const executeBulkUpdate = async (returning) => {
1709
+ if (data.length === 0) {
1710
+ return returning === undefined ? undefined : [];
1711
+ }
1712
+ const schema = table._getSchema();
1713
+ // Determine primary keys
1714
+ let primaryKeys = [];
1715
+ if (config?.primaryKey) {
1716
+ primaryKeys = Array.isArray(config.primaryKey) ? config.primaryKey : [config.primaryKey];
1717
+ }
1718
+ else {
1719
+ // Auto-detect from schema
1720
+ for (const [key, colBuilder] of Object.entries(schema.columns)) {
1721
+ const colConfig = colBuilder.build();
1722
+ if (colConfig.primaryKey) {
1723
+ primaryKeys.push(key);
1724
+ }
1725
+ }
1726
+ }
1727
+ if (primaryKeys.length === 0) {
1728
+ throw new Error('bulkUpdate requires at least one primary key column');
1729
+ }
1730
+ // Validate all records have primary keys
1731
+ for (let i = 0; i < data.length; i++) {
1732
+ for (const pk of primaryKeys) {
1733
+ if (data[i][pk] === undefined) {
1734
+ throw new Error(`Record at index ${i} is missing primary key "${pk}"`);
1735
+ }
1736
+ }
1737
+ }
1738
+ // Calculate chunk size
1739
+ let chunkSize = config?.chunkSize;
1740
+ if (chunkSize == null) {
1741
+ const POSTGRES_MAX_PARAMS = 65535;
1742
+ const referenceItem = data[0];
1743
+ const columnCount = Object.keys(referenceItem).length;
1744
+ const maxRowsPerBatch = Math.floor(POSTGRES_MAX_PARAMS / columnCount);
1745
+ chunkSize = Math.floor(maxRowsPerBatch * 0.6);
1746
+ }
1747
+ // Process in chunks if needed
1748
+ if (data.length > chunkSize) {
1749
+ const allResults = [];
1750
+ for (let i = 0; i < data.length; i += chunkSize) {
1751
+ const chunk = data.slice(i, i + chunkSize);
1752
+ const chunkResults = await table.bulkUpdateSingle(chunk, primaryKeys, returning);
1753
+ if (chunkResults)
1754
+ allResults.push(...chunkResults);
1755
+ }
1756
+ return returning === undefined ? undefined : allResults;
1757
+ }
1758
+ return table.bulkUpdateSingle(data, primaryKeys, returning);
1759
+ };
1760
+ return {
1761
+ then(onfulfilled, onrejected) {
1762
+ return executeBulkUpdate(undefined).then(onfulfilled, onrejected);
1763
+ },
1764
+ returning(selector) {
1765
+ const returningConfig = selector ?? true;
1766
+ return {
1767
+ then(onfulfilled, onrejected) {
1768
+ return executeBulkUpdate(returningConfig).then(onfulfilled, onrejected);
1769
+ }
1770
+ };
1771
+ }
1772
+ };
1773
+ }
1774
+ /**
1775
+ * Execute a single bulk update batch
1776
+ * @internal
1777
+ */
1778
+ async bulkUpdateSingle(data, primaryKeys, returning) {
1250
1779
  const schema = this._getSchema();
1251
1780
  const executor = this._getExecutor();
1252
1781
  const client = this._getClient();
1253
- // Build SET clause
1782
+ const qualifiedTableName = this._getQualifiedTableName();
1783
+ const primaryKeySet = new Set(primaryKeys);
1784
+ // Single pass: collect columns and build column info simultaneously
1785
+ const updateColumnsSet = new Set();
1786
+ const allColumnsSet = new Set(primaryKeys);
1787
+ for (const record of data) {
1788
+ for (const key of Object.keys(record)) {
1789
+ if (schema.columns[key]) {
1790
+ allColumnsSet.add(key);
1791
+ if (!primaryKeySet.has(key)) {
1792
+ updateColumnsSet.add(key);
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+ if (updateColumnsSet.size === 0) {
1798
+ throw new Error('No columns to update (only primary keys provided)');
1799
+ }
1800
+ // Build column info - access config once per column
1801
+ const columnInfoList = [];
1802
+ const valueColumnParts = [];
1254
1803
  const setClauses = [];
1255
- const values = [];
1256
- let paramIndex = 1;
1257
- for (const [key, value] of Object.entries(data)) {
1258
- const column = schema.columns[key];
1259
- if (column) {
1260
- const config = column.build();
1261
- setClauses.push(`"${config.name}" = $${paramIndex++}`);
1262
- values.push(value);
1804
+ const whereClauseParts = [];
1805
+ for (const propName of allColumnsSet) {
1806
+ const colConfig = schema.columns[propName].build();
1807
+ const dbName = colConfig.name;
1808
+ const pgType = DbEntityTable.PG_TYPE_MAP[colConfig.type] || colConfig.type;
1809
+ const isPK = primaryKeySet.has(propName);
1810
+ const info = { propName, dbName, pgType, isPK };
1811
+ columnInfoList.push(info);
1812
+ // Build VALUES column list
1813
+ valueColumnParts.push(`"${dbName}"`);
1814
+ if (!isPK) {
1815
+ valueColumnParts.push(`"${dbName}__provided"`);
1816
+ // Build SET clause
1817
+ setClauses.push(`"${dbName}" = CASE WHEN v."${dbName}__provided" THEN v."${dbName}" ELSE t."${dbName}" END`);
1818
+ }
1819
+ else {
1820
+ // Build WHERE clause for PK
1821
+ whereClauseParts.push(`t."${dbName}" = v."${dbName}"`);
1263
1822
  }
1264
1823
  }
1265
- if (setClauses.length === 0) {
1266
- throw new Error('No valid columns to update');
1824
+ const valueColumnList = valueColumnParts.join(', ');
1825
+ const whereClause = whereClauseParts.join(' AND ');
1826
+ // Build VALUES clause with parameters - single pass over data
1827
+ const valuesClauses = [];
1828
+ const params = [];
1829
+ let paramIndex = 1;
1830
+ for (const record of data) {
1831
+ const rowValues = [];
1832
+ for (const col of columnInfoList) {
1833
+ const hasKey = col.propName in record;
1834
+ const value = record[col.propName];
1835
+ // Add the value (always cast to correct type for NULL to work in CASE expressions)
1836
+ if (value === undefined || value === null) {
1837
+ rowValues.push(`NULL::${col.pgType}`);
1838
+ }
1839
+ else {
1840
+ rowValues.push(`$${paramIndex++}::${col.pgType}`);
1841
+ params.push(value);
1842
+ }
1843
+ // Add the "was provided" flag for non-PK columns
1844
+ if (!col.isPK) {
1845
+ rowValues.push(hasKey ? 'true' : 'false');
1846
+ }
1847
+ }
1848
+ valuesClauses.push(`(${rowValues.join(', ')})`);
1267
1849
  }
1268
- // Build WHERE clause
1269
- const mockEntity = this.createMockEntity();
1270
- const whereCondition = condition(mockEntity);
1271
- const condBuilder = new conditions_1.ConditionBuilder();
1272
- const { sql: whereSql, params: whereParams } = condBuilder.build(whereCondition, paramIndex);
1273
- values.push(...whereParams);
1274
1850
  // Build RETURNING clause
1275
- const returningColumns = Object.entries(schema.columns)
1276
- .map(([_, col]) => `"${col.build().name}"`)
1277
- .join(', ');
1278
- const qualifiedTableName = this._getQualifiedTableName();
1279
- const sql = `
1280
- UPDATE ${qualifiedTableName}
1281
- SET ${setClauses.join(', ')}
1282
- WHERE ${whereSql}
1283
- RETURNING ${returningColumns}
1284
- `;
1851
+ const returningClause = this.buildReturningClause(returning, 't');
1852
+ let sql = `
1853
+ UPDATE ${qualifiedTableName} AS t
1854
+ SET ${setClauses.join(', ')}
1855
+ FROM (VALUES ${valuesClauses.join(', ')}) AS v(${valueColumnList})
1856
+ WHERE ${whereClause}`.trim();
1857
+ if (returningClause) {
1858
+ sql += ` RETURNING ${returningClause.sql}`;
1859
+ }
1285
1860
  const result = executor
1286
- ? await executor.query(sql, values)
1287
- : await client.query(sql, values);
1288
- return this.mapResultsToEntities(result.rows);
1861
+ ? await executor.query(sql, params)
1862
+ : await client.query(sql, params);
1863
+ if (!returningClause) {
1864
+ return undefined;
1865
+ }
1866
+ return this.mapReturningResults(result.rows, returning);
1289
1867
  }
1290
1868
  /**
1291
1869
  * Delete records matching condition
@@ -1349,8 +1927,111 @@ class DbEntityTable {
1349
1927
  }
1350
1928
  return mock;
1351
1929
  }
1930
+ /**
1931
+ * Build RETURNING clause SQL based on config
1932
+ * @internal
1933
+ */
1934
+ buildReturningClause(returning, tableAlias) {
1935
+ if (returning === undefined) {
1936
+ return null; // No RETURNING
1937
+ }
1938
+ const schema = this._getSchema();
1939
+ const prefix = tableAlias ? `${tableAlias}.` : '';
1940
+ if (returning === true) {
1941
+ // Return all columns
1942
+ const columns = Object.values(schema.columns).map(col => col.build().name);
1943
+ const sql = columns.map(name => `${prefix}"${name}"`).join(', ');
1944
+ return { sql, columns };
1945
+ }
1946
+ // Selector function - extract selected columns
1947
+ const mockEntity = this.createMockEntity();
1948
+ const selection = returning(mockEntity);
1949
+ if (typeof selection === 'object' && selection !== null) {
1950
+ const columns = [];
1951
+ const sqlParts = [];
1952
+ for (const [alias, field] of Object.entries(selection)) {
1953
+ if (field && typeof field === 'object' && '__dbColumnName' in field) {
1954
+ const dbName = field.__dbColumnName;
1955
+ columns.push(alias);
1956
+ sqlParts.push(`${prefix}"${dbName}" AS "${alias}"`);
1957
+ }
1958
+ }
1959
+ return { sql: sqlParts.join(', '), columns };
1960
+ }
1961
+ // Single field selection
1962
+ if (selection && typeof selection === 'object' && '__dbColumnName' in selection) {
1963
+ const dbName = selection.__dbColumnName;
1964
+ return { sql: `${prefix}"${dbName}"`, columns: [dbName] };
1965
+ }
1966
+ return null;
1967
+ }
1968
+ /**
1969
+ * Map row results based on returning config
1970
+ * @internal
1971
+ */
1972
+ mapReturningResults(rows, returning) {
1973
+ if (returning === true) {
1974
+ // Full entity mapping
1975
+ return this.mapResultsToEntities(rows);
1976
+ }
1977
+ // For selector functions, rows are already in the correct shape
1978
+ // Just apply any type mappers needed
1979
+ const schema = this._getSchema();
1980
+ return rows.map(row => {
1981
+ const mapped = {};
1982
+ for (const [key, value] of Object.entries(row)) {
1983
+ // Try to find column by alias or name
1984
+ const colEntry = Object.entries(schema.columns).find(([propName, col]) => {
1985
+ const config = col.build();
1986
+ return propName === key || config.name === key;
1987
+ });
1988
+ if (colEntry) {
1989
+ const [propName, col] = colEntry;
1990
+ const config = col.build();
1991
+ // Apply fromDriver mapper if present
1992
+ mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
1993
+ }
1994
+ else {
1995
+ mapped[key] = value;
1996
+ }
1997
+ }
1998
+ return mapped;
1999
+ });
2000
+ }
1352
2001
  }
1353
2002
  exports.DbEntityTable = DbEntityTable;
2003
+ /** Static type map for PostgreSQL type casting - computed once */
2004
+ DbEntityTable.PG_TYPE_MAP = {
2005
+ 'smallint': 'smallint',
2006
+ 'integer': 'integer',
2007
+ 'bigint': 'bigint',
2008
+ 'serial': 'integer',
2009
+ 'smallserial': 'smallint',
2010
+ 'bigserial': 'bigint',
2011
+ 'decimal': 'decimal',
2012
+ 'numeric': 'numeric',
2013
+ 'real': 'real',
2014
+ 'double precision': 'double precision',
2015
+ 'money': 'money',
2016
+ 'varchar': 'varchar',
2017
+ 'char': 'char',
2018
+ 'text': 'text',
2019
+ 'bytea': 'bytea',
2020
+ 'timestamp': 'timestamp',
2021
+ 'timestamptz': 'timestamptz',
2022
+ 'date': 'date',
2023
+ 'time': 'time',
2024
+ 'timetz': 'timetz',
2025
+ 'interval': 'interval',
2026
+ 'boolean': 'boolean',
2027
+ 'uuid': 'uuid',
2028
+ 'json': 'json',
2029
+ 'jsonb': 'jsonb',
2030
+ 'inet': 'inet',
2031
+ 'cidr': 'cidr',
2032
+ 'macaddr': 'macaddr',
2033
+ 'macaddr8': 'macaddr8',
2034
+ };
1354
2035
  /**
1355
2036
  * Base database context with entity-first approach
1356
2037
  */