@cap-js/change-tracking 2.0.0-beta.1 → 2.0.0-beta.3

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.
@@ -7,17 +7,8 @@ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
7
7
 
8
8
  const TriggerCQN2SQL = createTriggerCQN2SQL(HANAService.CQN2SQL);
9
9
  let HANACQN2SQL;
10
- let model;
11
10
 
12
- function setModel(m) {
13
- model = m;
14
- }
15
-
16
- function getModel() {
17
- return model;
18
- }
19
-
20
- function toSQL(query) {
11
+ function toSQL(query, model) {
21
12
  if (!HANACQN2SQL) {
22
13
  HANACQN2SQL = new TriggerCQN2SQL();
23
14
  }
@@ -111,7 +102,7 @@ function getWhereCondition(col, modification) {
111
102
  /**
112
103
  * Returns SQL expression for a column's label (looked-up value for associations)
113
104
  */
114
- function getLabelExpr(col, refRow) {
105
+ function getLabelExpr(col, refRow, model) {
115
106
  if (!(col.target && col.alt)) {
116
107
  return `NULL`;
117
108
  }
@@ -139,36 +130,36 @@ function getLabelExpr(col, refRow) {
139
130
  const textsWhere = { ...where, locale: { func: 'SESSION_CONTEXT', args: [{ val: 'LOCALE' }] } };
140
131
  const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
141
132
  const baseQuery = SELECT.one.from(col.target).columns(columns).where(where);
142
- return `COALESCE((${toSQL(textsQuery)}), (${toSQL(baseQuery)}))`;
133
+ return `COALESCE((${toSQL(textsQuery, model)}), (${toSQL(baseQuery, model)}))`;
143
134
  }
144
135
 
145
136
  const query = SELECT.one.from(col.target).columns(columns).where(where);
146
- return `(${toSQL(query)})`;
137
+ return `(${toSQL(query, model)})`;
147
138
  }
148
139
 
149
140
  /**
150
141
  * Builds SQL expression for objectID (entity display name)
151
142
  * Uses @changelog annotation fields, falling back to entity name
152
143
  */
153
- function buildObjectIDExpr(objectIDs, entity, rowRef) {
144
+ function buildObjectIDExpr(objectIDs, entity, rowRef, model) {
154
145
  const keys = utils.extractKeys(entity.keys);
155
146
  const entityKeyExpr = compositeKeyExpr(keys.map((k) => `:${rowRef}.${k}`));
156
147
 
157
148
  if (!objectIDs || objectIDs.length === 0) {
158
- return `'${entity.name}'`;
149
+ return entityKeyExpr;
159
150
  }
160
151
 
161
152
  const parts = [];
162
153
  for (const oid of objectIDs) {
163
154
  if (oid.included) {
164
- parts.push(`TO_NVARCHAR(:${rowRef}.${oid.name})`);
155
+ parts.push(`COALESCE(TO_NVARCHAR(:${rowRef}.${oid.name}), '<empty>')`);
165
156
  } else {
166
157
  const where = keys.reduce((acc, k) => {
167
158
  acc[k] = { val: `:${rowRef}.${k}` };
168
159
  return acc;
169
160
  }, {});
170
161
  const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
171
- parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query)})), '')`);
162
+ parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query, model)})), '')`);
172
163
  }
173
164
  }
174
165
 
@@ -180,7 +171,7 @@ function buildObjectIDExpr(objectIDs, entity, rowRef) {
180
171
  * Builds SQL expression for grandparent entity's objectID
181
172
  * Used when creating grandparent composition entries for deep linking
182
173
  */
183
- function buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef) {
174
+ function buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef, model) {
184
175
  // Build WHERE clause to find the parent entity record (e.g., OrderItem from OrderItemNote's FK)
185
176
  const parentEntity = model.definitions[parentEntityName];
186
177
  const parentKeys = utils.extractKeys(parentEntity.keys);
@@ -209,8 +200,6 @@ function buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, p
209
200
  }
210
201
 
