linkgress-orm 0.2.4 → 0.2.5

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.
@@ -1728,14 +1728,63 @@ class SelectQueryBuilder {
1728
1728
  if (!queryBuilder.whereCond) {
1729
1729
  throw new Error('Delete requires a WHERE condition. Use where() before delete().');
1730
1730
  }
1731
+ // Detect navigation property joins from WHERE condition
1732
+ const whereJoins = [];
1733
+ queryBuilder.detectAndAddJoinsFromCondition(queryBuilder.whereCond, whereJoins);
1731
1734
  const condBuilder = new conditions_1.ConditionBuilder();
1732
1735
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
1733
- // Build RETURNING clause (not needed for count-only)
1736
+ const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1737
+ // Build USING clause for navigation properties (PostgreSQL syntax for DELETE with JOINs)
1738
+ let usingClause = '';
1739
+ let joinConditions = [];
1740
+ for (const join of whereJoins) {
1741
+ const sourceTable = join.sourceAlias || queryBuilder.schema.name;
1742
+ const joinTableName = queryBuilder.getQualifiedTableName(join.targetTable, join.targetSchema);
1743
+ if (usingClause) {
1744
+ usingClause += `, ${joinTableName} AS "${join.alias}"`;
1745
+ }
1746
+ else {
1747
+ usingClause = `USING ${joinTableName} AS "${join.alias}"`;
1748
+ }
1749
+ // Build ON conditions as part of WHERE clause
1750
+ for (let i = 0; i < join.foreignKeys.length; i++) {
1751
+ const fk = join.foreignKeys[i];
1752
+ const match = join.matches[i];
1753
+ joinConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
1754
+ }
1755
+ }
1756
+ // Combine join conditions with the original WHERE clause
1757
+ const fullWhereClause = joinConditions.length > 0
1758
+ ? `${joinConditions.join(' AND ')} AND ${whereSql}`
1759
+ : whereSql;
1760
+ // Check if RETURNING uses navigation properties
1761
+ const navigationInfo = returning && returning !== 'count' && returning !== true
1762
+ ? queryBuilder.detectNavigationInReturning(returning)
1763
+ : null;
1764
+ if (navigationInfo) {
1765
+ // Use CTE-based approach for navigation properties in RETURNING
1766
+ let deleteSql = `DELETE FROM ${qualifiedTableName}`;
1767
+ if (usingClause) {
1768
+ deleteSql += ` ${usingClause}`;
1769
+ }
1770
+ deleteSql += ` WHERE ${fullWhereClause}`;
1771
+ const { sql, params } = queryBuilder.buildReturningWithNavigation(deleteSql, whereParams, returning, navigationInfo);
1772
+ const result = queryBuilder.executor
1773
+ ? await queryBuilder.executor.query(sql, params)
1774
+ : await queryBuilder.client.query(sql, params);
1775
+ return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
1776
+ }
1777
+ // Standard RETURNING (no navigation properties)
1778
+ // Qualify columns with table name when using USING clause to avoid ambiguity
1779
+ const hasJoins = whereJoins.length > 0;
1734
1780
  const returningClause = returning !== 'count'
1735
- ? queryBuilder.buildDeleteReturningClause(returning)
1781
+ ? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins)
1736
1782
  : undefined;
1737
- const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1738
- let sql = `DELETE FROM ${qualifiedTableName} WHERE ${whereSql}`;
1783
+ let sql = `DELETE FROM ${qualifiedTableName}`;
1784
+ if (usingClause) {
1785
+ sql += ` ${usingClause}`;
1786
+ }
1787
+ sql += ` WHERE ${fullWhereClause}`;
1739
1788
  if (returningClause) {
1740
1789
  sql += ` RETURNING ${returningClause.sql}`;
1741
1790
  }
