@cap-js/change-tracking 2.0.0-beta.1 → 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.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
6
 
7
- ## Version 2.0.0 - TBD
7
+ ## Version 2.0.0-beta.3 - tbd
8
8
 
9
9
  ### Added
10
10
 
@@ -12,7 +12,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
12
12
 
13
13
  ### Changed
14
14
 
15
- ## Version 2.0.0-beta - 27.02.26
15
+ ## Version 2.0.0-beta.2 - 11.03.26
16
+
17
+ ### Fixed
18
+ - Fixed a server crash when resolving table names
19
+ - Support entity level `@changelog` annotation where no explicit elements for the object ID are defined
20
+ - Trigger generation works again for MTX scenarios
21
+
22
+ ### Changed
23
+ - Improved performance when quering changes
24
+
25
+
26
+ ## Version 2.0.0-beta.1 - 06.03.26
16
27
 
17
28
  ### Added
18
29
  - Trigger generation for SQLite, HANA, Postgres and H2 to perform change tracking on a database level
@@ -37,10 +37,6 @@ Changes.modification.create=Create
37
37
  Changes.modification.update=Update
38
38
  #XFLD: Field label
39
39
  Changes.modification.delete=Delete
40
- #XFLD: Field label
41
- Changes.createdAt=Changed On
42
- #XFLD: Field label
43
- Changes.createdBy=Changed By
44
40
 
45
41
  ## Change History Table##
46
42
  ########################################
package/cds-plugin.js CHANGED
@@ -9,8 +9,10 @@ const { registerHDICompilerHook } = require('./lib/hana/register.js');
9
9
 
10
10
  cds.on('loaded', enhanceModel);
11
11
  cds.on('listening', registerSessionVariableHandlers);
12
- cds.once('served', deploySQLiteTriggers);
13
- cds.once('served', deployPostgresLabels);
12
+ cds.once('served', async () => {
13
+ await deploySQLiteTriggers();
14
+ await deployPostgresLabels();
15
+ });
14
16
 
15
17
  registerH2CompilerHook();
16
18
  registerPostgresCompilerHook();
package/index.cds CHANGED
@@ -17,8 +17,8 @@ entity aspect @(UI.Facets: [{
17
17
  @UI.PartOfPreview: false
18
18
  }]) {
19
19
  changes : Association to many ChangeView
20
- on changes.entityKey = ID
21
- and changes.entity = 'ENTITY';
20
+ on changes.entityKey = ID
21
+ and changes.entity = 'ENTITY';
22
22
  key ID : String;
23
23
  }
24
24
 