211
202
  module.exports = {
212
- setModel,
213
- getModel,
214
203
  toSQL,
215
204
  getSkipCheckCondition,
216
205
  getElementSkipCondition,
@@ -2,19 +2,19 @@ const cds = require('@sap/cds');
2
2
  const utils = require('../utils/change-tracking.js');
3
3
  const config = cds.env.requires['change-tracking'];
4
4
  const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
5
- const { setModel, getSkipCheckCondition, getElementSkipCondition, compositeKeyExpr, getValueExpr, getWhereCondition, getLabelExpr, buildObjectIDExpr } = require('./sql-expressions.js');
5
+ const { getSkipCheckCondition, getElementSkipCondition, compositeKeyExpr, getValueExpr, getWhereCondition, getLabelExpr, buildObjectIDExpr } = require('./sql-expressions.js');
6
6
  const { buildCompositionParentContext, buildParentLookupOrCreateSQL, buildCompositionOnlyBody } = require('./composition.js');
7
7
 
8
- function buildTriggerContext(entity, objectIDs, rowRef, compositionParentInfo = null) {
8
+ function buildTriggerContext(entity, objectIDs, rowRef, model, compositionParentInfo = null) {
9
9
  const keys = utils.extractKeys(entity.keys);
10
10
  return {
11
11
  entityKeyExpr: compositeKeyExpr(keys.map((k) => `:${rowRef}.${k}`)),
12
- objectIDExpr: buildObjectIDExpr(objectIDs, entity, rowRef),
12
+ objectIDExpr: buildObjectIDExpr(objectIDs, entity, rowRef, model),
13
13
  parentLookupExpr: compositionParentInfo !== null ? 'parent_id' : null
14
14
  };
15
15
  }
16
16
 
17
- function buildInsertSQL(entity, columns, modification, ctx) {
17
+ function buildInsertSQL(entity, columns, modification, ctx, model) {
18
18
  // Generate single UNION ALL query for all changed columns
19
19
  const unionQuery = columns
20
20
  .map((col) => {
@@ -37,8 +37,8 @@ function buildInsertSQL(entity, columns, modification, ctx) {
37
37
 
38
38
  const oldVal = modification === 'create' ? 'NULL' : getValueExpr(col, 'old');
39
39
  const newVal = modification === 'delete' ? 'NULL' : getValueExpr(col, 'new');
40
- const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old');
41
- const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new');
40
+ const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old', model);
41
+ const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new', model);
42
42
 
43
43
  return `SELECT '${col.name}' AS attribute, ${oldVal} AS valueChangedFrom, ${newVal} AS valueChangedTo, ${oldLabel} AS valueChangedFromLabel, ${newLabel} AS valueChangedToLabel, '${col.type}' AS valueDataType FROM SAP_CHANGELOG_CHANGE_TRACKING_DUMMY WHERE ${fullWhere}`;
44
44
  })
@@ -81,18 +81,18 @@ function wrapInSkipCheck(entityName, insertSQL, compositionParentContext = null)
81
81
  END IF;`;
82
82
  }
83
83
 
84
- function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
85
- const ctx = buildTriggerContext(entity, objectIDs, 'new', compositionParentInfo);
84
+ function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
85
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
86
86
 
87
87
  // Build context for composition parent entry if this is a tracked composition target
88
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'create', 'new', grandParentCompositionInfo) : null;
88
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'create', 'new', model, grandParentCompositionInfo) : null;
89
89
 
90
90
  // Handle composition-only triggers (no tracked columns, only composition parent entry)
91
91
  let body;
92
92
  if (columns.length === 0 && compositionParentContext) {
93
93
  body = buildCompositionOnlyBody(entity.name, compositionParentContext);
94
94
  } else {
95
- const insertSQL = buildInsertSQL(entity, columns, 'create', ctx);
95
+ const insertSQL = buildInsertSQL(entity, columns, 'create', ctx, model);
96
96
  body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
97
97
  }
98
98
 
@@ -108,18 +108,18 @@ END;`,
108
108
  };
109
109
  }
110
110
 
111
- function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
112
- const ctx = buildTriggerContext(entity, objectIDs, 'new', compositionParentInfo);
111
+ function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
112
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
113
113
 
114
114
  // Build context for composition parent entry if this is a tracked composition target
115
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'update', 'new', grandParentCompositionInfo) : null;
115
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'update', 'new', model, grandParentCompositionInfo) : null;
116
116
 
117
117
  // Handle composition-only triggers (no tracked columns, only composition parent entry)
118
118
  let body;
119
119
  if (columns.length === 0 && compositionParentContext) {
120
120
  body = buildCompositionOnlyBody(entity.name, compositionParentContext);
121
121
  } else {
122
- const insertSQL = buildInsertSQL(entity, columns, 'update', ctx);
122
+ const insertSQL = buildInsertSQL(entity, columns, 'update', ctx, model);
123
123
  body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
124
124
  }
125
125
 
@@ -144,18 +144,18 @@ END;`,
144
144
  };
145
145
  }
146
146
 
147
- function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
148
- const ctx = buildTriggerContext(entity, objectIDs, 'old', compositionParentInfo);
147
+ function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
148
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
149
149
 
150
150
  // Build context for composition parent entry if this is a tracked composition target
151
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', grandParentCompositionInfo) : null;
151
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
152
152
 
153
153
  // Handle composition-only triggers (no tracked columns, only composition parent entry)
154
154
  let body;
155
155
  if (columns.length === 0 && compositionParentContext) {
156
156
  body = buildCompositionOnlyBody(entity.name, compositionParentContext);
157
157
  } else {
158
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
158
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
159
159
  body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
160
160
  }
161
161
 
@@ -171,16 +171,16 @@ END;`,
171
171
  };