@@ -1820,11 +1869,24 @@ class SelectQueryBuilder {
1820
1869
  const condBuilder = new conditions_1.ConditionBuilder();
1821
1870
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
1822
1871
  values.push(...whereParams);
1823
- // Build RETURNING clause (not needed for count-only)
1872
+ const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1873
+ // Check if RETURNING uses navigation properties
1874
+ const navigationInfo = returning && returning !== 'count' && returning !== true
1875
+ ? queryBuilder.detectNavigationInReturning(returning)
1876
+ : null;
1877
+ if (navigationInfo) {
1878
+ // Use CTE-based approach for navigation properties in RETURNING
1879
+ const updateSql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
1880
+ const { sql, params } = queryBuilder.buildReturningWithNavigation(updateSql, values, returning, navigationInfo);
1881
+ const result = queryBuilder.executor
1882
+ ? await queryBuilder.executor.query(sql, params)
1883
+ : await queryBuilder.client.query(sql, params);
1884
+ return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
1885
+ }
1886
+ // Standard RETURNING (no navigation properties)
1824
1887
  const returningClause = returning !== 'count'
1825
- ? queryBuilder.buildDeleteReturningClause(returning)
1888
+ ? queryBuilder.buildUpdateDeleteReturningClause(returning)
1826
1889
  : undefined;
1827
- const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1828
1890
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
1829
1891
  if (returningClause) {
1830
1892
  sql += ` RETURNING ${returningClause.sql}`;
@@ -1864,16 +1926,19 @@ class SelectQueryBuilder {
1864
1926
  }
1865
1927
  /**
1866
1928
  * Build RETURNING clause for delete/update operations
1929
+ * @param returning - The returning configuration
1930
+ * @param qualifyWithTable - If true, qualify column names with the main table name (needed for DELETE with USING)
1867
1931
  * @internal
1868
1932
  */
1869
- buildDeleteReturningClause(returning) {
1933
+ buildUpdateDeleteReturningClause(returning, qualifyWithTable = false) {
1870
1934
  if (returning === undefined) {
1871
1935
  return null;
1872
1936
  }
1937
+ const tablePrefix = qualifyWithTable ? `"${this.schema.name}".` : '';
1873
1938
  if (returning === true) {
1874
1939
  // Return all columns
1875
1940
  const columns = Object.values(this.schema.columns).map(col => col.build().name);
1876
- const sql = columns.map(name => `"${name}"`).join(', ');
1941
+ const sql = columns.map(name => `${tablePrefix}"${name}"`).join(', ');
1877
1942
  return { sql, columns };
1878
1943
  }
1879
1944
  // Selector function - extract selected columns
@@ -1887,7 +1952,7 @@ class SelectQueryBuilder {
1887
1952
  if (field && typeof field === 'object' && '__dbColumnName' in field) {
1888
1953
  const dbName = field.__dbColumnName;
1889
1954
  columns.push(alias);
1890
- sqlParts.push(`"${dbName}" AS "${alias}"`);
1955
+ sqlParts.push(`${tablePrefix}"${dbName}" AS "${alias}"`);
1891
1956
  }
1892
1957
  }
1893
1958
  return { sql: sqlParts.join(', '), columns };
@@ -1934,6 +1999,197 @@ class SelectQueryBuilder {
1934
1999
  return mapped;
1935
2000
  });
1936
2001
  }
2002
+ /**
2003
+ * Detect if a RETURNING selector uses navigation properties
2004
+ * Returns navigation info if found, null otherwise
2005
+ * @internal
2006
+ */
2007
+ detectNavigationInReturning(returning) {
2008
+ if (returning === true) {
2009
+ // Full entity returning doesn't need navigation support
2010
+ return null;
2011
+ }
2012
+ // Analyze the returning selector
2013
+ const mockRow = this._createMockRow();
2014
+ const selectedMock = this.selector(mockRow);
2015
+ const selection = returning(selectedMock);
2016
+ if (typeof selection !== 'object' || selection === null) {
2017
+ return null;
2018
+ }
2019
+ const navigationFields = new Map();
2020
+ const allTableAliases = new Set();
2021
+ // Check each field in the selection for navigation properties
2022
+ for (const [alias, field] of Object.entries(selection)) {
2023
+ if (field && typeof field === 'object' && '__dbColumnName' in field && '__tableAlias' in field) {
2024
+ const tableAlias = field.__tableAlias;
2025
+ if (tableAlias && tableAlias !== this.schema.name) {
2026
+ allTableAliases.add(tableAlias);
2027
+ navigationFields.set(alias, {
2028
+ tableAlias,
2029
+ dbColumnName: field.__dbColumnName,
2030
+ schemaTable: field.__sourceTable,
2031
+ });
2032
+ }
2033
+ // Also collect intermediate navigation aliases for multi-level navigation
2034
+ if ('__navigationAliases' in field && Array.isArray(field.__navigationAliases)) {
2035
+ for (const navAlias of field.__navigationAliases) {
2036
+ if (navAlias && navAlias !== this.schema.name) {
2037
+ allTableAliases.add(navAlias);
2038
+ }
2039
+ }
2040
+ }
2041
+ }
2042
+ }
2043
+ if (navigationFields.size === 0) {
2044
+ return null;
2045
+ }
2046
+ // Resolve navigation joins
2047
+ const joins = [];
2048
+ this.resolveJoinsForTableAliases(allTableAliases, joins);
2049
+ return { hasNavigation: true, selection, joins, navigationFields };
2050
+ }
2051
+ /**
2052
+ * Build RETURNING clause with navigation property support using CTE
2053
+ * @internal
2054
+ */
2055
+ buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo) {
2056
+ // Build the CTE wrapping the mutation
2057
+ // First, collect all columns needed from the main table in the mutation's RETURNING
2058
+ const mainTableColumns = new Set();
2059
+ const selectParts = [];
2060
+ const mockRow = this._createMockRow();
2061
+ const selectedMock = this.selector(mockRow);
2062
+ const selection = returning(selectedMock);
2063
+ // For each selected field, determine if it's from main table or navigation
2064
+ for (const [alias, field] of Object.entries(selection)) {
2065
+ if (field && typeof field === 'object' && '__dbColumnName' in field) {
2066
+ const tableAlias = field.__tableAlias;
2067
+ const dbColumnName = field.__dbColumnName;
2068
+ if (!tableAlias || tableAlias === this.schema.name) {
2069
+ // Main table column - add to CTE RETURNING and final SELECT
2070
+ mainTableColumns.add(dbColumnName);
2071
+ selectParts.push(`"__mutation__"."${dbColumnName}" AS "${alias}"`);
2072
+ }
2073
+ else {
2074
+ // Navigation column - will be joined in outer query
2075
+ selectParts.push(`"${tableAlias}"."${dbColumnName}" AS "${alias}"`);
2076
+ }
2077
+ }
2078
+ }
2079
+ // We also need to include foreign keys for joins that aren't already in the selection
2080
+ for (const join of navigationInfo.joins) {
2081
+ for (const fk of join.foreignKeys) {
2082
+ // Find the db column name for this foreign key
2083
+ const colEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === fk);
2084
+ if (colEntry) {
2085
+ const config = colEntry[1].build();
2086
+ const dbColName = config.name;
2087
+ mainTableColumns.add(dbColName);
2088
+ }
2089
+ else {
2090
+ // FK might be the db column name directly
2091
+ mainTableColumns.add(fk);
2092
+ }
2093
+ }
2094
+ }
2095
+ // Also include foreign keys from intermediate joins (join to another join)
2096
+ for (const join of navigationInfo.joins) {
2097
+ if (join.sourceAlias && join.sourceAlias !== this.schema.name) {
2098
+ // This join comes from another join, we need to include those FK columns too
2099
+ // Find the source join
2100
+ const sourceJoin = navigationInfo.joins.find(j => j.alias === join.sourceAlias);
2101
+ if (sourceJoin) {
2102
+ // We need the source table's FK columns in our RETURNING if the source is the main table
2103
+ // But if source is itself a navigation, we handle it in the outer SELECT
2104
+ }
2105
+ }
2106
+ }
2107
+ // Build RETURNING clause for CTE with all needed columns from main table
2108
+ const cteReturningCols = Array.from(mainTableColumns).map(col => `"${col}"`).join(', ');
2109
+ // Add RETURNING to the mutation SQL for CTE
2110
+ const mutationWithReturning = `${mutationSql} RETURNING ${cteReturningCols}`;
2111
+ // Build the JOINs for the outer SELECT
2112
+ const joinClauses = [];
2113
+ for (const join of navigationInfo.joins) {
2114
+ const sourceAlias = join.sourceAlias === this.schema.name ? '__mutation__' : `"${join.sourceAlias}"`;
2115
+ const qualifiedJoinTable = this.getQualifiedTableName(join.targetTable, join.targetSchema);
2116
+ // Find the db column names for the foreign keys
2117
+ const joinConditions = [];
2118
+ for (let i = 0; i < join.foreignKeys.length; i++) {
2119
+ const fk = join.foreignKeys[i];
2120
+ const match = join.matches[i] || 'id';
2121
+ // Convert property name to db column name for FK
2122
+ let fkDbCol = fk;
2123
+ const colEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === fk);
2124
+ if (colEntry) {
2125
+ const config = colEntry[1].build();
2126
+ fkDbCol = config.name;
2127
+ }
2128
+ // For source that's the mutation CTE
2129
+ if (join.sourceAlias === this.schema.name || !join.sourceAlias) {
2130
+ joinConditions.push(`"__mutation__"."${fkDbCol}" = "${join.alias}"."${match}"`);
2131
+ }
2132
+ else {
2133
+ // Source is another joined table
2134
+ joinConditions.push(`"${join.sourceAlias}"."${fk}" = "${join.alias}"."${match}"`);
2135
+ }
2136
+ }
2137
+ const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
2138
+ joinClauses.push(`${joinType} ${qualifiedJoinTable} AS "${join.alias}" ON ${joinConditions.join(' AND ')}`);
2139
+ }
2140
+ // Build the final CTE query
2141
+ const sql = `WITH "__mutation__" AS (
2142
+ ${mutationWithReturning}
2143
+ )
2144
+ SELECT ${selectParts.join(', ')}
2145
+ FROM "__mutation__"
2146
+ ${joinClauses.join('\n')}`;
2147
+ return { sql, params: mutationParams };
2148
+ }
2149
+ /**
2150
+ * Map RETURNING results with navigation properties
2151
+ * Applies type mappers based on source table schemas
2152
+ * @internal
2153
+ */
2154
+ mapReturningResultsWithNavigation(rows, returning, navigationFields) {
2155
+ return rows.map(row => {
2156
+ const mapped = {};
2157
+ for (const [key, value] of Object.entries(row)) {
2158
+ // Check if this is a navigation field
2159
+ const navInfo = navigationFields.get(key);
2160
+ if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
2161
+ // Try to get mapper from the navigation target's schema
2162
+ const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
2163
+ if (targetSchema) {
2164
+ // Find the column and its mapper
2165
+ const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
2166
+ const config = col.build();
2167
+ return config.name === navInfo.dbColumnName;
2168
+ });
2169
+ if (colEntry) {
2170
+ const config = colEntry[1].build();
2171
+ mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
2172
+ continue;
2173
+ }
2174
+ }
2175
+ }
2176
+ // Try to find column by alias or name in main schema
2177
+ const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
2178
+ const config = col.build();
2179
+ return propName === key || config.name === key;
2180
+ });
2181
+ if (colEntry) {
2182
+ const [, col] = colEntry;
2183
+ const config = col.build();
2184
+ mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
2185
+ }
2186
+ else {
2187
+ mapped[key] = value;
2188
+ }
2189
+ }
2190
+ return mapped;
2191
+ });
2192
+ }
1937
2193
  /**
1938
2194
  * Create mock row for analysis
1939
2195
  * @internal