linkgress-orm 0.1.17 → 0.1.19

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 (38) hide show
  1. package/dist/database/database-client.interface.d.ts +29 -0
  2. package/dist/database/database-client.interface.d.ts.map +1 -1
  3. package/dist/database/database-client.interface.js +45 -1
  4. package/dist/database/database-client.interface.js.map +1 -1
  5. package/dist/database/pg-client.d.ts +5 -0
  6. package/dist/database/pg-client.d.ts.map +1 -1
  7. package/dist/database/pg-client.js +27 -0
  8. package/dist/database/pg-client.js.map +1 -1
  9. package/dist/database/postgres-client.d.ts +6 -0
  10. package/dist/database/postgres-client.d.ts.map +1 -1
  11. package/dist/database/postgres-client.js +17 -0
  12. package/dist/database/postgres-client.js.map +1 -1
  13. package/dist/entity/db-column.d.ts +17 -0
  14. package/dist/entity/db-column.d.ts.map +1 -1
  15. package/dist/entity/db-column.js.map +1 -1
  16. package/dist/entity/db-context.d.ts +134 -25
  17. package/dist/entity/db-context.d.ts.map +1 -1
  18. package/dist/entity/db-context.js +237 -174
  19. package/dist/entity/db-context.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/query/cte-builder.d.ts +1 -1
  24. package/dist/query/cte-builder.js +8 -8
  25. package/dist/query/cte-builder.js.map +1 -1
  26. package/dist/query/grouped-query.d.ts +8 -0
  27. package/dist/query/grouped-query.d.ts.map +1 -1
  28. package/dist/query/grouped-query.js +12 -0
  29. package/dist/query/grouped-query.js.map +1 -1
  30. package/dist/query/query-builder.d.ts +13 -2
  31. package/dist/query/query-builder.d.ts.map +1 -1
  32. package/dist/query/query-builder.js +128 -42
  33. package/dist/query/query-builder.js.map +1 -1
  34. package/dist/query/sql-utils.d.ts +2 -0
  35. package/dist/query/sql-utils.d.ts.map +1 -1
  36. package/dist/query/sql-utils.js +24 -7
  37. package/dist/query/sql-utils.js.map +1 -1
  38. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DatabaseContext = exports.DbEntityTable = exports.EntityInsertBuilder = exports.DataContext = exports.TableAccessor = exports.InsertBuilder = exports.QueryExecutor = exports.TimeTracer = void 0;
4
+ const database_client_interface_1 = require("../database/database-client.interface");
4
5
  const entity_base_1 = require("./entity-base");
5
6
  const model_config_1 = require("./model-config");
6
7
  const query_builder_1 = require("../query/query-builder");
@@ -528,15 +529,21 @@ class TableAccessor {
528
529
  const column = this.schema.columns[key];
529
530
  if (column) {
530
531
  const config = column.build();
531
- if (!config.autoIncrement) {
532
- columns.push(`"${config.name}"`);
533
- // Apply toDriver mapper if present
534
- const mappedValue = config.mapper
535
- ? config.mapper.toDriver(value)
536
- : value;
537
- values.push(mappedValue);
538
- placeholders.push(`$${paramIndex++}`);
532
+ // Skip auto-increment columns
533
+ if (config.autoIncrement) {
534
+ continue;
535
+ }
536
+ // Skip columns with undefined values if they have a default - let the DB use the default
537
+ if (value === undefined && (config.default !== undefined || config.identity)) {
538
+ continue;
539
539
  }
540
+ columns.push(`"${config.name}"`);
541
+ // Apply toDriver mapper if present
542
+ const mappedValue = config.mapper
543
+ ? config.mapper.toDriver(value)
544
+ : value;
545
+ values.push(mappedValue);
546
+ placeholders.push(`$${paramIndex++}`);
540
547
  }
541
548
  }
542
549
  const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
@@ -600,7 +607,12 @@ class TableAccessor {
600
607
  sql += '\n OVERRIDING SYSTEM VALUE';
601
608
  }
602
609
  sql += `
603
- VALUES ${valueClauses.join(', ')}
610
+ VALUES ${valueClauses.join(', ')}`;
611
+ // Add ON CONFLICT DO NOTHING if specified
612
+ if (insertConfig?.onConflictDoNothing) {
613
+ sql += '\n ON CONFLICT DO NOTHING';
614
+ }
615
+ sql += `
604
616
  RETURNING ${returningColumns}
605
617
  `;
606
618
  const result = this.executor
@@ -663,65 +675,6 @@ class TableAccessor {
663
675
  }
664
676
  return builder.execute();
665
677
  }
