@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.
- package/CHANGELOG.md +27 -2
- package/README.md +87 -244
- package/_i18n/i18n.properties +0 -4
- package/cds-plugin.js +4 -2
- package/index.cds +114 -90
- package/lib/h2/java-codegen.js +34 -44
- package/lib/h2/triggers.js +5 -6
- package/lib/hana/composition.js +13 -15
- package/lib/hana/register.js +26 -7
- package/lib/hana/sql-expressions.js +9 -20
- package/lib/hana/triggers.js +26 -27
- package/lib/model-enhancer.js +1 -1
- package/lib/postgres/composition.js +11 -12
- package/lib/postgres/sql-expressions.js +24 -34
- package/lib/postgres/triggers.js +10 -12
- package/lib/skipHandlers.js +1 -1
- package/lib/sqlite/composition.js +12 -13
- package/lib/sqlite/sql-expressions.js +22 -32
- package/lib/sqlite/triggers.js +24 -25
- package/lib/utils/change-tracking.js +7 -3
- package/lib/utils/composition-helpers.js +4 -6
- package/lib/utils/entity-collector.js +67 -42
- package/lib/utils/session-variables.js +10 -12
- package/package.json +1 -2
|
@@ -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
|
|
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
|
|
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,
|
package/lib/hana/triggers.js
CHANGED
|
@@ -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 {
|
|
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;
|
package/lib/model-enhancer.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
let model;
|
|
8
|
+
const _cqn2sqlCache = new WeakMap();
|
|
10
9
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
15
|
+
cqn2sql = new TriggerCQN2SQL({ model });
|
|
16
|
+
_cqn2sqlCache.set(model, cqn2sql);
|
|
25
17
|
}
|
|
26
18
|
const sqlCQN = cqn4sql(query, model);
|
|
27
|
-
return
|
|
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} :=
|
|
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(
|
|
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,
|
package/lib/postgres/triggers.js
CHANGED
|
@@ -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 {
|
|
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;' : '';
|
package/lib/skipHandlers.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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}'
|