linkgress-orm 0.4.56 → 0.4.58

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.
@@ -2491,7 +2491,7 @@ class SelectQueryBuilder {
2491
2491
  // Qualify columns with table name when using USING clause to avoid ambiguity
2492
2492
  const hasJoins = whereJoins.length > 0;
2493
2493
  const returningClause = returning !== 'count'
2494
- ? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins)
2494
+ ? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins, { paramCounter: whereParams.length + 1, params: whereParams })
2495
2495
  : undefined;
2496
2496
  let sql = `DELETE FROM ${qualifiedTableName}`;
2497
2497
  if (usingClause) {
@@ -2511,7 +2511,7 @@ class SelectQueryBuilder {
2511
2511
  if (!returningClause) {
2512
2512
  return undefined;
2513
2513
  }
2514
- return queryBuilder.mapDeleteReturningResults(result.rows, returning);
2514
+ return queryBuilder.mapDeleteReturningResults(result.rows, returning, returningClause.fragmentMappers);
2515
2515
  };
2516
2516
  return {
2517
2517
  then(onfulfilled, onrejected) {
@@ -2650,7 +2650,7 @@ class SelectQueryBuilder {
2650
2650
  // Qualify columns with table name when using FROM clause to avoid ambiguity
2651
2651
  const hasJoins = whereJoins.length > 0;
2652
2652
  const returningClause = returning !== 'count'
2653
- ? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins)
2653
+ ? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins, { paramCounter: values.length + 1, params: values })
2654
2654
  : undefined;
2655
2655
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')}`;
2656
2656
  if (fromClause) {
@@ -2670,7 +2670,77 @@ class SelectQueryBuilder {
2670
2670
  if (!returningClause) {
2671
2671
  return undefined;
2672
2672
  }
2673
- return queryBuilder.mapDeleteReturningResults(result.rows, returning);
2673
+ return queryBuilder.mapDeleteReturningResults(result.rows, returning, returningClause.fragmentMappers);
2674
+ };
2675
+ /**
2676
+ * Compile the UPDATE into `{ sql, params }` WITHOUT executing — the
2677
+ * standard-path assembly (same SET/WHERE/FROM semantics as execution,
2678
+ * SqlFragment values and fragment-capable RETURNING included; navigation
2679
+ * RETURNING is not supported here). Used to ride the statement as a
2680
+ * data-modifying CTE (`DbCteBuilder.withMutation`).
2681
+ */
2682
+ const compileUpdate = (returning) => {
2683
+ if (!queryBuilder.whereCond) {
2684
+ throw new Error('Update requires a WHERE condition. Use where() before update().');
2685
+ }
2686
+ const whereJoins = [];
2687
+ queryBuilder.detectAndAddJoinsFromCondition(queryBuilder.whereCond, whereJoins);
2688
+ const resolvedData = typeof data === 'function'
2689
+ ? data(queryBuilder._createMockRow())
2690
+ : data;
2691
+ const setClauses = [];
2692
+ const values = [];
2693
+ let paramIndex = 1;
2694
+ for (const [key, value] of Object.entries(resolvedData)) {
2695
+ const column = queryBuilder.schema.columns[key];
2696
+ if (column) {
2697
+ const config = column.build();
2698
+ if (value instanceof conditions_1.SqlFragment) {
2699
+ const sqlBuildContext = {
2700
+ paramCounter: paramIndex,
2701
+ params: values,
2702
+ };
2703
+ const fragmentSql = value.buildSql(sqlBuildContext);
2704
+ paramIndex = sqlBuildContext.paramCounter;
2705
+ setClauses.push(`"${config.name}" = ${fragmentSql}`);
2706
+ continue;
2707
+ }
2708
+ setClauses.push(`"${config.name}" = $${paramIndex++}`);
2709
+ values.push(config.mapper ? config.mapper.toDriver(value) : value);
2710
+ }
2711
+ }
2712
+ if (setClauses.length === 0) {
2713
+ throw new Error('No valid columns to update');
2714
+ }
2715
+ const condBuilder = new conditions_1.ConditionBuilder();
2716
+ const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
2717
+ values.push(...whereParams);
2718
+ const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
2719
+ let fromClause = '';
2720
+ const joinConditions = [];
2721
+ for (const join of whereJoins) {
2722
+ const sourceTable = join.sourceAlias || queryBuilder.schema.name;
2723
+ const joinTableName = queryBuilder.getQualifiedTableName(join.targetTable, join.targetSchema);
2724
+ fromClause = fromClause ? `${fromClause}, ${joinTableName} AS "${join.alias}"` : `FROM ${joinTableName} AS "${join.alias}"`;
2725
+ for (let i = 0; i < join.foreignKeys.length; i++) {
2726
+ joinConditions.push(`${(0, join_utils_1.formatJoinValue)(sourceTable, join.foreignKeys[i])} = ${(0, join_utils_1.formatJoinValue)(join.alias, join.matches[i])}`);
2727
+ }
2728
+ }
2729
+ const fullWhereClause = joinConditions.length > 0
2730
+ ? `${joinConditions.join(' AND ')} AND ${whereSql}`
2731
+ : whereSql;
2732
+ const returningClause = returning != null
2733
+ ? queryBuilder.buildUpdateDeleteReturningClause(returning, whereJoins.length > 0, { paramCounter: values.length + 1, params: values })
2734
+ : null;
2735
+ let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')}`;
2736
+ if (fromClause) {
2737
+ sql += ` ${fromClause}`;
2738
+ }
2739
+ sql += ` WHERE ${fullWhereClause}`;
2740
+ if (returningClause) {
2741
+ sql += ` RETURNING ${returningClause.sql}`;
2742
+ }
2743
+ return { sql, params: values };
2674
2744
  };
2675
2745
  return {
2676
2746
  then(onfulfilled, onrejected) {
@@ -2683,6 +2753,9 @@ class SelectQueryBuilder {
2683
2753
  }
2684
2754
  };
2685
2755
  },
2756
+ toStatement(selector) {
2757
+ return compileUpdate(selector);
2758
+ },
2686
2759
  returning(selector) {
2687
2760
  const returningConfig = selector ?? true;
2688
2761
  return {
@@ -2697,9 +2770,12 @@ class SelectQueryBuilder {
2697
2770
  * Build RETURNING clause for delete/update operations
2698
2771
  * @param returning - The returning configuration
2699
2772
  * @param qualifyWithTable - If true, qualify column names with the main table name (needed for DELETE with USING)
2773
+ * @param paramContext - Statement parameter state (params array + next $n). Required when the
2774
+ * selector contains SqlFragments — their parameters append here, which is
2775
+ * positionally correct because RETURNING is last in the statement text.
2700
2776
  * @internal
2701
2777
  */
2702
- buildUpdateDeleteReturningClause(returning, qualifyWithTable = false) {
2778
+ buildUpdateDeleteReturningClause(returning, qualifyWithTable = false, paramContext) {
2703
2779
  if (returning === undefined) {
2704
2780
  return null;
2705
2781
  }
@@ -2717,22 +2793,39 @@ class SelectQueryBuilder {
2717
2793
  if (typeof selection === 'object' && selection !== null) {
2718
2794
  const columns = [];
2719
2795
  const sqlParts = [];
2796
+ let fragmentMappers;
2720
2797
  for (const [alias, field] of Object.entries(selection)) {
2721
- if (field && typeof field === 'object' && '__dbColumnName' in field) {
2798
+ if (field instanceof conditions_1.SqlFragment) {
2799
+ // Raw fragment under its selector key (the key is the alias — a
2800
+ // fragment-side .as() is ignored here). Params append to the
2801
+ // statement's array; headline use case: PG18 `old."col"` capture.
2802
+ if (!paramContext) {
2803
+ throw new Error(`Returning selector field "${alias}" is a SqlFragment, which this mutation path does not support`);
2804
+ }
2805
+ const fragmentSql = field.buildSql(paramContext);
2806
+ columns.push(alias);
2807
+ sqlParts.push(`${fragmentSql} AS "${alias}"`);
2808
+ fragmentMappers = fragmentMappers ?? new Map();
2809
+ fragmentMappers.set(alias, field.getMapper());
2810
+ }
2811
+ else if (field && typeof field === 'object' && '__dbColumnName' in field) {
2722
2812
  const dbName = field.__dbColumnName;
2723
2813
  columns.push(alias);
2724
2814
  sqlParts.push(`${tablePrefix}"${dbName}" AS "${alias}"`);
2725
2815
  }
2726
2816
  }
2727
- return { sql: sqlParts.join(', '), columns };
2817
+ return { sql: sqlParts.join(', '), columns, fragmentMappers };
2728
2818
  }
2729
2819
  return null;
2730
2820
  }
2731
2821
  /**
2732
2822
  * Map row results for delete/update RETURNING clause
2823
+ * @param fragmentMappers - Per-alias mapWith mappers for SqlFragment selector fields; a
2824
+ * fragment alias bypasses the schema-column mapper scan entirely
2825
+ * (its value is the fragment's, not any column's).
2733
2826
  * @internal
2734
2827
  */
2735
- mapDeleteReturningResults(rows, returning) {
2828
+ mapDeleteReturningResults(rows, returning, fragmentMappers) {
2736
2829
  if (returning === true) {
2737
2830
  // Full entity mapping - apply fromDriver mappers
2738
2831
  return rows.map(row => {
@@ -2750,6 +2843,11 @@ class SelectQueryBuilder {
2750
2843
  return rows.map(row => {
2751
2844
  const mapped = {};
2752
2845
  for (const [key, value] of Object.entries(row)) {
2846
+ if (fragmentMappers?.has(key)) {
2847
+ const fragmentMapper = fragmentMappers.get(key);
2848
+ mapped[key] = fragmentMapper?.fromDriver ? fragmentMapper.fromDriver(value) : value;
2849
+ continue;
2850
+ }
2753
2851
  // Try to find column by alias or name
2754
2852
  const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
2755
2853
  const config = col.build();
@@ -2833,6 +2931,13 @@ class SelectQueryBuilder {
2833
2931
  allTableAliases.add(collectionBuilder.sourceTable);
2834
2932
  }
2835
2933
  }
2934
+ else if (field instanceof conditions_1.SqlFragment) {
2935
+ // Raw SQL fragment — rendered verbatim by the plain returning-clause
2936
+ // builder; it never implies navigation. Without this skip it would
2937
+ // fall into the nested-object branch and derail the whole selector
2938
+ // onto the CTE navigation path. (v1: FieldRefs inside returning
2939
+ // fragments are unsupported — reference columns as raw quoted SQL.)
2940
+ }
2836
2941
  else if (!Array.isArray(field)) {
2837
2942
  // Nested plain object - recurse into it
2838
2943
  nestedObjects.set(fieldPath, field);