666
- /**
667
- * Bulk insert multiple rows (simple version, kept for compatibility)
668
- */
669
- async insertMany(dataArray) {
670
- if (dataArray.length === 0) {
671
- return [];
672
- }
673
- // Extract all unique column names from all data objects
674
- const columnSet = new Set();
675
- for (const data of dataArray) {
676
- for (const key of Object.keys(data)) {
677
- const column = this.schema.columns[key];
678
- if (column) {
679
- const config = column.build();
680
- if (!config.autoIncrement) {
681
- columnSet.add(key);
682
- }
683
- }
684
- }
685
- }
686
- const columns = Array.from(columnSet);
687
- const values = [];
688
- const valuePlaceholders = [];
689
- let paramIndex = 1;
690
- // Build placeholders for each row
691
- for (const data of dataArray) {
692
- const rowPlaceholders = [];
693
- for (const key of columns) {
694
- const value = data[key];
695
- const column = this.schema.columns[key];
696
- const config = column.build();
697
- // Apply toDriver mapper if present
698
- const mappedValue = config.mapper
699
- ? config.mapper.toDriver(value !== undefined ? value : null)
700
- : (value !== undefined ? value : null);
701
- values.push(mappedValue);
702
- rowPlaceholders.push(`$${paramIndex++}`);
703
- }
704
- valuePlaceholders.push(`(${rowPlaceholders.join(', ')})`);
705
- }
706
- const columnNames = columns.map(key => {
707
- const column = this.schema.columns[key];
708
- const config = column.build();
709
- return `"${config.name}"`;
710
- });
711
- const returningColumns = Object.entries(this.schema.columns)
712
- .map(([_, col]) => `"${col.build().name}"`)
713
- .join(', ');
714
- const qualifiedTableName = (0, sql_utils_1.getQualifiedTableName)(this.schema);
715
- const sql = `
716
- INSERT INTO ${qualifiedTableName} (${columnNames.join(', ')})
717
- VALUES ${valuePlaceholders.join(', ')}
718
- RETURNING ${returningColumns}
719
- `;
720
- const result = this.executor
721
- ? await this.executor.query(sql, values)
722
- : await this.client.query(sql, values);
723
- return result.rows;
724
- }
725
678
  /**
726
679
  * Insert with conflict resolution (upsert)
727
680
  */
@@ -846,13 +799,18 @@ class DataContext {
846
799
  }
847
800
  /**
848
801
  * Get table accessor by name
802
+ * When in a transaction, creates a fresh accessor with the transactional client
849
803
  */
