@cap-js/change-tracking 1.1.4 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +45 -71
  3. package/_i18n/i18n.properties +10 -22
  4. package/_i18n/i18n_ar.properties +3 -3
  5. package/_i18n/i18n_bg.properties +3 -3
  6. package/_i18n/i18n_cs.properties +3 -3
  7. package/_i18n/i18n_da.properties +3 -3
  8. package/_i18n/i18n_de.properties +3 -3
  9. package/_i18n/i18n_el.properties +3 -3
  10. package/_i18n/i18n_en.properties +3 -3
  11. package/_i18n/i18n_en_US_saptrc.properties +3 -32
  12. package/_i18n/i18n_es.properties +3 -3
  13. package/_i18n/i18n_es_MX.properties +3 -3
  14. package/_i18n/i18n_fi.properties +3 -3
  15. package/_i18n/i18n_fr.properties +3 -3
  16. package/_i18n/i18n_he.properties +3 -3
  17. package/_i18n/i18n_hr.properties +3 -3
  18. package/_i18n/i18n_hu.properties +3 -3
  19. package/_i18n/i18n_it.properties +3 -3
  20. package/_i18n/i18n_ja.properties +3 -3
  21. package/_i18n/i18n_kk.properties +3 -3
  22. package/_i18n/i18n_ko.properties +3 -3
  23. package/_i18n/i18n_ms.properties +3 -3
  24. package/_i18n/i18n_nl.properties +3 -3
  25. package/_i18n/i18n_no.properties +3 -3
  26. package/_i18n/i18n_pl.properties +3 -3
  27. package/_i18n/i18n_pt.properties +3 -3
  28. package/_i18n/i18n_ro.properties +3 -3
  29. package/_i18n/i18n_ru.properties +3 -3
  30. package/_i18n/i18n_sh.properties +3 -3
  31. package/_i18n/i18n_sk.properties +3 -3
  32. package/_i18n/i18n_sl.properties +3 -3
  33. package/_i18n/i18n_sv.properties +3 -3
  34. package/_i18n/i18n_th.properties +3 -3
  35. package/_i18n/i18n_tr.properties +3 -3
  36. package/_i18n/i18n_uk.properties +3 -3
  37. package/_i18n/i18n_vi.properties +3 -3
  38. package/_i18n/i18n_zh_CN.properties +3 -3
  39. package/_i18n/i18n_zh_TW.properties +3 -3
  40. package/cds-plugin.js +16 -263
  41. package/index.cds +187 -76
  42. package/lib/TriggerCQN2SQL.js +42 -0
  43. package/lib/h2/java-codegen.js +833 -0
  44. package/lib/h2/register.js +27 -0
  45. package/lib/h2/triggers.js +41 -0
  46. package/lib/hana/composition.js +248 -0
  47. package/lib/hana/register.js +28 -0
  48. package/lib/hana/sql-expressions.js +213 -0
  49. package/lib/hana/triggers.js +253 -0
  50. package/lib/localization.js +53 -117
  51. package/lib/model-enhancer.js +266 -0
  52. package/lib/postgres/composition.js +190 -0
  53. package/lib/postgres/register.js +44 -0
  54. package/lib/postgres/sql-expressions.js +261 -0
  55. package/lib/postgres/triggers.js +113 -0
  56. package/lib/skipHandlers.js +34 -0
  57. package/lib/sqlite/composition.js +234 -0
  58. package/lib/sqlite/register.js +28 -0
  59. package/lib/sqlite/sql-expressions.js +228 -0
  60. package/lib/sqlite/triggers.js +163 -0
  61. package/lib/utils/change-tracking.js +394 -0
  62. package/lib/utils/composition-helpers.js +67 -0
  63. package/lib/utils/entity-collector.js +297 -0
  64. package/lib/utils/session-variables.js +276 -0
  65. package/lib/utils/trigger-utils.js +94 -0
  66. package/package.json +17 -7
  67. package/lib/change-log.js +0 -538
  68. package/lib/entity-helper.js +0 -217
  69. package/lib/format-options.js +0 -66
  70. package/lib/template-processor.js +0 -115