172
172
  }
173
173
 
174
- function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
174
+ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
175
175
  const keys = utils.extractKeys(entity.keys);
176
176
  const entityKey = compositeKeyExpr(keys.map((k) => `:old.${k}`));
177
177
 
178
- const ctx = buildTriggerContext(entity, objectIDs, 'old', compositionParentInfo);
178
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
179
179
 
180
180
  const deleteSQL = `DELETE FROM SAP_CHANGELOG_CHANGES WHERE entity = '${entity.name}' AND entityKey = ${entityKey};`;
181
181
 
182
182
  // Build context for composition parent entry if this is a tracked composition target
183
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', grandParentCompositionInfo) : null;
183
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
184
184
 
185
185
  // Special wrapping for delete - need variable declared if using composition
186
186
  let body;
@@ -189,7 +189,7 @@ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compos
189
189
  body = buildCompositionOnlyBody(entity.name, compositionParentContext, deleteSQL);
190
190
  } else if (compositionParentContext) {
191
191
  // Mixed case: both composition parent entry and child column inserts
192
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
192
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
193
193
  const { declares } = compositionParentContext;
194
194
  body = `${declares}
195
195
  IF ${getSkipCheckCondition(entity.name)} THEN
@@ -199,7 +199,7 @@ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compos
199
199
  END IF;`;
200
200
  } else {
201
201
  // No composition: standard delete with column inserts
202
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
202
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
203
203
  body = wrapInSkipCheck(entity.name, `${deleteSQL}\n\t\t${insertSQL}`);
204
204
  }
205
205
 