@@ -31,73 +31,60 @@ entity aspect @(UI.Facets: [{
31
31
  @readonly
32
32
  @cds.autoexpose
33
33
  view ChangeView as
34
- select from Changes as change {
35
- key change.ID @UI.Hidden,
36
- *,
37
- COALESCE(
38
- (
39
- select text from i18nKeys
40
- where
41
- ID = change.attribute
42
- and locale = $user.locale
43
- ), (
44
- select text from i18nKeys
45
- where
46
- ID = change.attribute
47
- and locale = 'en'
48
- ), change.attribute
49
- ) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
50
- COALESCE(
51
- (
52
- select text from i18nKeys
53
- where
54
- ID = change.entity
55
- and locale = $user.locale
56
- ), (
57
- select text from i18nKeys
58
- where
59
- ID = change.entity
60
- and locale = 'en'
61
- ), change.entity
62
- ) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
63
- COALESCE(
64
- (
65
- select text from i18nKeys
66
- where
67
- ID = change.modification
68
- and locale = $user.locale
69
- ), (
70
- select text from i18nKeys
71
- where
72
- ID = change.modification
73
- and locale = 'en'
74
- ), change.modification
75
- ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
76
- COALESCE(
77
- (
78
- select text from i18nKeys
79
- where
80
- ID = change.objectID
81
- and locale = $user.locale
82
- ), (
83
- select text from i18nKeys
84
- where
85
- ID = change.objectID
86
- and locale = 'en'
87
- ), change.objectID
88
- ) as objectID : String(24) @title: '{i18n>Changes.objectID}',
89
- COALESCE(
90
- change.valueChangedFromLabel, change.valueChangedFrom
91
- ) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
92
- COALESCE(
93
- change.valueChangedToLabel, change.valueChangedTo
94
- ) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
95
-
96
- // For the hierarchy
97
- null as LimitedDescendantCount : Int16 @UI.Hidden,
98
- null as DistanceFromRoot : Int16 @UI.Hidden,
99
- null as DrillState : String @UI.Hidden,
100
- null as LimitedRank : Int16 @UI.Hidden,
34
+ select from Changes as change
35
+ left outer join i18nKeys as attributeI18n
36
+ on attributeI18n.ID = change.attribute
37
+ and (
38
+ attributeI18n.locale = $user.locale
39
+ or attributeI18n.locale = 'en'
40
+ )
41
+ left outer join i18nKeys as entityI18n
42
+ on entityI18n.ID = change.entity
43
+ and (
44
+ entityI18n.locale = $user.locale
45
+ or entityI18n.locale = 'en'
46
+ )
47
+ left outer join i18nKeys as modificationI18n
48
+ on modificationI18n.ID = change.modification
49
+ and (
50
+ modificationI18n.locale = $user.locale
51
+ or modificationI18n.locale = 'en'
52
+ )
53
+ {
54
+ key change.ID @UI.Hidden,
55
+ change.parent: redirected to ChangeView,
56
+ change.children: redirected to ChangeView,
57
+ change.attribute,
58
+ change.valueChangedFrom,
59
+ change.valueChangedTo,
60
+ change.entity,
61
+ change.entityKey,
62
+ change.objectID,
63
+ change.modification,
64
+ change.valueDataType,
65
+ change.createdAt,
66
+ change.createdBy,
67
+ change.transactionID,
68
+ COALESCE(
69
+ attributeI18n.text, change.attribute
70
+ ) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
71
+ COALESCE(
72
+ entityI18n.text, change.entity
73
+ ) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
74
+ COALESCE(
75
+ modificationI18n.text, change.modification
76
+ ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
77
+ COALESCE(
78
+ change.valueChangedFromLabel, change.valueChangedFrom
79
+ ) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
80
+ COALESCE(
81
+ change.valueChangedToLabel, change.valueChangedTo
82
+ ) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
83
+ // For the hierarchy
84
+ null as LimitedDescendantCount : Int16 @UI.Hidden,
85
+ null as DistanceFromRoot : Int16 @UI.Hidden,
86
+ null as DrillState : String @UI.Hidden,
87
+ null as LimitedRank : Int16 @UI.Hidden,
101
88
  };
102
89
 
103
90
  entity i18nKeys {
@@ -159,46 +146,46 @@ annotate ChangeView with @(UI: {
159
146
  },
160
147
  LineItem : [
161
148
  {
162
- Value : modificationLabel,
163
- @UI.Importance : #Low
149
+ Value : modificationLabel,
150
+ @UI.Importance: #Low
164
151
  },
165
152
  {
166
- Value : entityLabel,
167
- @UI.Importance : #Medium
153
+ Value : entityLabel,
154
+ @UI.Importance: #Medium
168
155
  },
169
156
  {
170
- Value : objectID,
171
- @UI.Importance : #Medium
157
+ Value : objectID,
158
+ @UI.Importance: #Medium
172
159
  },
173
160
  {
174
- Value : attributeLabel,
175
- @UI.Importance : #Medium
161
+ Value : attributeLabel,
162
+ @UI.Importance: #Medium
176
163
  },
177
164
  {
178
- Value : valueChangedToLabel,
179
- @UI.Importance : #High
165
+ Value : valueChangedToLabel,
166
+ @UI.Importance: #High
180
167
  },
181
168
  {
182
- Value : valueChangedFromLabel,
183
- @UI.Importance : #High
169
+ Value : valueChangedFromLabel,
170
+ @UI.Importance: #High
184
171
  },
185
172
  {
186
- Value : createdAt,
187
- @UI.Importance : #Low
173
+ Value : createdAt,
174
+ @UI.Importance: #Low
188
175
  },
189
176
  {
190
- Value : createdBy,
191
- @UI.Importance : #High
177
+ Value : createdBy,
178
+ @UI.Importance: #High
192
179
  },
193
180
  ],
194
181
  DeleteHidden : true,
195
182
  }) {
196
183
  valueChangedFrom @UI.Hidden;
197
- valueChangedTo @UI.Hidden;
198
- parent @UI.Hidden;
199
- entityKey @UI.Hidden;
200
- entity @UI.Hidden;
201
- attribute @UI.Hidden;
184
+ valueChangedTo @UI.Hidden;
185
+ parent @UI.Hidden;
186
+ entityKey @UI.Hidden;
187
+ entity @UI.Hidden;
188
+ attribute @UI.Hidden;
202
189
  };
203
190
 
204
191
  annotate ChangeView with @(
@@ -4,29 +4,21 @@ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
4
4
 
5
5
  const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
6
6
 
7
- let SQLiteCQN2SQL;
8
- let model;
7
+ const _cqn2sqlCache = new WeakMap();
9
8
 
10
- function setModel(m) {
11
- model = m;
12
- SQLiteCQN2SQL = null;
13
- }
14
-
15
- function getModel() {
16
- return model;
17
- }
18
-
19
- function _toSQL(query) {
20
- if (!SQLiteCQN2SQL) {
9
+ function _toSQL(query, model) {
10
+ let cqn2sql = _cqn2sqlCache.get(model);
11
+ if (!cqn2sql) {
21
12
  const SQLiteService = require('@cap-js/sqlite');
22
13
  const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
23
- SQLiteCQN2SQL = new TriggerCQN2SQL({ model: model });
14
+ cqn2sql = new TriggerCQN2SQL({ model: model });
15
+ _cqn2sqlCache.set(model, cqn2sql);
24
16
  }
25
17
  const sqlCQN = cqn4sql(query, model);
26
- return SQLiteCQN2SQL.SELECT(sqlCQN);
18
+ return cqn2sql.SELECT(sqlCQN);
27
19
  }
28
20
 
29
- function handleAssocLookup(column, refRow) {
21
+ function handleAssocLookup(column, refRow, model) {
30
22
  let bindings = [];
31
23
  let where = {};
32
24
 
@@ -56,8 +48,8 @@ function handleAssocLookup(column, refRow) {
56
48
  const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
57
49
  const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
58
50
 
59
- const textsSQL = _toSQL(textsQuery);
60
- const baseSQL = _toSQL(baseQuery);
51
+ const textsSQL = _toSQL(textsQuery, model);
52
+ const baseSQL = _toSQL(baseQuery, model);
61
53
 
62
54
  // Add locale binding (fetched from session variable @$user.locale)
63
55
  const textsBindings = [...bindings, 'locale'];
@@ -73,7 +65,7 @@ function handleAssocLookup(column, refRow) {
73
65
  const query = SELECT.one.from(column.target).columns(columns).where(where);
74
66
 
75
67
  return {
76
- sql: `(${_toSQL(query)})`,
68
+ sql: `(${_toSQL(query, model)})`,
77
69
  bindings: bindings
78
70
  };
79
71
  }
@@ -288,12 +280,12 @@ ${grandParentHelper}${parentIdHelper}
288
280
  }`;
289
281
  }
290
282
 
291
- function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
283
+ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
292
284
  const reference = 'newRow';
293
285
 
294
286
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
295
287
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "create";' : '';
296
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
288
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
297
289
 
298
290
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
299
291
  let parentIdSetup = '';
@@ -314,7 +306,7 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
314
306
  .map((col) => {
315
307
  // Prepare Value Expression
316
308
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
317
- const labelRes = _prepareLabelExpression(col, reference); // label expression
309
+ const labelRes = _prepareLabelExpression(col, reference, model); // label expression
318
310
 
319
311
  // SQL Statement - include PARENT_ID if composition parent info exists
320
312
  const insertSQL = compositionParentInfo
@@ -346,12 +338,12 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
346
338
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
347
339
  }
348
340
 
349
- function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
341
+ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
350
342
  const reference = 'newRow';
351
343
 
352
344
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
353
345
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "update";' : '';
354
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
346
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
355
347
 
356
348
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
357
349
  let parentIdSetup = '';
@@ -374,8 +366,8 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
374
366
  const newRes = _prepareValueExpression(col, 'newRow');
375
367
  const oldRes = _prepareValueExpression(col, 'oldRow');
376
368
  // Prepare new and old Label (lookup values if col.alt exists)
377
- const newLabelRes = _prepareLabelExpression(col, 'newRow');
378
- const oldLabelRes = _prepareLabelExpression(col, 'oldRow');
369
+ const newLabelRes = _prepareLabelExpression(col, 'newRow', model);
370
+ const oldLabelRes = _prepareLabelExpression(col, 'oldRow', model);
379
371
 
380
372
  // Check column values from ResultSet for Change Logic
381
373
  let checkCols = [col.name];
@@ -420,12 +412,12 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
420
412
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
421
413
  }
422
414
 
423
- function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
415
+ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
424
416
  const reference = 'oldRow';
425
417
 
426
418
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
427
419
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
428
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
420
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
429
421
 
430
422
  // First delete existing changelogs for this entity
431
423
  const deleteSQL = `DELETE FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ?`;
@@ -452,7 +444,7 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
452
444
  // Prepare Old Value (raw FK value)
453
445
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
454
446
  // Prepare Old Label (lookup value if col.alt exists)
455
- const labelRes = _prepareLabelExpression(col, reference);
447
+ const labelRes = _prepareLabelExpression(col, reference, model);
456
448
 
457
449
  // SQL Statement - include PARENT_ID if composition parent info exists
458
450
  const insertSQL = compositionParentInfo
@@ -487,12 +479,12 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
487
479
  ${columnBlocks}`;
488
480
  }
489
481
 
490
- function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
482
+ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
491
483
  const reference = 'oldRow';
492
484
 
493
485
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
494
486
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
495
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
487
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
496
488
 
497
489
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
498
490
  let parentIdSetup = '';
@@ -514,7 +506,7 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
514
506
  // Prepare Old Value (raw FK value)
515
507
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
516
508
  // Prepare Old Label (lookup value if col.alt exists)
517
- const labelRes = _prepareLabelExpression(col, reference);
509
+ const labelRes = _prepareLabelExpression(col, reference, model);
518
510
 
519
511
  // SQL Statement - include PARENT_ID if composition parent info exists
520
512
  const insertSQL = compositionParentInfo
@@ -545,13 +537,13 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
545
537
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
546
538
  }
547
539
 
548
- function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
540
+ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
549
541
  // extract keys for entity (entity.keys is undefined)
550
542
  let keys = utils.extractKeys(entity.keys);
551
543
  const entityKeyExp = keys.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
552
544
 
553
545
  const objectIDs = utils.getObjectIDs(entity, model);
554
- const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref);
546
+ const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref, model);
555
547
 
556
548
  // Add parent key calculation for composition parent linking
557
549
  let parentKeyBlock = '';
@@ -589,7 +581,7 @@ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, com
589
581
  }
590
582
 
591
583
  // Compute parent objectID (the display name of the composition parent entity)
592
- const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity);
584
+ const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity, model);
593
585
  parentObjectIDBlock = parentObjectIDCalcBlock;
594
586
 
595
587
  if (grandParentCompositionInfo && !parentKeyBinding.type) {
@@ -683,9 +675,9 @@ function _prepareValueExpression(col, rowVar) {
683
675
  }
684
676
 
685
677
  // Returns label expression for a column
686
- function _prepareLabelExpression(col, rowVar) {
678
+ function _prepareLabelExpression(col, rowVar, model) {
687
679
  if (col.target && col.alt) {
688
- const { sql, bindings } = handleAssocLookup(col, rowVar);
680
+ const { sql, bindings } = handleAssocLookup(col, rowVar, model);
689
681
  return { sqlExpr: sql, bindings: bindings };
690
682
  }
691
683
  // No label for scalars or associations without @changelog override
@@ -704,7 +696,7 @@ function _wrapInTryCatch(sql, bindings) {
704
696
  }`;
705
697
  }
706
698
 
707
- function _generateObjectIDCalculation(objectIDs, entity, refRow) {
699
+ function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
708
700
  // fallback to entity name (will be translated via i18nKeys in ChangeView)
709
701
  if (!objectIDs || objectIDs.length === 0) {
710
702
  return `String objectID = "${entity.name}";`;
@@ -727,7 +719,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
727
719
  }, {});
728
720
 
729
721
  const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
730
- const sql = `(${_toSQL(query)})`;
722
+ const sql = `(${_toSQL(query, model)})`;
731
723
 
732
724
  parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
733
725
 
@@ -761,7 +753,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
761
753
  * This is used when a child entity has a tracked composition parent — the parent's
762
754
  * changelog entry needs a meaningful objectID rather than just the key.
763
755
  */
764
- function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity) {
756
+ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity, model) {
765
757
  if (!rootObjectIDs || rootObjectIDs.length === 0) {
766
758
  const rootEntityName = rootEntity ? rootEntity.name : '';
767
759
  return `String parentObjectID = "${rootEntityName}";`;
@@ -798,7 +790,7 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
798
790
 
799
791
  const targetEntity = isCompositionOfOne ? binding.rootEntityName : rootEntity.name;
800
792
  const query = SELECT.one.from(targetEntity).columns(oid.name).where(where);
801
- const sql = `(${_toSQL(query)})`;
793
+ const sql = `(${_toSQL(query, model)})`;
802
794
 
803
795
  parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
804
796
 
@@ -833,8 +825,6 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
833
825
  }
834
826
 
835
827
  module.exports = {
836
- setModel,
837
- getModel,
838
828
  _generateJavaMethod,
839
829
  _generateCreateBody,
840
830
  _generateUpdateBody,
@@ -1,10 +1,9 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
2
  const config = require('@sap/cds').env.requires['change-tracking'];
3
3
  const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
4
- const { setModel, _generateJavaMethod, _generateCreateBody, _generateUpdateBody, _generateDeleteBody, _generateDeleteBodyPreserve } = require('./java-codegen.js');
4
+ const { _generateJavaMethod, _generateCreateBody, _generateUpdateBody, _generateDeleteBody, _generateDeleteBodyPreserve } = require('./java-codegen.js');
5
5
 
6
6
  function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
7
- setModel(csn);
8
7
  const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
9
8
  const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
10
9
  const rootObjectIDs = utils.getObjectIDs(rootEntity, csn, rootMergedAnnotations?.entityAnnotation);
@@ -21,13 +20,13 @@ function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, ro
21
20
  if (!shouldGenerateTriggers) return null;
22
21
 
23
22
  // Generate the Java code for each section
24
- const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
25
- const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
23
+ const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
24
+ const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
26
25
  let deleteBody = '';
27
26
  if (!config?.disableDeleteTracking) {
28
27
  deleteBody = config?.preserveDeletes
29
- ? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo)
30
- : _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
28
+ ? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo)
29
+ : _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo);
31
30
  }
32
31
 
33
32
  // Define the full Create Trigger SQL
@@ -1,14 +1,16 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
- const { getModel, toSQL, compositeKeyExpr, buildGrandParentObjectIDExpr } = require('./sql-expressions.js');
2
+ const { toSQL, compositeKeyExpr, buildGrandParentObjectIDExpr } = 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,11 +20,10 @@ 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(TO_NVARCHAR((${toSQL(query)})), '')`);
23
+ parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query, model)})), '')`);
22
24
  }
23
25
 
24
26
  const concatLogic = parts.join(" || ', ' || ");
25
- const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `:${refRow}.${k}`));
26
27
 
27
28
  return `COALESCE(NULLIF(${concatLogic}, ''), ${rootEntityKeyExpr})`;
28
29
  }
@@ -32,8 +33,7 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
32
33
  * In composition of one, the parent entity has FK to the child (e.g., BookStores.registry_ID -> BookStoreRegistry.ID)
33
34
  * So we need to do a reverse lookup: find the parent record that has FK pointing to this child.
34
35
  */
35
- function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef) {
36
- const model = getModel();
36
+ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model) {
37
37
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
38
38
  const { compositionName, childKeys } = parentKeyBinding;
39
39
 
@@ -101,23 +101,21 @@ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs
101
101
  return { declares, insertSQL, parentEntityName, compositionFieldName, parentKeyExpr };
102
102
  }
103
103
 
104
- function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, grandParentCompositionInfo = null) {
105
- const model = getModel();
104
+ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model, grandParentCompositionInfo = null) {
106
105
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
107
106
 
108
107
  // Handle composition of one (parent has FK to child - need reverse lookup)
109
108
  if (parentKeyBinding.type === 'compositionOfOne') {
110
- return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef);
109
+ return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model);
111
110
  }
112
111
 
113
112
  const parentKeyExpr = compositeKeyExpr(parentKeyBinding.map((k) => `:${rowRef}.${k}`));
114
113
 
115
114
  // Build rootObjectID expression for the parent entity
116
115
  const rootEntity = model.definitions[parentEntityName];
117
- const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef);
116
+ const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef, model);
118
117
 
119
- let declares = 'DECLARE parent_id NVARCHAR(36);';
120
- let insertSQL;
118
+ let declares, insertSQL;
121
119
 
122
120
  if (grandParentCompositionInfo) {
123
121
  // When we have grandparent info, we need to:
@@ -134,7 +132,7 @@ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, mod
134
132
  // Build grandparent objectID expression
135
133
  const grandParentEntity = model.definitions[grandParentEntityName];
136
134
  const grandParentObjectIDs = utils.getObjectIDs(grandParentEntity, model);
137
- const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef) : grandParentKeyExpr;
135
+ const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef, model) : grandParentKeyExpr;
138
136
 
139
137
  // Add grandparent_id to declares
140
138
  declares = 'DECLARE parent_id NVARCHAR(36);\n\tDECLARE grandparent_id NVARCHAR(36);';