@@ -0,0 +1,833 @@
1
+ const utils = require('../utils/change-tracking.js');
2
+ const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
3
+ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
4
+
5
+ const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
6
+
7
+ const _cqn2sqlCache = new WeakMap();
8
+
9
+ function _toSQL(query, model) {
10
+ let cqn2sql = _cqn2sqlCache.get(model);
11
+ if (!cqn2sql) {
12
+ const SQLiteService = require('@cap-js/sqlite');
13
+ const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
14
+ cqn2sql = new TriggerCQN2SQL({ model: model });
15
+ _cqn2sqlCache.set(model, cqn2sql);
16
+ }
17
+ const sqlCQN = cqn4sql(query, model);
18
+ return cqn2sql.SELECT(sqlCQN);
19
+ }
20
+
21
+ function handleAssocLookup(column, refRow, model) {
22
+ let bindings = [];
23
+ let where = {};
24
+
25
+ if (column.foreignKeys) {
26
+ where = column.foreignKeys.reduce((acc, k) => {
27
+ acc[k] = { ref: ['?'], param: true };
28
+ return acc;
29
+ }, {});
30
+ bindings = column.foreignKeys.map((fk) => `${refRow}.getString("${column.name}_${fk}")`);
31
+ } else if (column.on) {
32
+ where = column.on.reduce((acc, k) => {
33
+ acc[k] = { ref: ['?'], param: true };
34
+ return acc;
35
+ }, {});
36
+ bindings = column.on.map((assoc) => `${refRow}.getString("${assoc}")`);
37
+ }
38
+
39
+ const alt = column.alt.map((s) => s.split('.').slice(1).join('.'));
40
+ const columns = alt.length === 1 ? alt[0] : utils.buildConcatXpr(alt);
41
+
42
+ // Check if target entity has localized data
43
+ const localizedInfo = utils.getLocalizedLookupInfo(column.target, column.alt, model);
44
+
45
+ if (localizedInfo) {
46
+ // Build locale-aware lookup: try .texts table first, fall back to base entity
47
+ const textsWhere = { ...where, locale: { ref: ['?'], param: true } };
48
+ const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
49
+ const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
50
+
51
+ const textsSQL = _toSQL(textsQuery, model);
52
+ const baseSQL = _toSQL(baseQuery, model);
53
+
54
+ // Add locale binding (fetched from session variable @$user.locale)
55
+ const textsBindings = [...bindings, 'locale'];
56
+ const baseBindings = [...bindings];
57
+
58
+ return {
59
+ sql: `(SELECT COALESCE((${textsSQL}), (${baseSQL})))`,
60
+ bindings: [...textsBindings, ...baseBindings],
61
+ needsLocale: true
62
+ };
63
+ }
64
+
65
+ const query = SELECT.one.from(column.target).columns(columns).where(where);
66
+
67
+ return {
68
+ sql: `(${_toSQL(query, model)})`,
69
+ bindings: bindings
70
+ };
71
+ }
72
+
73
+ function _generateJavaMethod(createBody, updateBody, deleteBody, entityName, compositionParentInfo = null, grandParentCompositionInfo = null) {
74
+ const entitySkipVar = getEntitySkipVarName(entityName);
75
+
76
+ // Extract values for use in template strings
77
+ const parentEntityName = compositionParentInfo?.parentEntityName ?? '';
78
+ const compositionFieldName = compositionParentInfo?.compositionFieldName ?? '';
79
+ const grandParentEntityName = grandParentCompositionInfo?.grandParentEntityName ?? '';
80
+ const grandParentCompositionFieldName = grandParentCompositionInfo?.grandParentCompositionFieldName ?? '';
81
+
82
+ // Add grandparent entry helper method when grandParentCompositionInfo exists
83
+ // Note: grandparent entries always use 'update' modification since they represent changes to an existing parent's composition
84
+ const grandParentHelper = grandParentCompositionInfo
85
+ ? `
86
+ private String ensureGrandParentCompositionEntry(Connection conn, String grandParentKey, String grandParentObjectID) throws SQLException {
87
+ String grandParentId = null;
88
+ long transactionId = getTransactionId(conn);
89
+
90
+ String checkSQL = "SELECT ID FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = ?";
91
+ try (PreparedStatement stmt = conn.prepareStatement(checkSQL)) {
92
+ stmt.setString(1, "${grandParentEntityName}");
93
+ stmt.setString(2, grandParentKey);
94
+ stmt.setString(3, "${grandParentCompositionFieldName}");
95
+ stmt.setLong(4, transactionId);
96
+ try (ResultSet rs = stmt.executeQuery()) {
97
+ if (rs.next()) {
98
+ grandParentId = rs.getString(1);
99
+ }
100
+ }
101
+ }
102
+
103
+ if (grandParentId == null) {
104
+ grandParentId = java.util.UUID.randomUUID().toString();
105
+ String insertSQL = "INSERT INTO sap_changelog_Changes (ID, PARENT_ID, ATTRIBUTE, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID) VALUES (?, NULL, ?, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), 'cds.Composition', 'update', ?)";
106
+ try (PreparedStatement stmt = conn.prepareStatement(insertSQL)) {
107
+ stmt.setString(1, grandParentId);
108
+ stmt.setString(2, "${grandParentCompositionFieldName}");
109
+ stmt.setString(3, "${grandParentEntityName}");
110
+ stmt.setString(4, grandParentKey);
111
+ stmt.setString(5, grandParentObjectID);
112
+ stmt.setLong(6, transactionId);
113
+ stmt.executeUpdate();
114
+ }
115
+ }
116
+
117
+ return grandParentId;
118
+ }
119
+ `
120
+ : '';
121
+
122
+ // Add parent ID helper method if needed
123
+ const parentIdHelper = compositionParentInfo
124
+ ? `
125
+ private String ensureCompositionParentEntry(Connection conn, String parentEntityKey, String objectID, String modification${grandParentCompositionInfo ? ', String parentChangelogId' : ''}) throws SQLException {
126
+ String parentId = null;
127
+ long transactionId = getTransactionId(conn);
128
+
129
+ String checkSQL = "SELECT ID FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = ?";
130
+ try (PreparedStatement stmt = conn.prepareStatement(checkSQL)) {
131
+ stmt.setString(1, "${parentEntityName}");
132
+ stmt.setString(2, parentEntityKey);
133
+ stmt.setString(3, "${compositionFieldName}");
134
+ stmt.setLong(4, transactionId);
135
+ try (ResultSet rs = stmt.executeQuery()) {
136
+ if (rs.next()) {
137
+ parentId = rs.getString(1);
138
+ }
139
+ }
140
+ }
141
+
142
+ if (parentId == null) {
143
+ ${
144
+ grandParentCompositionInfo
145
+ ? ''
146
+ : `
147
+ String effectiveModification = modification;
148
+ if ("create".equals(modification)) {
149
+ String parentCreatedCheckSQL = "SELECT COUNT(*) FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND MODIFICATION = 'create' AND TRANSACTIONID = ?";
150
+ try (PreparedStatement checkStmt = conn.prepareStatement(parentCreatedCheckSQL)) {
151
+ checkStmt.setString(1, "${parentEntityName}");
152
+ checkStmt.setString(2, parentEntityKey);
153
+ checkStmt.setLong(3, transactionId);
154
+ try (ResultSet rs = checkStmt.executeQuery()) {
155
+ if (rs.next() && rs.getInt(1) == 0) {
156
+ effectiveModification = "update";
157
+ }
158
+ }
159
+ }
160
+ }
161
+ modification = effectiveModification;`
162
+ }
163
+ parentId = java.util.UUID.randomUUID().toString();
164
+ String insertSQL = "INSERT INTO sap_changelog_Changes (ID, PARENT_ID, ATTRIBUTE, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), 'cds.Composition', ?, ?)";
165
+ try (PreparedStatement stmt = conn.prepareStatement(insertSQL)) {
166
+ stmt.setString(1, parentId);
167
+ stmt.setString(2, ${grandParentCompositionInfo ? 'parentChangelogId' : 'null'});
168
+ stmt.setString(3, "${compositionFieldName}");
169
+ stmt.setString(4, "${parentEntityName}");
170
+ stmt.setString(5, parentEntityKey);
171
+ stmt.setString(6, objectID);
172
+ stmt.setString(7, modification);
173
+ stmt.setLong(8, transactionId);
174
+ stmt.executeUpdate();
175
+ }
176
+ }
177
+
178
+ return parentId;
179
+ }
180
+
181
+ private long getTransactionId(Connection conn) throws SQLException {
182
+ try (PreparedStatement stmt = conn.prepareStatement("SELECT TRANSACTION_ID()")) {
183
+ try (ResultSet rs = stmt.executeQuery()) {
184
+ if (rs.next()) {
185
+ return rs.getLong(1);
186
+ }
187
+ }
188
+ }
189
+ return 0;
190
+ }
191
+ `
192
+ : '';
193
+
194
+ return `
195
+ import org.h2.tools.TriggerAdapter;
196
+ import java.sql.Connection;
197
+ import java.sql.ResultSet;
198
+ import java.sql.PreparedStatement;
199
+ import java.sql.SQLException;
200
+ import java.util.Objects;
201
+
202
+ @CODE
203
+ TriggerAdapter create() {
204
+ return new TriggerAdapter() {
205
+ private boolean shouldSkipChangeTracking(Connection conn) throws SQLException {
206
+ try (PreparedStatement stmt = conn.prepareStatement("SELECT @${CT_SKIP_VAR}")) {
207
+ try (ResultSet rs = stmt.executeQuery()) {
208
+ if (rs.next()) {
209
+ String value = rs.getString(1);
210
+ if ("true".equals(value)) return true;
211
+ }
212
+ }
213
+ }
214
+ try (PreparedStatement stmt = conn.prepareStatement("SELECT @${entitySkipVar}")) {
215
+ try (ResultSet rs = stmt.executeQuery()) {
216
+ if (rs.next()) {
217
+ String value = rs.getString(1);
218
+ if ("true".equals(value)) return true;
219
+ }
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+
225
+ private boolean shouldSkipElement(Connection conn, String varName) throws SQLException {
226
+ try (PreparedStatement stmt = conn.prepareStatement("SELECT @" + varName)) {
227
+ try (ResultSet rs = stmt.executeQuery()) {
228
+ if (rs.next()) {
229
+ String value = rs.getString(1);
230
+ if ("true".equals(value)) return true;
231
+ }
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
237
+ private String getLocale(Connection conn) throws SQLException {
238
+ try (PreparedStatement stmt = conn.prepareStatement("SELECT @\\$user.locale")) {
239
+ try (ResultSet rs = stmt.executeQuery()) {
240
+ if (rs.next()) {
241
+ return rs.getString(1);
242
+ }
243
+ }
244
+ }
245
+ return null;
246
+ }
247
+
248
+ private boolean hasExistingCompositionEntry(Connection conn, String entityName, String entityKey, String attribute) throws SQLException {
249
+ String sql = "SELECT 1 FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ? AND ATTRIBUTE = ? AND VALUEDATATYPE = 'cds.Composition' AND TRANSACTIONID = TRANSACTION_ID()";
250
+ try (PreparedStatement stmt = conn.prepareStatement(sql)) {
251
+ stmt.setString(1, entityName);
252
+ stmt.setString(2, entityKey);
253
+ stmt.setString(3, attribute);
254
+ try (ResultSet rs = stmt.executeQuery()) {
255
+ return rs.next();
256
+ }
257
+ }
258
+ }
259
+ ${grandParentHelper}${parentIdHelper}
260
+ @Override
261
+ public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) throws SQLException {
262
+ if (shouldSkipChangeTracking(conn)) {
263
+ return;
264
+ }
265
+ String locale = getLocale(conn);
266
+
267
+ boolean isInsert = oldRow == null;
268
+ boolean isDelete = newRow == null;
269
+ boolean isUpdate = !isInsert && !isDelete;
270
+
271
+ if (isInsert) {
272
+ ${createBody}
273
+ } else if (isUpdate) {
274
+ ${updateBody}
275
+ } else if (isDelete) {
276
+ ${deleteBody}
277
+ }
278
+ }
279
+ };
280
+ }`;
281
+ }
282
+
283
+ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
284
+ const reference = 'newRow';
285
+
286
+ // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
287
+ const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "create";' : '';
288
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
289
+
290
+ // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
291
+ let parentIdSetup = '';
292
+ if (compositionParentInfo) {
293
+ if (grandParentCompositionInfo) {
294
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "create", parentChangelogId);`;
295
+ } else {
296
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "create");`;
297
+ }
298
+ }
299
+
300
+ // Handle composition-only triggers (no tracked columns)
301
+ if (columns.length === 0 && compositionParentInfo) {
302
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
303
+ }
304
+
305
+ const columnBlocks = columns
306
+ .map((col) => {
307
+ // Prepare Value Expression
308
+ const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
309
+ const labelRes = _prepareLabelExpression(col, reference, model); // label expression
310
+
311
+ // SQL Statement - include PARENT_ID if composition parent info exists
312
+ const insertSQL = compositionParentInfo
313
+ ? `INSERT INTO sap_changelog_Changes
314
+ (ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
315
+ VALUES
316
+ (RANDOM_UUID(), ?, '${col.name}', NULL, ${sqlExpr}, NULL, ${labelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'create', TRANSACTION_ID())`
317
+ : `INSERT INTO sap_changelog_Changes
318
+ (ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
319
+ VALUES
320
+ (RANDOM_UUID(), '${col.name}', NULL, ${sqlExpr}, NULL, ${labelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'create', TRANSACTION_ID())`;
321
+
322
+ // Bindings: ParentId (if applicable) + NewVal Bindings + Standard Metadata Bindings
323
+ const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
324
+
325
+ const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
326
+
327
+ // Element skip check variable name
328
+ const elementSkipVar = getElementSkipVarName(entity.name, col.name);
329
+
330
+ // Null Check Wrapper + Element Skip Check
331
+ const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
332
+ return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
333
+ ${tryBlock}
334
+ }`;
335
+ })
336
+ .join('\n');
337
+
338
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
339
+ }
340
+
341
+ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
342
+ const reference = 'newRow';
343
+
344
+ // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
345
+ const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "update";' : '';
346
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
347
+
348
+ // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
349
+ let parentIdSetup = '';
350
+ if (compositionParentInfo) {
351
+ if (grandParentCompositionInfo) {
352
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "update", parentChangelogId);`;
353
+ } else {
354
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "update");`;
355
+ }
356
+ }
357
+
358
+ // Handle composition-only triggers (no tracked columns)
359
+ if (columns.length === 0 && compositionParentInfo) {
360
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
361
+ }
362
+
363
+ const columnBlocks = columns
364
+ .map((col) => {
365
+ // Prepare new and old Value
366
+ const newRes = _prepareValueExpression(col, 'newRow');
367
+ const oldRes = _prepareValueExpression(col, 'oldRow');
368
+ // Prepare new and old Label (lookup values if col.alt exists)
369
+ const newLabelRes = _prepareLabelExpression(col, 'newRow', model);
370
+ const oldLabelRes = _prepareLabelExpression(col, 'oldRow', model);
371
+
372
+ // Check column values from ResultSet for Change Logic
373
+ let checkCols = [col.name];
374
+ if (col.foreignKeys && col.foreignKeys.length > 0) {
375
+ checkCols = col.foreignKeys.map((fk) => `${col.name}_${fk}`);
376
+ } else if (col.on && col.on.length > 0) {
377
+ checkCols = col.on.map((m) => m.foreignKeyField);
378
+ }
379
+
380
+ // Generate the Java condition: (col1_new != col1_old || col2_new != col2_old)
381
+ const changeCheck = checkCols.map((dbCol) => `!Objects.equals(newRow.getObject("${dbCol}"), oldRow.getObject("${dbCol}"))`).join(' || ');
382
+
383
+ // SQL Statement - include PARENT_ID if composition parent info exists
384
+ const insertSQL = compositionParentInfo
385
+ ? `INSERT INTO sap_changelog_Changes
386
+ (ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
387
+ VALUES
388
+ (RANDOM_UUID(), ?, '${col.name}', ${oldRes.sqlExpr}, ${newRes.sqlExpr}, ${oldLabelRes.sqlExpr}, ${newLabelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'update', TRANSACTION_ID())`
389
+ : `INSERT INTO sap_changelog_Changes
390
+ (ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
391
+ VALUES
392
+ (RANDOM_UUID(), '${col.name}', ${oldRes.sqlExpr}, ${newRes.sqlExpr}, ${oldLabelRes.sqlExpr}, ${newLabelRes.sqlExpr}, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'update', TRANSACTION_ID())`;
393
+
394
+ // Bindings: ParentId (if applicable) + OldVal + NewVal + Metadata
395
+ const allBindings = compositionParentInfo
396
+ ? ['parentId', ...oldRes.bindings, ...newRes.bindings, ...oldLabelRes.bindings, ...newLabelRes.bindings, 'entityName', 'entityKey', 'objectID']
397
+ : [...oldRes.bindings, ...newRes.bindings, ...oldLabelRes.bindings, ...newLabelRes.bindings, 'entityName', 'entityKey', 'objectID'];
398
+
399
+ // Element skip check variable name
400
+ const elementSkipVar = getElementSkipVarName(entity.name, col.name);
401
+
402
+ // For composition-of-one columns, add deduplication check to prevent duplicate entries
403
+ // when child trigger has already created a composition entry for this transaction
404
+ const compositionCheck = col.type === 'cds.Composition' ? ` && !hasExistingCompositionEntry(conn, entityName, entityKey, "${col.name}")` : '';
405
+
406
+ return `if ((${changeCheck}) && !shouldSkipElement(conn, "${elementSkipVar}")${compositionCheck}) {
407
+ ${_wrapInTryCatch(insertSQL, allBindings)}
408
+ }`;
409
+ })
410
+ .join('\n');
411
+
412
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
413
+ }
414
+
415
+ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
416
+ const reference = 'oldRow';
417
+
418
+ // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
419
+ const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
420
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
421
+
422
+ // First delete existing changelogs for this entity
423
+ const deleteSQL = `DELETE FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ?`;
424
+ const deleteBlock = _wrapInTryCatch(deleteSQL, ['entityName', 'entityKey']);
425
+
426
+ // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
427
+ let parentIdSetup = '';
428
+ if (compositionParentInfo) {
429
+ if (grandParentCompositionInfo) {
430
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete", parentChangelogId);`;
431
+ } else {
432
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete");`;
433
+ }
434
+ }
435
+
436
+ // Handle composition-only triggers (no tracked columns)
437
+ if (columns.length === 0 && compositionParentInfo) {
438
+ return `${modificationTypeSetup}\n${keysCalc}\n${deleteBlock}\n${parentIdSetup}`;
439
+ }
440
+
441
+ // Then insert delete changelog entries for each tracked column
442
+ const columnBlocks = columns
443
+ .map((col) => {
444
+ // Prepare Old Value (raw FK value)
445
+ const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
446
+ // Prepare Old Label (lookup value if col.alt exists)
447
+ const labelRes = _prepareLabelExpression(col, reference, model);
448
+
449
+ // SQL Statement - include PARENT_ID if composition parent info exists
450
+ const insertSQL = compositionParentInfo
451
+ ? `INSERT INTO sap_changelog_Changes
452
+ (ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
453
+ VALUES
454
+ (RANDOM_UUID(), ?, '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`
455
+ : `INSERT INTO sap_changelog_Changes
456
+ (ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
457
+ VALUES
458
+ (RANDOM_UUID(), '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`;
459
+
460
+ const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
461
+
462
+ const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
463
+
464
+ // Element skip check variable name
465
+ const elementSkipVar = getElementSkipVarName(entity.name, col.name);
466
+
467
+ // Null Check Wrapper + Element Skip Check
468
+ const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
469
+ return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
470
+ ${tryBlock}
471
+ }`;
472
+ })
473
+ .join('\n');
474
+
475
+ return `${modificationTypeSetup}
476
+ ${keysCalc}
477
+ ${deleteBlock}
478
+ ${parentIdSetup}
479
+ ${columnBlocks}`;
480
+ }
481
+
482
+ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
483
+ const reference = 'oldRow';
484
+
485
+ // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
486
+ const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
487
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
488
+
489
+ // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
490
+ let parentIdSetup = '';
491
+ if (compositionParentInfo) {
492
+ if (grandParentCompositionInfo) {
493
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete", parentChangelogId);`;
494
+ } else {
495
+ parentIdSetup = `String parentId = ensureCompositionParentEntry(conn, parentEntityKey, parentObjectID, "delete");`;
496
+ }
497
+ }
498
+
499
+ // Handle composition-only triggers (no tracked columns)
500
+ if (columns.length === 0 && compositionParentInfo) {
501
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}`;
502
+ }
503
+
504
+ const columnBlocks = columns
505
+ .map((col) => {
506
+ // Prepare Old Value (raw FK value)
507
+ const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
508
+ // Prepare Old Label (lookup value if col.alt exists)
509
+ const labelRes = _prepareLabelExpression(col, reference, model);
510
+
511
+ // SQL Statement - include PARENT_ID if composition parent info exists
512
+ const insertSQL = compositionParentInfo
513
+ ? `INSERT INTO sap_changelog_Changes
514
+ (ID, PARENT_ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
515
+ VALUES
516
+ (RANDOM_UUID(), ?, '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`
517
+ : `INSERT INTO sap_changelog_Changes
518
+ (ID, ATTRIBUTE, VALUECHANGEDFROM, VALUECHANGEDTO, VALUECHANGEDFROMLABEL, VALUECHANGEDTOLABEL, ENTITY, ENTITYKEY, OBJECTID, CREATEDAT, CREATEDBY, VALUEDATATYPE, MODIFICATION, TRANSACTIONID)
519
+ VALUES
520
+ (RANDOM_UUID(), '${col.name}', ${sqlExpr}, NULL, ${labelRes.sqlExpr}, NULL, ?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_USER(), '${col.type}', 'delete', TRANSACTION_ID())`;
521
+
522
+ const allBindings = compositionParentInfo ? ['parentId', ...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'] : [...bindings, ...labelRes.bindings, 'entityName', 'entityKey', 'objectID'];
523
+
524
+ const tryBlock = _wrapInTryCatch(insertSQL, allBindings);
525
+
526
+ // Element skip check variable name
527
+ const elementSkipVar = getElementSkipVarName(entity.name, col.name);
528
+
529
+ // Null Check Wrapper + Element Skip Check
530
+ const valExpression = bindings.map((b) => b).join(' != null && ') + ' != null';
531
+ return `if ((${valExpression}) && !shouldSkipElement(conn, "${elementSkipVar}")) {
532
+ ${tryBlock}
533
+ }`;
534
+ })
535
+ .join('\n');
536
+
537
+ return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
538
+ }
539
+
540
+ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
541
+ // extract keys for entity (entity.keys is undefined)
542
+ let keys = utils.extractKeys(entity.keys);
543
+ const entityKeyExp = keys.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
544
+
545
+ const objectIDs = utils.getObjectIDs(entity, model);
546
+ const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref, model);
547
+
548
+ // Add parent key calculation for composition parent linking
549
+ let parentKeyBlock = '';
550
+ let parentObjectIDBlock = '';
551
+ let parentChangelogLookupBlock = '';
552
+ if (compositionParentInfo) {
553
+ const { parentKeyBinding } = compositionParentInfo;
554
+
555
+ // Handle composition of one (parent has FK to child - need reverse lookup)
556
+ if (parentKeyBinding.type === 'compositionOfOne') {
557
+ const { compositionName, childKeys } = parentKeyBinding;
558
+ const parentEntity = model.definitions[compositionParentInfo.parentEntityName];
559
+ const parentKeys = utils.extractKeys(parentEntity.keys);
560
+
561
+ // Build FK field names and WHERE clause for reverse lookup
562
+ const parentFKFields = childKeys.map((k) => `${compositionName}_${k}`);
563
+ const whereClause = parentFKFields.map((fk) => `${fk} = ?`).join(' AND ');
564
+ const selectColumns = parentKeys.join(" || '||' || ");
565
+ const selectSQL = `SELECT ${selectColumns} FROM ${utils.transformName(compositionParentInfo.parentEntityName)} WHERE ${whereClause}`;
566
+ const bindings = childKeys.map((ck) => `${ref}.getString("${ck}")`);
567
+
568
+ parentKeyBlock = `String parentEntityKey = null;
569
+ try (PreparedStatement stmtPK = conn.prepareStatement("${selectSQL.replace(/"/g, '\\"')}")) {
570
+ ${bindings.map((b, i) => `stmtPK.setString(${i + 1}, ${b});`).join('\n ')}
571
+ try (ResultSet rsPK = stmtPK.executeQuery()) {
572
+ if (rsPK.next()) {
573
+ parentEntityKey = rsPK.getString(1);
574
+ }
575
+ }
576
+ }`;
577
+ } else {
578
+ // Standard composition of many: child has FK to parent
579
+ const parentKeyExp = parentKeyBinding.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
580
+ parentKeyBlock = `String parentEntityKey = ${parentKeyExp};`;
581
+ }
582
+
583
+ // Compute parent objectID (the display name of the composition parent entity)
584
+ const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity, model);
585
+ parentObjectIDBlock = parentObjectIDCalcBlock;
586
+
587
+ if (grandParentCompositionInfo && !parentKeyBinding.type) {
588
+ const { grandParentKeyBinding } = grandParentCompositionInfo;
589
+ const parentEntity = model.definitions[compositionParentInfo.parentEntityName];
590
+ const parentKeys = utils.extractKeys(parentEntity.keys);
591
+
592
+ // Build SQL to look up grandparent key from parent entity
593
+ const grandParentKeyLookupSQL = grandParentKeyBinding.map((k) => k).join(" || '||' || ");
594
+ const parentTableName = utils.transformName(compositionParentInfo.parentEntityName);
595
+ const parentWhereClause = parentKeys.map((pk) => `${pk} = ?`).join(' AND ');
596
+
597
+ parentChangelogLookupBlock = `
598
+ String parentChangelogId = null;
599
+ String grandParentKeySQL = "SELECT ${grandParentKeyLookupSQL} FROM ${parentTableName} WHERE ${parentWhereClause}";
600
+ String grandParentKey = null;
601
+ try (PreparedStatement gpKeyStmt = conn.prepareStatement(grandParentKeySQL)) {
602
+ ${parentKeyBinding.map((k, i) => `gpKeyStmt.setString(${i + 1}, ${ref}.getString("${k}"));`).join('\n')}
603
+ try (ResultSet gpKeyRs = gpKeyStmt.executeQuery()) {
604
+ if (gpKeyRs.next()) {
605
+ grandParentKey = gpKeyRs.getString(1);
606
+ }
607
+ }
608
+ }
609
+ if (grandParentKey != null) {
610
+ parentChangelogId = ensureGrandParentCompositionEntry(conn, grandParentKey, parentObjectID);
611
+ }`;
612
+ }
613
+ }
614
+
615
+ return `
616
+ String entityName = "${entity.name}";
617
+ String entityKey = ${entityKeyExp};
618
+ ${objectIDBlock}
619
+ ${parentKeyBlock}
620
+ ${parentObjectIDBlock}
621
+ ${parentChangelogLookupBlock}
622
+ `;
623
+ }
624
+
625
+ function _prepareValueExpression(col, rowVar) {
626
+ // REVISIT
627
+ if (col.type === 'cds.Boolean') {
628
+ const val = `${rowVar}.getString("${col.name}")`;
629
+ return {
630
+ sqlExpr: `CASE WHEN ? IN ('1', 'TRUE', 'true') THEN 'true' WHEN ? IN ('0', 'FALSE', 'false') THEN 'false' ELSE NULL END`,
631
+ bindings: [val, val]
632
+ };
633
+ }
634
+
635
+ if (col.target && col.foreignKeys) {
636
+ if (col.foreignKeys.length === 1) {
637
+ // Single foreign key
638
+ return {
639
+ sqlExpr: '?',
640
+ bindings: [`${rowVar}.getString("${col.name}_${col.foreignKeys[0]}")`]
641
+ };
642
+ } else {
643
+ // Composite key handling (concatenation)
644
+ const expr = col.foreignKeys.map(() => '?').join(" || ' ' || ");
645
+ const binds = col.foreignKeys.map((fk) => `${rowVar}.getString("${col.name}_${fk}")`);
646
+ return { sqlExpr: expr, bindings: binds };
647
+ }
648
+ }
649
+
650
+ if (col.target && col.on) {
651
+ if (col.on.length === 1) {
652
+ return {
653
+ sqlExpr: '?',
654
+ bindings: [`${rowVar}.getString("${col.on[0].foreignKeyField}")`]
655
+ };
656
+ } else {
657
+ const expr = col.on.map(() => '?').join(" || ' ' || ");
658
+ const binds = col.on.map((m) => `${rowVar}.getString("${m.foreignKeyField}")`);
659
+ return { sqlExpr: expr, bindings: binds };
660
+ }
661
+ }
662
+
663
+ // Scalar value - apply truncation for String and LargeString types
664
+ if (col.type === 'cds.String' || col.type === 'cds.LargeString') {
665
+ return {
666
+ sqlExpr: "CASE WHEN LENGTH(?) > 5000 THEN LEFT(?, 4997) || '...' ELSE ? END",
667
+ bindings: [`${rowVar}.getString("${col.name}")`, `${rowVar}.getString("${col.name}")`, `${rowVar}.getString("${col.name}")`]
668
+ };
669
+ }
670
+
671
+ return {
672
+ sqlExpr: '?',
673
+ bindings: [`${rowVar}.getString("${col.name}")`]
674
+ };
675
+ }
676
+
677
+ // Returns label expression for a column
678
+ function _prepareLabelExpression(col, rowVar, model) {
679
+ if (col.target && col.alt) {
680
+ const { sql, bindings } = handleAssocLookup(col, rowVar, model);
681
+ return { sqlExpr: sql, bindings: bindings };
682
+ }
683
+ // No label for scalars or associations without @changelog override
684
+ return { sqlExpr: 'NULL', bindings: [] };
685
+ }
686
+
687
+ function _wrapInTryCatch(sql, bindings) {
688
+ // Escapes quotes for Java String
689
+ const cleanSql = sql.replace(/"/g, '\\"').replace(/\n/g, ' ');
690
+
691
+ const setParams = bindings.map((b, i) => `stmt.setString(${i + 1}, ${b});`).join('\n ');
692
+
693
+ return `try (PreparedStatement stmt = conn.prepareStatement("${cleanSql}")) {
694
+ ${setParams}
695
+ stmt.executeUpdate();
696
+ }`;
697
+ }
698
+
699
+ function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
700
+ // fallback to entity name (will be translated via i18nKeys in ChangeView)
701
+ if (!objectIDs || objectIDs.length === 0) {
702
+ return `String objectID = "${entity.name}";`;
703
+ }
704
+
705
+ // Build SQL Query for the ObjectID string
706
+ const parts = [];
707
+ const bindings = [];
708
+ const keys = utils.extractKeys(entity.keys);
709
+
710
+ for (const oid of objectIDs) {
711
+ if (oid.included) {
712
+ parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
713
+ bindings.push(`${refRow}.getString("${oid.name}")`);
714
+ } else {
715
+ // Sub-select needed (Lookup)
716
+ const where = keys.reduce((acc, k) => {
717
+ acc[k] = { ref: ['?'], param: true };
718
+ return acc;
719
+ }, {});
720
+
721
+ const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
722
+ const sql = `(${_toSQL(query, model)})`;
723
+
724
+ parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
725
+
726
+ // Add bindings for the WHERE clause of this sub-select
727
+ keys.forEach((k) => bindings.push(`${refRow}.getString("${k}")`));
728
+ }
729
+ }
730
+
731
+ // Combine parts into one query that returns a single string
732
+ // H2 Syntax: SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (...)
733
+ const unionSql = parts.join(' UNION ALL ');
734
+ const finalSql = `SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (${unionSql}) AS tmp`;
735
+
736
+ // Return Java Code Block
737
+ return `
738
+ String objectID = entityKey;
739
+ try (PreparedStatement stmtOID = conn.prepareStatement("${finalSql.replace(/"/g, '\\"')}")) {
740
+ ${bindings.map((b, i) => `stmtOID.setString(${i + 1}, ${b});`).join('\n ')}
741
+
742
+ try (ResultSet rsOID = stmtOID.executeQuery()) {
743
+ if (rsOID.next()) {
744
+ String res = rsOID.getString(1);
745
+ if (res != null) objectID = res;
746
+ }
747
+ }
748
+ }`;
749
+ }
750
+
751
+ /**
752
+ * Generates Java code to compute the composition parent's objectID (display name).
753
+ * This is used when a child entity has a tracked composition parent — the parent's
754
+ * changelog entry needs a meaningful objectID rather than just the key.
755
+ */
756
+ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity, model) {
757
+ if (!rootObjectIDs || rootObjectIDs.length === 0) {
758
+ const rootEntityName = rootEntity ? rootEntity.name : '';
759
+ return `String parentObjectID = "${rootEntityName}";`;
760
+ }
761
+
762
+ // Build SQL Query for the parent's ObjectID string
763
+ const parts = [];
764
+ const bindings = [];
765
+
766
+ // Check for composition of one scenario
767
+ const binding = childEntity ? utils.getRootBinding(childEntity, rootEntity) : null;
768
+ const isCompositionOfOne = binding && binding.type === 'compositionOfOne';
769
+
770
+ for (const oid of rootObjectIDs) {
771
+ if (oid.included && !isCompositionOfOne) {
772
+ parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
773
+ bindings.push(`${refRow}.getString("${oid.name}")`);
774
+ } else {
775
+ // Sub-select needed (Lookup)
776
+ let where;
777
+ if (isCompositionOfOne) {
778
+ // For composition of one, use the backlink pattern
779
+ where = binding.childKeys.reduce((acc, ck) => {
780
+ acc[`${binding.compositionName}_${ck}`] = { ref: ['?'], param: true };
781
+ return acc;
782
+ }, {});
783
+ } else {
784
+ const rootKeys = utils.extractKeys(rootEntity.keys);
785
+ where = rootKeys.reduce((acc, k) => {
786
+ acc[k] = { ref: ['?'], param: true };
787
+ return acc;
788
+ }, {});
789
+ }
790
+
791
+ const targetEntity = isCompositionOfOne ? binding.rootEntityName : rootEntity.name;
792
+ const query = SELECT.one.from(targetEntity).columns(oid.name).where(where);
793
+ const sql = `(${_toSQL(query, model)})`;
794
+
795
+ parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
796
+
797
+ // Add bindings for the WHERE clause of this sub-select
798
+ if (isCompositionOfOne) {
799
+ binding.childKeys.forEach((ck) => bindings.push(`${refRow}.getString("${ck}")`));
800
+ } else {
801
+ const rootKeys = utils.extractKeys(rootEntity.keys);
802
+ rootKeys.forEach((k) => bindings.push(`${refRow}.getString("${k}")`));
803
+ }
804
+ }
805
+ }
806
+
807
+ // Combine parts into one query that returns a single string
808
+ // H2 Syntax: SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (...)
809
+ const unionSql = parts.join(' UNION ALL ');
810
+ const finalSql = `SELECT GROUP_CONCAT(val SEPARATOR ', ') FROM (${unionSql}) AS tmp`;
811
+
812
+ // Return Java Code Block
813
+ return `
814
+ String parentObjectID = entityKey;
815
+ try (PreparedStatement stmtPOID = conn.prepareStatement("${finalSql.replace(/"/g, '\\"')}")) {
816
+ ${bindings.map((b, i) => `stmtPOID.setString(${i + 1}, ${b});`).join('\n ')}
817
+
818
+ try (ResultSet rsPOID = stmtPOID.executeQuery()) {
819
+ if (rsPOID.next()) {
820
+ String res = rsPOID.getString(1);
821
+ if (res != null) parentObjectID = res;
822
+ }
823
+ }
824
+ }`;
825
+ }
826
+
827
+ module.exports = {
828
+ _generateJavaMethod,
829
+ _generateCreateBody,
830
+ _generateUpdateBody,
831
+ _generateDeleteBody,
832
+ _generateDeleteBodyPreserve
833
+ };