@@ -216,7 +216,6 @@ END;`,
216
216
  }
217
217
 
218
218
  function generateHANATriggers(csn, entity, rootEntity = null, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
219
- setModel(csn);
220
219
  const triggers = [];
221
220
  const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
222
221
 
@@ -238,14 +237,14 @@ function generateHANATriggers(csn, entity, rootEntity = null, mergedAnnotations
238
237
 
239
238
  // Generate triggers - either for tracked columns or for composition-only tracking
240
239
  if (!config?.disableCreateTracking) {
241
- triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
240
+ triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
242
241
  }
243
242
  if (!config?.disableUpdateTracking) {
244
- triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
243
+ triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
245
244
  }
246
245
  if (!config?.disableDeleteTracking) {
247
246
  const generateDeleteTriggerFunc = config?.preserveDeletes ? generateDeleteTriggerPreserve : generateDeleteTrigger;
248
- triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
247
+ triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
249
248
  }
250
249
 
251
250
  return triggers;
@@ -203,7 +203,7 @@ function enhanceModel(m) {
203
203
  if (!cqn) {
204
204
  return false;
205
205
  }
206
- // from.args is the case for joins
206
+ // from.args is the case for joins //REVISIT: only works in case it is one join, multiple joins and the ref is nested in further args
207
207
  baseE = cqn.from?.ref?.[0] ?? cqn.from?.args?.[0]?.ref?.[0];
208
208
  }
209
209
  return false;
@@ -1,14 +1,16 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
- const { getModel, toSQL, compositeKeyExpr } = require('./sql-expressions.js');
2
+ const { toSQL, compositeKeyExpr } = require('./sql-expressions.js');
3
3
 
4
4
  /**
5
5
  * Builds rootObjectID select for composition of many
6
6
  */
7
- function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow) {
8
- if (!rootObjectIDs || rootObjectIDs.length === 0) return `'${rootEntity.name}'`;
7
+ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow, model) {
8
+ const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `${refRow}.${k}`));
9
+
10
+ if (!rootObjectIDs || rootObjectIDs.length === 0) return rootEntityKeyExpr;
9
11
 
10
12
  const rootKeys = utils.extractKeys(rootEntity.keys);
11
- if (rootKeys.length !== binding.length) return `'${rootEntity.name}'`;
13
+ if (rootKeys.length !== binding.length) return rootEntityKeyExpr;
12
14
 
13
15
  const where = {};
14
16
  for (let i = 0; i < rootKeys.length; i++) {
@@ -18,17 +20,15 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
18
20
  const parts = [];
19
21
  for (const oid of rootObjectIDs) {
20
22
  const query = SELECT.one.from(rootEntity.name).columns(oid.name).where(where);
21
- parts.push(`COALESCE((${toSQL(query)})::TEXT, '')`);
23
+ parts.push(`COALESCE((${toSQL(query, model)})::TEXT, '')`);
22
24
  }
23
25
 
24
26
  const concatLogic = `CONCAT_WS(', ', ${parts.join(', ')})`;
25
- const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `${refRow}.${k}`));
26
27
 
27
28
  return `COALESCE(NULLIF(${concatLogic}, ''), ${rootEntityKeyExpr})`;
28
29
  }
29
30
 
30
- function buildCompositionOfOneParentBlock(compositionParentInfo, rootObjectIDs) {
31
- const model = getModel();
31
+ function buildCompositionOfOneParentBlock(compositionParentInfo, rootObjectIDs, model) {
32
32
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
33
33
  const { compositionName, childKeys } = parentKeyBinding;
34
34
 
@@ -84,20 +84,19 @@ function buildCompositionOfOneParentBlock(compositionParentInfo, rootObjectIDs)
84
84
  END IF;`;
85
85
  }
86
86
 
87
- function buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, modification, grandParentCompositionInfo = null) {
88
- const model = getModel();
87
+ function buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, modification, model, grandParentCompositionInfo = null) {
89
88
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
90
89
 
91
90
  // Handle composition of one (parent has FK to child - need reverse lookup)
92
91
  if (parentKeyBinding.type === 'compositionOfOne') {
93
- return buildCompositionOfOneParentBlock(compositionParentInfo, rootObjectIDs);
92
+ return buildCompositionOfOneParentBlock(compositionParentInfo, rootObjectIDs, model);
94
93
  }
95
94
 
96
95
  const parentKeyExpr = compositeKeyExpr(parentKeyBinding.map((k) => `rec.${k}`));
97
96
 
98
97
  // Build rootObjectID expression for the parent entity
99
98
  const rootEntity = model.definitions[parentEntityName];
100
- const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, 'rec');
99
+ const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, 'rec', model);
101
100
 
102
101
  let grandparentBlock = '';
103
102
  let grandparentLookupExpr = 'NULL';
@@ -5,26 +5,18 @@ const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
5
5
  const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
6
6
  const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
7
7
 
8
- let PostgresCQN2SQL;
9
- let model;
8
+ const _cqn2sqlCache = new WeakMap();
10
9
 