850
804
  getTable(name) {
851
- const accessor = this.tableAccessors.get(name);
852
- if (!accessor) {
805
+ const cachedAccessor = this.tableAccessors.get(name);
806
+ if (!cachedAccessor) {
853
807
  throw new Error(`Table ${String(name)} not found in schema`);
854
808
  }
855
- return accessor;
809
+ // If in a transaction, create a new accessor with the current (transactional) client
810
+ if (this.client.isInTransaction()) {
811
+ return new TableAccessor(cachedAccessor.tableBuilder, this.client, this.schemaRegistry, this.executor, this.queryOptions?.collectionStrategy);
812
+ }
813
+ return cachedAccessor;
856
814
  }
857
815
  /**
858
816
  * Execute raw SQL query with optional type parameter for results
@@ -875,22 +833,25 @@ class DataContext {
875
833
  }
876
834
  /**
877
835
  * Execute in transaction
836
+ * Uses the database client's native transaction support for proper handling
837
+ * across different drivers (pg uses BEGIN/COMMIT, postgres uses sql.begin())
878
838
  */
879
839
  async transaction(fn) {
880
- const connection = await this.client.connect();
881
- try {
882
- await connection.query('BEGIN');
883
- const result = await fn(this);
884
- await connection.query('COMMIT');
885
- return result;
886
- }
887
- catch (error) {
888
- await connection.query('ROLLBACK');
889
- throw error;
890
- }
891
- finally {
892
- connection.release();
893
- }
840
+ // Store the original client
841
+ const originalClient = this.client;
842
+ return await this.client.transaction(async (queryFn) => {
843
+ // Create a transactional client that routes all queries through the transaction
844
+ const txClient = new database_client_interface_1.TransactionalClient(queryFn, originalClient);
845
+ // Temporarily swap the client to use the transactional one
846
+ this.client = txClient;
847
+ try {
848
+ return await fn(this);
849
+ }
850
+ finally {
851
+ // Restore the original client
852
+ this.client = originalClient;
853
+ }
854
+ });
894
855
  }
895
856
  /**
896
857
  * Get schema manager for create/drop operations and automatic migrations
@@ -984,6 +945,119 @@ class DbEntityTable {
984
945
  _getCollectionStrategy() {
985
946
  return this.context.queryOptions?.collectionStrategy;
986
947
  }
948
+ /**
949
+ * Get information about all columns in this table.
950
+ * This method returns metadata about database columns only, excluding navigation properties.
951
+ *
952
+ * @param options - Optional configuration
953
+ * @param options.includeNavigation - If false (default), only returns database columns.
954
+ * Navigation properties are never included as they are not columns.
955
+ *
956
+ * @returns Array of column information objects
957
+ *
958
+ * @example
959
+ * ```typescript
960
+ * // Get all columns
961
+ * const columns = db.users.getColumns();
962
+ * // Returns: [
963
+ * // { propertyName: 'id', columnName: 'id', type: 'integer', isPrimaryKey: true, ... },
964
+ * // { propertyName: 'username', columnName: 'username', type: 'varchar', ... },
965
+ * // { propertyName: 'email', columnName: 'email', type: 'text', ... },
966
+ * // ]
967
+ *
968
+ * // Get column names only
969
+ * const columnNames = db.users.getColumns().map(c => c.propertyName);
970
+ * // Returns: ['id', 'username', 'email', ...]
971
+ *
972
+ * // Get database column names
973
+ * const dbColumnNames = db.users.getColumns().map(c => c.columnName);
974
+ * // Returns: ['id', 'username', 'email', ...]
975
+ * ```
976
+ */
977
+ getColumns() {
978
+ const schema = this._getSchema();
979
+ const columns = [];
980
+ for (const [propName, colBuilder] of Object.entries(schema.columns)) {
981
+ const config = colBuilder.build();
982
+ columns.push({
983
+ propertyName: propName,
984
+ columnName: config.name,
985
+ type: config.type,
986
+ isPrimaryKey: config.primaryKey || false,
987
+ isAutoIncrement: config.autoIncrement || !!config.identity,
988
+ isNullable: config.nullable !== false,
989
+ isUnique: config.unique || false,
990
+ defaultValue: config.default,
991
+ });
992
+ }
993
+ return columns;
994
+ }
995
+ /**
996
+ * Get an array of all column property names (keys) for this entity.
997
+ * Returns a strongly typed array where each element is a valid column key of TEntity.
998
+ *
999
+ * This is a convenience method equivalent to `getColumns().map(c => c.propertyName)`
1000
+ * but with better type inference.
1001
+ *
1002
+ * @returns Array of column property names typed as ExtractDbColumnKeys<TEntity>
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * // Get all column keys
1007
+ * const keys = db.users.getColumnKeys();
1008
+ * // Type: ExtractDbColumnKeys<User>[] which is ('id' | 'username' | 'email' | ...)[]
1009
+ *
1010
+ * // Use for dynamic property access
1011
+ * const user = await db.users.findOne(u => eq(u.id, 1));
1012
+ * for (const key of db.users.getColumnKeys()) {
1013
+ * console.log(`${key}: ${user[key]}`); // TypeScript knows key is valid
1014
+ * }
1015
+ *
1016
+ * // Use for building dynamic queries
1017
+ * const columnKeys = db.users.getColumnKeys();
1018
+ * // columnKeys[0] is typed as 'id' | 'username' | 'email' | ...
1019
+ * ```
1020
+ */
1021
+ getColumnKeys() {
1022
+ const schema = this._getSchema();
1023
+ return Object.keys(schema.columns);
1024
+ }
1025
+ /**
1026
+ * Get an object containing all entity properties as DbColumn references.
1027
+ * Useful for building dynamic queries or accessing column metadata.
1028
+ *
1029
+ * @param options - Optional configuration
1030
+ * @param options.excludeNavigation - If true (default), excludes navigation properties.
1031
+ * Set to false to include navigation properties.
1032
+ *
1033
+ * @returns Object with property names as keys and their DbColumn/navigation references as values
1034
+ *
1035
+ * @example
1036
+ * ```typescript
1037
+ * // Get all column properties (excludes navigation by default)
1038
+ * const cols = db.users.props();
1039
+ * // Use in select: db.users.select(u => ({ id: cols.id, name: cols.username }))
1040
+ *
1041
+ * // Include navigation properties
1042
+ * const allProps = db.users.props({ excludeNavigation: false });
1043
+ * ```
1044
+ */
1045
+ props(options) {
1046
+ const schema = this._getSchema();
1047
+ const excludeNav = options?.excludeNavigation !== false; // Default true
1048
+ // Create a temporary QueryBuilder to reuse the createMockRow logic
1049
+ const qb = new query_builder_1.QueryBuilder(schema, this._getClient(), undefined, undefined, undefined, undefined, this._getExecutor(), undefined, undefined, this._getCollectionStrategy(), this.context.schemaRegistry);
1050
+ const mockRow = qb._createMockRow();
1051
+ if (excludeNav) {
1052
+ // Filter out navigation properties, keep only columns
1053
+ const result = {};
1054
+ for (const propName of Object.keys(schema.columns)) {
1055
+ result[propName] = mockRow[propName];
1056
+ }
1057
+ return result;
1058
+ }
1059
+ return mockRow;
1060
+ }
987
1061
  /**
988
1062
  * Get qualified table name with schema prefix if specified
989
1063
  * @internal
@@ -1196,21 +1270,20 @@ class DbEntityTable {
1196
1270
  */
1197
1271
  where(condition) {
1198
1272
  const schema = this._getSchema();
1199
- // Create a selector that selects all columns with navigation property access
1273
+ // Create a selector that selects all columns only (not navigation properties)
1200
1274
  const allColumnsSelector = (e) => {
1201
1275
  const result = {};
1202
1276
  // Copy all column properties
1203
1277
  for (const colName of Object.keys(schema.columns)) {
1204
1278
  result[colName] = e[colName];
1205
1279
  }
1206
- // Add navigation properties as enumerable getters
1207
- // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1208
- // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1209
- // When the selection is analyzed, the builder is detected and handled appropriately
1280
+ // Add navigation properties as non-enumerable getters
1281
+ // This allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1282
+ // but they won't be included in the default query output
1210
1283
  for (const relName of Object.keys(schema.relations)) {
1211
1284
  Object.defineProperty(result, relName, {
1212
1285
  get: () => e[relName],
1213
- enumerable: true, // Enumerable so it's included in Object.entries
1286
+ enumerable: false, // Non-enumerable so it's NOT included in default selection
1214
1287
  configurable: true,
1215
1288
  });
1216
1289
  }
@@ -1226,21 +1299,20 @@ class DbEntityTable {
1226
1299
  */
1227
1300
  with(...ctes) {
1228
1301
  const schema = this._getSchema();
1229
- // Create a selector that selects all columns with navigation property access
1302
+ // Create a selector that selects all columns only (not navigation properties)
1230
1303
  const allColumnsSelector = (e) => {
1231
1304
  const result = {};
1232
1305
  // Copy all column properties
1233
1306
  for (const colName of Object.keys(schema.columns)) {
1234
1307
  result[colName] = e[colName];
1235
1308
  }
1236
- // Add navigation properties as enumerable getters
1237
- // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1238
- // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1239
- // When the selection is analyzed, the builder is detected and handled appropriately
1309
+ // Add navigation properties as non-enumerable getters
1310
+ // This allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1311
+ // but they won't be included in the default query output
1240
1312
  for (const relName of Object.keys(schema.relations)) {
1241
1313
  Object.defineProperty(result, relName, {
1242
1314
  get: () => e[relName],
1243
- enumerable: true, // Enumerable so it's included in Object.entries
1315
+ enumerable: false, // Non-enumerable so it's NOT included in default selection
1244
1316
  configurable: true,
1245
1317
  });
1246
1318
  }
@@ -1276,77 +1348,22 @@ class DbEntityTable {
1276
1348
  * ```
1277
1349
  */
1278
1350
  insert(data) {
1279
- const table = this;
1280
- const executeInsert = async (returning) => {
1281
- const schema = table._getSchema();
1282
- const executor = table._getExecutor();
1283
- const client = table._getClient();
1284
- // Build INSERT columns and values
1285
- const columns = [];
1286
- const values = [];
1287
- const placeholders = [];
1288
- let paramIndex = 1;
1289
- for (const [key, value] of Object.entries(data)) {
1290
- const column = schema.columns[key];
1291
- if (column) {
1292
- const config = column.build();
1293
- if (!config.autoIncrement) {
1294
- columns.push(`"${config.name}"`);
1295
- const mappedValue = config.mapper ? config.mapper.toDriver(value) : value;
1296
- values.push(mappedValue);
1297
- placeholders.push(`$${paramIndex++}`);
1298
- }
1299
- }
1300
- }
1301
- // Build RETURNING clause
1302
- const returningClause = table.buildReturningClause(returning);
1303
- const qualifiedTableName = table._getQualifiedTableName();
1304
- let sql = `INSERT INTO ${qualifiedTableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
1305
- if (returningClause) {
1306
- sql += ` RETURNING ${returningClause.sql}`;
1307
- }
1308
- const result = executor
1309
- ? await executor.query(sql, values)
1310
- : await client.query(sql, values);
1311
- if (!returningClause) {
1312
- return undefined;
1313
- }
1314
- const mappedResults = table.mapReturningResults(result.rows, returning);
1315
- return mappedResults[0];
1316
- };
1351
+ const bulkBuilder = this.insertBulk([data]);
1317
1352
  return {
1318
1353
  then(onfulfilled, onrejected) {
1319
- return executeInsert(undefined).then(onfulfilled, onrejected);
1354
+ return bulkBuilder.then(onfulfilled, onrejected);
1320
1355
  },
1321
1356
  returning(selector) {
1322
- const returningConfig = selector ?? true;
1357
+ const bulkReturning = selector ? bulkBuilder.returning(selector) : bulkBuilder.returning();
1323
1358
  return {
1324
1359
  then(onfulfilled, onrejected) {
1325
- return executeInsert(returningConfig).then(onfulfilled, onrejected);
1360
+ // Unwrap array to single item
1361
+ return bulkReturning.then((results) => results?.[0], undefined).then(onfulfilled, onrejected);
1326
1362
  }
1327
1363
  };
1328
1364
  }
1329
1365
  };
1330
1366
  }
1331
- /**
1332
- * Insert multiple records
1333
- * Returns a fluent builder that can be awaited directly or chained with .returning()
1334
- *
1335
- * @example
1336
- * ```typescript
1337
- * // No returning (default) - returns void
1338
- * await db.users.insertMany([{ username: 'alice' }, { username: 'bob' }]);
1339
- *
1340
- * // With returning() - returns full entities
1341
- * const users = await db.users.insertMany([{ username: 'alice' }]).returning();
1342
- *
1343
- * // With returning(selector) - returns selected columns
1344
- * const results = await db.users.insertMany([{ username: 'alice' }]).returning(u => ({ id: u.id }));
1345
- * ```
1346
- */
1347
- insertMany(data) {
1348
- return this.insertBulk(data);
1349
- }
1350
1367
  /**
1351
1368
  * Upsert (insert or update on conflict)
1352
1369
  * Returns a fluent builder that can be awaited directly or chained with .returning()
@@ -1379,8 +1396,11 @@ class DbEntityTable {
1379
1396
  * // With returning(selector) - returns selected columns
1380
1397
  * const results = await db.users.insertBulk([{ username: 'alice' }]).returning(u => ({ id: u.id }));
1381
1398
  *
1382
- * // With options
1399
+ * // With chunk size
1383
1400
  * await db.users.insertBulk([{ username: 'alice' }], { chunkSize: 100 });
1401
+ *
1402
+ * // Skip duplicates (ON CONFLICT DO NOTHING)
1403
+ * await db.users.insertBulk([{ username: 'alice' }], { onConflictDoNothing: true });
1384
1404
  * ```
1385
1405
  */
1386
1406
  insertBulk(value, options) {
@@ -1403,13 +1423,13 @@ class DbEntityTable {
1403
1423
  const allResults = [];
1404
1424
  for (let i = 0; i < dataArray.length; i += chunkSize) {
1405
1425
  const chunk = dataArray.slice(i, i + chunkSize);
1406
- const chunkResults = await table.insertBulkSingle(chunk, returning, options?.overridingSystemValue);
1426
+ const chunkResults = await table.insertBulkSingle(chunk, returning, options?.overridingSystemValue, options?.onConflictDoNothing);
1407
1427
  if (chunkResults)
1408
1428
  allResults.push(...chunkResults);
1409
1429
  }
1410
1430
  return returning === undefined ? undefined : allResults;
1411
1431
  }
1412
- return table.insertBulkSingle(dataArray, returning, options?.overridingSystemValue);
1432
+ return table.insertBulkSingle(dataArray, returning, options?.overridingSystemValue, options?.onConflictDoNothing);
1413
1433
  };
1414
1434
  return {
1415
1435
  then(onfulfilled, onrejected) {
@@ -1429,25 +1449,42 @@ class DbEntityTable {
1429
1449
  * Execute a single bulk insert batch
1430
1450
  * @internal
1431
1451
  */
1432
- async insertBulkSingle(data, returning, overridingSystemValue) {
1452
+ async insertBulkSingle(data, returning, overridingSystemValue, onConflictDoNothing) {
1433
1453
  const schema = this._getSchema();
1434
1454
  const executor = this._getExecutor();
1435
1455
  const client = this._getClient();
1436
1456
  const qualifiedTableName = this._getQualifiedTableName();
1437
- // Get columns from first row
1438
- const referenceItem = data[0];
1457
+ // Get columns from all rows - a column is included if ANY row has a non-undefined value for it
1439
1458
  const columnConfigs = [];
1440
1459
  for (const [propName, colBuilder] of Object.entries(schema.columns)) {
1441
- if (propName in referenceItem) {
1442
- const config = colBuilder.build();
1443
- if (!config.autoIncrement || overridingSystemValue) {
1444
- columnConfigs.push({
1445
- propName,
1446
- dbName: config.name,
1447
- mapper: config.mapper,
1448
- });
1460
+ const config = colBuilder.build();
1461
+ // Skip auto-increment columns (unless overriding)
1462
+ if (config.autoIncrement && !overridingSystemValue) {
1463
+ continue;
1464
+ }
1465
+ // Check if any row has a defined (non-undefined) value for this column
1466
+ const hasDefinedValue = data.some(record => {
1467
+ const value = record[propName];
1468
+ return value !== undefined;
1469
+ });
1470
+ // If column has a default and ALL rows have undefined, skip it (let DB use default)
1471
+ // Otherwise, include it if at least one row has a defined value
1472
+ if (!hasDefinedValue) {
1473
+ // All rows have undefined - if there's a default, skip the column
1474
+ if (config.default !== undefined || config.identity) {
1475
+ continue;
1476
+ }
1477
+ // No default, but column is present with undefined in data - include it (will become NULL)
1478
+ const isPresentInAnyRow = data.some(record => propName in record);
1479
+ if (!isPresentInAnyRow) {
1480
+ continue;
1449
1481
  }
1450
1482
  }
1483
+ columnConfigs.push({
1484
+ propName,
1485
+ dbName: config.name,
1486
+ mapper: config.mapper,
1487
+ });
1451
1488
  }
1452
1489
  // Build VALUES clauses
1453
1490
  const valuesClauses = [];
@@ -1472,6 +1509,9 @@ class DbEntityTable {
1472
1509
  sql += ' OVERRIDING SYSTEM VALUE';
1473
1510
  }
1474
1511
  sql += ` VALUES ${valuesClauses.join(', ')}`;
1512
+ if (onConflictDoNothing) {
1513
+ sql += ' ON CONFLICT DO NOTHING';
1514
+ }
1475
1515
  if (returningClause) {
1476
1516
  sql += ` RETURNING ${returningClause.sql}`;
1477
1517
  }
@@ -1772,8 +1812,10 @@ class DbEntityTable {
1772
1812
  throw new Error('No valid columns to update');
1773
1813
  }
1774
1814
  // No WHERE clause - updates all records
1775
- // Build RETURNING clause
1776
- const returningClause = table.buildReturningClause(returning);
1815
+ // Build RETURNING clause (not needed for count-only)
1816
+ const returningClause = returning !== 'count'
1817
+ ? table.buildReturningClause(returning)
1818
+ : undefined;
1777
1819
  const qualifiedTableName = table._getQualifiedTableName();
1778
1820
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')}`;
1779
1821
  if (returningClause) {
@@ -1782,6 +1824,10 @@ class DbEntityTable {
1782
1824
  const result = executor
1783
1825
  ? await executor.query(sql, values)
1784
1826
  : await client.query(sql, values);
1827
+ // Return affected count
1828
+ if (returning === 'count') {
1829
+ return result.rowCount ?? 0;
1830
+ }
1785
1831
  if (!returningClause) {
1786
1832
  return undefined;
1787
1833
  }
@@ -1791,6 +1837,13 @@ class DbEntityTable {
1791
1837
  then(onfulfilled, onrejected) {
1792
1838
  return executeUpdate(undefined).then(onfulfilled, onrejected);
1793
1839
  },
1840
+ affectedCount() {
1841
+ return {
1842
+ then(onfulfilled, onrejected) {
1843
+ return executeUpdate('count').then(onfulfilled, onrejected);
1844
+ }
1845
+ };
1846
+ },
1794
1847
  returning(selector) {
1795
1848
  const returningConfig = selector ?? true;
1796
1849
  return {
@@ -2011,11 +2064,14 @@ WHERE ${whereClause}`.trim();
2011
2064
  const baseSql = `DELETE FROM ${qualifiedTableName}`;
2012
2065
  const table = this;
2013
2066
  const executeDelete = async (returningConfig) => {
2014
- if (!returningConfig) {
2067
+ if (!returningConfig || returningConfig === 'count') {
2015
2068
  const sql = baseSql;
2016
2069
  const result = executor
2017
2070
  ? await executor.query(sql, [])
2018
2071
  : await client.query(sql, []);
2072
+ if (returningConfig === 'count') {
2073
+ return result.rowCount ?? 0;
2074
+ }
2019
2075
  return;
2020
2076
  }
2021
2077
  // Build RETURNING clause
@@ -2032,6 +2088,13 @@ WHERE ${whereClause}`.trim();
2032
2088
  then: (resolve, reject) => {
2033
2089
  return executeDelete().then(resolve, reject);
2034
2090
  },
2091
+ affectedCount: () => {
2092
+ return {
2093
+ then: (resolve, reject) => {
2094
+ return executeDelete('count').then(resolve, reject);
2095
+ }
2096
+ };
2097
+ },
2035
2098
  returning: ((selector) => {
2036
2099
  const returningConfig = selector ?? true;
2037
2100
  return {