11
- function setModel(m) {
12
- model = m;
13
- PostgresCQN2SQL = null;
14
- }
15
-
16
- function getModel() {
17
- return model;
18
- }
19
-
20
- function toSQL(query) {
21
- if (!PostgresCQN2SQL) {
10
+ function toSQL(query, model) {
11
+ let cqn2sql = _cqn2sqlCache.get(model);
12
+ if (!cqn2sql) {
22
13
  const Service = require('@cap-js/postgres');
23
14
  const TriggerCQN2SQL = createTriggerCQN2SQL(Service.CQN2SQL);
24
- PostgresCQN2SQL = new TriggerCQN2SQL({ model });
15
+ cqn2sql = new TriggerCQN2SQL({ model });
16
+ _cqn2sqlCache.set(model, cqn2sql);
25
17
  }
26
18
  const sqlCQN = cqn4sql(query, model);
27
- return PostgresCQN2SQL.SELECT(sqlCQN);
19
+ return cqn2sql.SELECT(sqlCQN);
28
20
  }
29
21
 
30
22
  function getSkipCheckCondition(entityName) {
@@ -94,7 +86,7 @@ function getWhereCondition(col, modification) {
94
86
  /**
95
87
  * Builds scalar subselect for association label lookup with locale support
96
88
  */
97
- function buildAssocLookup(column, refRow) {
89
+ function buildAssocLookup(column, refRow, model) {
98
90
  let where = {};
99
91
  if (column.foreignKeys) {
100
92
  where = column.foreignKeys.reduce((acc, k) => {
@@ -117,19 +109,19 @@ function buildAssocLookup(column, refRow) {
117
109
  const textsWhere = { ...where, locale: { func: 'current_setting', args: [{ val: 'cap.locale' }, { val: true }] } };
118
110
  const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
119
111
  const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
120
- return `(SELECT COALESCE((${toSQL(textsQuery)}), (${toSQL(baseQuery)})))`;
112
+ return `(SELECT COALESCE((${toSQL(textsQuery, model)}), (${toSQL(baseQuery, model)})))`;
121
113
  }
122
114
 
123
115
  const query = SELECT.one.from(column.target).columns(columns).where(where);
124
- return `(${toSQL(query)})`;
116
+ return `(${toSQL(query, model)})`;
125
117
  }
126
118
 
127
119
  /**
128
120
  * Returns SQL expression for a column's label (looked-up value for associations)
129
121
  */
130
- function getLabelExpr(col, refRow) {
122
+ function getLabelExpr(col, refRow, model) {
131
123
  if (col.target && col.alt) {
132
- return buildAssocLookup(col, refRow);
124
+ return buildAssocLookup(col, refRow, model);
133
125
  }
134
126
  return 'NULL';
135
127
  }
@@ -137,22 +129,22 @@ function getLabelExpr(col, refRow) {
137
129
  /**
138
130
  * Builds PL/pgSQL statement for objectID assignment
139
131
  */
140
- function buildObjectIDAssignment(objectIDs, entity, keys, recVar, targetVar) {
132
+ function buildObjectIDAssignment(objectIDs, entity, keys, recVar, targetVar, model) {
141
133
  if (!objectIDs || objectIDs.length === 0) {
142
- return `${targetVar} := '${entity.name}';`;
134
+ return `${targetVar} := entity_key;`;
143
135
  }
144
136
 
145
137
  const parts = [];
146
138
  for (const oid of objectIDs) {
147
139
  if (oid.included) {
148
- parts.push(`${recVar}.${oid.name}::TEXT`);
140
+ parts.push(`COALESCE(${recVar}.${oid.name}::TEXT, '<empty>')`);
149
141
  } else {
150
142
  const where = keys.reduce((acc, k) => {
151
143
  acc[k] = { val: `${recVar}.${k}` };
152
144
  return acc;
153
145
  }, {});
154
146
  const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
155
- parts.push(`COALESCE((${toSQL(query)})::TEXT, '')`);
147
+ parts.push(`COALESCE((${toSQL(query, model)})::TEXT, '')`);
156
148
  }
157
149
  }
158
150
 
@@ -164,7 +156,7 @@ function buildObjectIDAssignment(objectIDs, entity, keys, recVar, targetVar) {
164
156
  `;
165
157
  }
166
158
 
167
- function buildColumnSubquery(col, modification, entity) {
159
+ function buildColumnSubquery(col, modification, entity, model) {
168
160
  const whereCondition = getWhereCondition(col, modification);
169
161
  const elementSkipCondition = getElementSkipCondition(entity.name, col.name);
170
162
  let fullWhere = `(${whereCondition}) AND ${elementSkipCondition}`;
@@ -184,8 +176,8 @@ function buildColumnSubquery(col, modification, entity) {
184
176
 
185
177
  const oldVal = modification === 'create' ? 'NULL' : getValueExpr(col, 'OLD');
186
178
  const newVal = modification === 'delete' ? 'NULL' : getValueExpr(col, 'NEW');
187
- const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'OLD');
188
- const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'NEW');
179
+ const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'OLD', model);
180
+ const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'NEW', model);
189
181
 
190
182
  return `SELECT '${col.name}' AS attribute, ${oldVal} AS valueChangedFrom, ${newVal} AS valueChangedTo, ${oldLabel} AS valueChangedFromLabel, ${newLabel} AS valueChangedToLabel, '${col.type}' AS valueDataType WHERE ${fullWhere}`;
191
183
  }
@@ -193,8 +185,8 @@ function buildColumnSubquery(col, modification, entity) {
193
185
  /**
194
186
  * Generates INSERT SQL for changelog entries from UNION query
195
187
  */
196
- function buildChangelogInsertSQL(columns, modification, entity, hasCompositionParent = false) {
197
- const unionQuery = columns.map((col) => buildColumnSubquery(col, modification, entity)).join('\n UNION ALL\n ');
188
+ function buildChangelogInsertSQL(columns, modification, entity, model, hasCompositionParent = false) {
189
+ const unionQuery = columns.map((col) => buildColumnSubquery(col, modification, entity, model)).join('\n UNION ALL\n ');
198
190
  const parentIdValue = hasCompositionParent ? 'comp_parent_id' : 'NULL';
199
191
 
200
192
  return `INSERT INTO sap_changelog_changes
@@ -223,7 +215,7 @@ function buildChangelogInsertSQL(columns, modification, entity, hasCompositionPa
223
215
  /**
224
216
  * Generates INSERT block for a modification type (with config check)
225
217
  */
226
- function buildInsertBlock(columns, modification, entity, hasCompositionParent = false) {
218
+ function buildInsertBlock(columns, modification, entity, model, hasCompositionParent = false) {
227
219
  if (!config || (modification === 'create' && config.disableCreateTracking) || (modification === 'update' && config.disableUpdateTracking) || (modification === 'delete' && config.disableDeleteTracking)) {
228
220
  return '';
229
221
  }
@@ -232,10 +224,10 @@ function buildInsertBlock(columns, modification, entity, hasCompositionParent =
232
224
  const keys = utils.extractKeys(entity.keys);
233
225
  const entityKey = compositeKeyExpr(keys.map((k) => `OLD.${k}`));
234
226
  const deleteSQL = `DELETE FROM sap_changelog_changes WHERE entity = '${entity.name}' AND entitykey = ${entityKey};`;
235
- return `${deleteSQL}\n ${buildChangelogInsertSQL(columns, modification, entity, hasCompositionParent)}`;
227
+ return `${deleteSQL}\n ${buildChangelogInsertSQL(columns, modification, entity, model, hasCompositionParent)}`;
236
228
  }
237
229
 
238
- return buildChangelogInsertSQL(columns, modification, entity, hasCompositionParent);
230
+ return buildChangelogInsertSQL(columns, modification, entity, model, hasCompositionParent);
239
231
  }
240
232
 
241
233
  /**
@@ -256,8 +248,6 @@ function extractTrackedDbColumns(columns) {
256
248
  }
257
249
 
258
250
  module.exports = {
259
- setModel,
260
- getModel,
261
251
  toSQL,
262
252
  getSkipCheckCondition,
263
253
  getElementSkipCondition,
@@ -1,26 +1,26 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
2
  const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
3
- const { setModel, getSkipCheckCondition, compositeKeyExpr, buildObjectIDAssignment, buildInsertBlock, extractTrackedDbColumns } = require('./sql-expressions.js');
3
+ const { getSkipCheckCondition, compositeKeyExpr, buildObjectIDAssignment, buildInsertBlock, extractTrackedDbColumns } = require('./sql-expressions.js');
4
4
  const { buildCompositionParentBlock } = require('./composition.js');
5
5
 
6
6
  /**
7
7
  * Generates the PL/pgSQL function body for the main change tracking trigger
8
8
  */
9
- function buildFunctionBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
9
+ function buildFunctionBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
10
10
  const keys = utils.extractKeys(entity.keys);
11
11
  const entityKeyExpr = compositeKeyExpr(keys.map((k) => `rec.${k}`));
12
12
 
13
- const objectIDAssignment = buildObjectIDAssignment(objectIDs, entity, keys, 'rec', 'object_id');
13
+ const objectIDAssignment = buildObjectIDAssignment(objectIDs, entity, keys, 'rec', 'object_id', model);
14
14
 
15
15
  const hasCompositionParent = compositionParentInfo !== null;
16
- const createBlock = columns.length > 0 ? buildInsertBlock(columns, 'create', entity, hasCompositionParent) : '';
17
- const updateBlock = columns.length > 0 ? buildInsertBlock(columns, 'update', entity, hasCompositionParent) : '';
18
- const deleteBlock = columns.length > 0 ? buildInsertBlock(columns, 'delete', entity, hasCompositionParent) : '';
16
+ const createBlock = columns.length > 0 ? buildInsertBlock(columns, 'create', entity, model, hasCompositionParent) : '';
17
+ const updateBlock = columns.length > 0 ? buildInsertBlock(columns, 'update', entity, model, hasCompositionParent) : '';
18
+ const deleteBlock = columns.length > 0 ? buildInsertBlock(columns, 'delete', entity, model, hasCompositionParent) : '';
19
19
 
20
20
  // Build composition parent blocks if needed
21
- const createParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'create', grandParentCompositionInfo) : '';
22
- const updateParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'update', grandParentCompositionInfo) : '';
23
- const deleteParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'delete', grandParentCompositionInfo) : '';
21
+ const createParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'create', model, grandParentCompositionInfo) : '';
22
+ const updateParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'update', model, grandParentCompositionInfo) : '';
23
+ const deleteParentBlock = compositionParentInfo ? buildCompositionParentBlock(compositionParentInfo, rootObjectIDs, 'delete', model, grandParentCompositionInfo) : '';
24
24
 
25
25
  return `
26
26
  DECLARE
@@ -53,8 +53,6 @@ function buildFunctionBody(entity, columns, objectIDs, rootEntity, rootObjectIDs
53
53
  }
54
54
 
55
55
  function generatePostgresTriggers(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
56
- setModel(csn);
57
-
58
56
  const triggers = [];
59
57
  const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
60
58
  const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
@@ -75,7 +73,7 @@ function generatePostgresTriggers(csn, entity, rootEntity, mergedAnnotations = n
75
73
  const triggerName = `${tableName}_tr_change`;
76
74
  const functionName = `${tableName}_func_change`;
77
75
 
78
- const funcBody = buildFunctionBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
76
+ const funcBody = buildFunctionBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo);
79
77
 
80
78
  // Include comp_parent_id, comp_parent_modification and comp_grandparent_id variable declarations if needed
81
79
  const parentIdDecl = compositionParentInfo ? 'comp_parent_id UUID := NULL;' : '';
@@ -9,7 +9,7 @@ const { setSkipSessionVariables, resetSkipSessionVariables, resetAutoSkipForServ
9
9
  function registerSessionVariableHandlers() {
10
10
  cds.db.before(['INSERT', 'UPDATE', 'DELETE'], async (req) => {
11
11
  const model = cds.context?.model ?? cds.model;
12
- const { collectedEntities } = collectEntities(model);
12
+ const collectedEntities = model.collectEntities || (model.collectEntities = collectEntities(model).collectedEntities);
13
13
  if (!req.target || req.target.name.endsWith('.drafts')) return;
14
14
  const srv = req.target._service;
15
15
  if (!srv) return;
@@ -1,14 +1,16 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
- const { getModel, toSQL, compositeKeyExpr } = require('./sql-expressions.js');
2
+ const { toSQL, compositeKeyExpr } = require('./sql-expressions.js');
3
3
 
4
4
  /**
5
5
  * Builds rootObjectID select for composition of many
6
6
  */
7
- function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow) {
8
- if (!rootObjectIDs || rootObjectIDs.length === 0) return `'${rootEntity.name}'`;
7
+ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow, model) {
8
+ const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `${refRow}.${k}`));
9
+
10
+ if (!rootObjectIDs || rootObjectIDs.length === 0) return rootEntityKeyExpr;
9
11
 
10
12
  const rootKeys = utils.extractKeys(rootEntity.keys);
11
- if (rootKeys.length !== binding.length) return `'${rootEntity.name}'`;
13
+ if (rootKeys.length !== binding.length) return rootEntityKeyExpr;
12
14
 
13
15
  const where = {};
14
16
  for (let i = 0; i < rootKeys.length; i++) {
@@ -19,15 +21,14 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
19
21
  const oids = rootObjectIDs.map((o) => ({ ...o }));
20
22
  for (const oid of oids) {
21
23
  const q = SELECT.one.from(rootEntity.name).columns(oid.name).where(where);
22
- oid.selectSQL = toSQL(q);
24
+ oid.selectSQL = toSQL(q, model);
23
25
  }
24
26
 
25
27
  const unions = oids.map((oid) => `SELECT (${oid.selectSQL}) AS value`).join('\nUNION ALL\n');
26
28
  return `(SELECT GROUP_CONCAT(value, ', ') FROM (${unions}))`;
27
29
  }
28
30
 
29
- function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef) {
30
- const model = getModel();
31
+ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model) {
31
32
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
32
33
  const { compositionName, childKeys } = parentKeyBinding;
33
34
 
@@ -95,23 +96,21 @@ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs
95
96
  return { insertSQL, parentEntityName, compositionFieldName, parentKeyExpr, parentLookupExpr };
96
97
  }
97
98
 
98
- function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, grandParentCompositionInfo = null) {
99
- const model = getModel();
99
+ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model, grandParentCompositionInfo = null) {
100
100
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
101
101
 
102
102
  // Handle composition of one (parent has FK to child - need reverse lookup)
103
103
  if (parentKeyBinding.type === 'compositionOfOne') {
104
- return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef);
104
+ return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model);
105
105
  }
106
106
 
107
107
  const parentKeyExpr = compositeKeyExpr(parentKeyBinding.map((k) => `${rowRef}.${k}`));
108
108
 
109
109
  // Build rootObjectID expression for the parent entity
110
110
  const rootEntity = model.definitions[parentEntityName];
111
- const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef);
111
+ const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef, model);
112
112
 
113
113
  let insertSQL;
114
- let grandParentLookupExpr = 'NULL';
115
114
 
116
115
  if (grandParentCompositionInfo) {
117
116
  const { grandParentEntityName, grandParentCompositionFieldName, grandParentKeyBinding } = grandParentCompositionInfo;
@@ -124,7 +123,7 @@ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, mod
124
123
 
125
124
  // Build expression for grandparent lookup (for linking parent entry to it)
126
125
  // Must filter by createdAt to get entry from current transaction
127
- grandParentLookupExpr = `(SELECT ID FROM sap_changelog_Changes
126
+ const grandParentLookupExpr = `(SELECT ID FROM sap_changelog_Changes
128
127
  WHERE entity = '${grandParentEntityName}'
129
128
  AND entityKey = ${grandParentKeyExpr}
130
129
  AND attribute = '${grandParentCompositionFieldName}'