@cap-js/change-tracking 2.0.0-beta.5 → 2.0.0-beta.6
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 +16 -1
- package/cds-plugin.js +2 -0
- package/index.cds +30 -36
- package/lib/addMigrationTable.js +59 -0
- package/lib/csn-enhancements/dynamicLocalization.js +1 -1
- package/lib/csn-enhancements/index.js +1 -1
- package/lib/csn-enhancements/timezoneProperties.js +2 -2
- package/lib/hana/migrationTable.js +69 -0
- package/lib/hana/register.js +32 -9
- package/lib/hana/restoreProcedure.js +342 -0
- package/lib/hana/sql-expressions.js +4 -5
- package/lib/postgres/register.js +0 -12
- package/lib/postgres/sql-expressions.js +1 -1
- package/lib/skipHandlers.js +2 -2
- package/lib/sqlite/sql-expressions.js +1 -1
- package/lib/utils/change-tracking.js +6 -0
- package/package.json +3 -2
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-beta.
|
|
7
|
+
## Version 2.0.0-beta.7 - tbd
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -12,6 +12,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
12
12
|
|
|
13
13
|
### Changed
|
|
14
14
|
|
|
15
|
+
## Version 2.0.0-beta.6 - 26.03.26
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Provide detailed plan for v1 to v2 HANA migration
|
|
19
|
+
- Generation of `.hdbmigrationtable` and updating `undeploy.json` via `cds add change-tracking-migration`
|
|
20
|
+
- HANA procedure `SAP_CHANGELOG_RESTORE_BACKLINKS` to restore parent-child hierarchy for composition changes
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Explicit type casts for Date, DateTime, Time, Timestamp and Decimal fields in `ChangeView` to avoid conversion errors
|
|
24
|
+
- Lazy load database adapters to prevent crashes when optional dependencies are not installed
|
|
25
|
+
- Skip changelogs referencing association targets annotated with `@cds.persistence.skip`
|
|
26
|
+
- Cast single entity keys to `cds.String` to prevent type conversion errors
|
|
27
|
+
- Dynamic localization now verifies `.texts` entity existence before attempting localized lookup
|
|
28
|
+
|
|
29
|
+
|
|
15
30
|
## Version 2.0.0-beta.5 - 17.03.26
|
|
16
31
|
|
|
17
32
|
### Added
|
package/cds-plugin.js
CHANGED
package/index.cds
CHANGED
|
@@ -45,17 +45,11 @@ view ChangeView as
|
|
|
45
45
|
key change.ID @UI.Hidden,
|
|
46
46
|
change.parent : redirected to ChangeView,
|
|
47
47
|
change.children : redirected to ChangeView,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
change.objectID,
|
|
54
|
-
change.modification,
|
|
55
|
-
change.valueDataType,
|
|
56
|
-
change.createdAt,
|
|
57
|
-
change.createdBy,
|
|
58
|
-
change.transactionID,
|
|
48
|
+
// Needed to make the * possible
|
|
49
|
+
attributeI18n.locale @UI.Hidden,
|
|
50
|
+
attributeI18n.text @UI.Hidden,
|
|
51
|
+
// * is important to allow for application extensions of Changes
|
|
52
|
+
*,
|
|
59
53
|
COALESCE(
|
|
60
54
|
attributeI18n.text, (
|
|
61
55
|
select text from i18nKeys
|
|
@@ -89,9 +83,9 @@ view ChangeView as
|
|
|
89
83
|
(
|
|
90
84
|
case
|
|
91
85
|
when valueDataType = 'cds.DateTime'
|
|
92
|
-
then COALESCE(
|
|
86
|
+
then cast(COALESCE(
|
|
93
87
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
94
|
-
)
|
|
88
|
+
) as DateTime)
|
|
95
89
|
else null
|
|
96
90
|
end
|
|
97
91
|
) as valueChangedFromLabelDateTime : DateTime @(title: '{i18n>Changes.valueChangedFrom}',
|
|
@@ -99,9 +93,9 @@ view ChangeView as
|
|
|
99
93
|
(
|
|
100
94
|
case
|
|
101
95
|
when valueDataType = 'cds.DateTime' or valueDataType = 'cds.Timestamp'
|
|
102
|
-
then COALESCE(
|
|
96
|
+
then cast(COALESCE(
|
|
103
97
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
104
|
-
)
|
|
98
|
+
) as DateTime)
|
|
105
99
|
else null
|
|
106
100
|
end
|
|
107
101
|
) as valueChangedFromLabelDateTimeWTZ : DateTime @(
|
|
@@ -111,9 +105,9 @@ view ChangeView as
|
|
|
111
105
|
(
|
|
112
106
|
case
|
|
113
107
|
when valueDataType = 'cds.Time'
|
|
114
|
-
then COALESCE(
|
|
108
|
+
then cast(COALESCE(
|
|
115
109
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
116
|
-
)
|
|
110
|
+
) as Time)
|
|
117
111
|
else null
|
|
118
112
|
end
|
|
119
113
|
) as valueChangedFromLabelTime : Time @(title: '{i18n>Changes.valueChangedFrom}',
|
|
@@ -121,9 +115,9 @@ view ChangeView as
|
|
|
121
115
|
(
|
|
122
116
|
case
|
|
123
117
|
when valueDataType = 'cds.Date'
|
|
124
|
-
then COALESCE(
|
|
118
|
+
then cast(COALESCE(
|
|
125
119
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
126
|
-
)
|
|
120
|
+
) as Date)
|
|
127
121
|
else null
|
|
128
122
|
end
|
|
129
123
|
) as valueChangedFromLabelDate : Date @(title: '{i18n>Changes.valueChangedFrom}',
|
|
@@ -131,9 +125,9 @@ view ChangeView as
|
|
|
131
125
|
(
|
|
132
126
|
case
|
|
133
127
|
when valueDataType = 'cds.Timestamp'
|
|
134
|
-
then COALESCE(
|
|
128
|
+
then cast(COALESCE(
|
|
135
129
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
136
|
-
)
|
|
130
|
+
) as Timestamp)
|
|
137
131
|
else null
|
|
138
132
|
end
|
|
139
133
|
) as valueChangedFromLabelTimestamp : Timestamp @(title: '{i18n>Changes.valueChangedFrom}',
|
|
@@ -141,9 +135,9 @@ view ChangeView as
|
|
|
141
135
|
(
|
|
142
136
|
case
|
|
143
137
|
when valueDataType = 'cds.Decimal'
|
|
144
|
-
then COALESCE(
|
|
138
|
+
then cast(COALESCE(
|
|
145
139
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
146
|
-
)
|
|
140
|
+
) as Decimal)
|
|
147
141
|
else null
|
|
148
142
|
end
|
|
149
143
|
) as valueChangedFromLabelDecimal : Decimal @(title: '{i18n>Changes.valueChangedFrom}',
|
|
@@ -157,9 +151,9 @@ view ChangeView as
|
|
|
157
151
|
(
|
|
158
152
|
case
|
|
159
153
|
when valueDataType = 'cds.DateTime'
|
|
160
|
-
then COALESCE(
|
|
154
|
+
then cast(COALESCE(
|
|
161
155
|
change.valueChangedToLabel, change.valueChangedTo
|
|
162
|
-
)
|
|
156
|
+
) as DateTime)
|
|
163
157
|
else null
|
|
164
158
|
end
|
|
165
159
|
) as valueChangedToLabelDateTime : DateTime @(title: '{i18n>Changes.valueChangedTo}',
|
|
@@ -167,9 +161,9 @@ view ChangeView as
|
|
|
167
161
|
(
|
|
168
162
|
case
|
|
169
163
|
when valueDataType = 'cds.DateTime' or valueDataType = 'cds.Timestamp'
|
|
170
|
-
then COALESCE(
|
|
171
|
-
change.
|
|
172
|
-
)
|
|
164
|
+
then cast(COALESCE(
|
|
165
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
166
|
+
) as DateTime)
|
|
173
167
|
else null
|
|
174
168
|
end
|
|
175
169
|
) as valueChangedToLabelDateTimeWTZ : DateTime @(
|
|
@@ -179,9 +173,9 @@ view ChangeView as
|
|
|
179
173
|
(
|
|
180
174
|
case
|
|
181
175
|
when valueDataType = 'cds.Time'
|
|
182
|
-
then COALESCE(
|
|
176
|
+
then cast(COALESCE(
|
|
183
177
|
change.valueChangedToLabel, change.valueChangedTo
|
|
184
|
-
)
|
|
178
|
+
) as Time)
|
|
185
179
|
else null
|
|
186
180
|
end
|
|
187
181
|
) as valueChangedToLabelTime : Time @(title: '{i18n>Changes.valueChangedTo}',
|
|
@@ -189,9 +183,9 @@ view ChangeView as
|
|
|
189
183
|
(
|
|
190
184
|
case
|
|
191
185
|
when valueDataType = 'cds.Date'
|
|
192
|
-
then COALESCE(
|
|
186
|
+
then cast(COALESCE(
|
|
193
187
|
change.valueChangedToLabel, change.valueChangedTo
|
|
194
|
-
)
|
|
188
|
+
) as Date)
|
|
195
189
|
else null
|
|
196
190
|
end
|
|
197
191
|
) as valueChangedToLabelDate : Date @(title: '{i18n>Changes.valueChangedTo}',
|
|
@@ -199,9 +193,9 @@ view ChangeView as
|
|
|
199
193
|
(
|
|
200
194
|
case
|
|
201
195
|
when valueDataType = 'cds.Timestamp'
|
|
202
|
-
then COALESCE(
|
|
196
|
+
then cast(COALESCE(
|
|
203
197
|
change.valueChangedToLabel, change.valueChangedTo
|
|
204
|
-
)
|
|
198
|
+
) as Timestamp)
|
|
205
199
|
else null
|
|
206
200
|
end
|
|
207
201
|
) as valueChangedToLabelTimestamp : Timestamp @(title: '{i18n>Changes.valueChangedTo}',
|
|
@@ -209,9 +203,9 @@ view ChangeView as
|
|
|
209
203
|
(
|
|
210
204
|
case
|
|
211
205
|
when valueDataType = 'cds.Decimal'
|
|
212
|
-
then COALESCE(
|
|
206
|
+
then cast(COALESCE(
|
|
213
207
|
change.valueChangedToLabel, change.valueChangedTo
|
|
214
|
-
)
|
|
208
|
+
) as Decimal)
|
|
215
209
|
else null
|
|
216
210
|
end
|
|
217
211
|
) as valueChangedToLabelDecimal : Decimal @(title: '{i18n>Changes.valueChangedTo}',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const { fs, path } = cds.utils;
|
|
3
|
+
const { join } = path;
|
|
4
|
+
|
|
5
|
+
const MIGRATION_TABLE_PATH = join('db', 'src', 'sap.changelog.Changes.hdbmigrationtable');
|
|
6
|
+
const UNDEPLOY_JSON_PATH = join('db', 'undeploy.json');
|
|
7
|
+
|
|
8
|
+
const UNDEPLOY_ENTRIES = ['src/gen/**/sap.changelog.Changes.hdbtable', 'src/gen/**/sap.changelog.ChangeLog.hdbtable'];
|
|
9
|
+
|
|
10
|
+
const LOG = cds.log('change-tracking');
|
|
11
|
+
|
|
12
|
+
const { getMigrationTableSQL } = require('./hana/migrationTable.js');
|
|
13
|
+
|
|
14
|
+
module.exports = class extends cds.add.Plugin {
|
|
15
|
+
async run() {
|
|
16
|
+
if (fs.existsSync(MIGRATION_TABLE_PATH)) {
|
|
17
|
+
const existing = fs.readFileSync(MIGRATION_TABLE_PATH, 'utf8');
|
|
18
|
+
const versionMatch = [...existing.matchAll(/==\s*version=(\d+)/g)];
|
|
19
|
+
const latestVersion = versionMatch.length > 0 ? Math.max(...versionMatch.map((m) => parseInt(m[1]))) : 1;
|
|
20
|
+
|
|
21
|
+
if (latestVersion >= 2) {
|
|
22
|
+
LOG.warn(`Migration table already exists at ${MIGRATION_TABLE_PATH} (latest version: ${latestVersion}). ` + `Only the initial v1 -> v2 migration is supported by this command. ` + `Please add new migration steps manually.`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Rewrite file with v2 DDL and migration (replaces v1 content)
|
|
27
|
+
fs.writeFileSync(MIGRATION_TABLE_PATH, getMigrationTableSQL());
|
|
28
|
+
LOG.info(`Updated ${MIGRATION_TABLE_PATH} with v2 migration`);
|
|
29
|
+
} else {
|
|
30
|
+
// Write the migration table file
|
|
31
|
+
const dir = join('db', 'src');
|
|
32
|
+
if (!fs.existsSync(dir)) {
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(MIGRATION_TABLE_PATH, getMigrationTableSQL());
|
|
36
|
+
LOG.info(`Created ${MIGRATION_TABLE_PATH}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Update undeploy.json
|
|
40
|
+
let undeploy = [];
|
|
41
|
+
if (fs.existsSync(UNDEPLOY_JSON_PATH)) {
|
|
42
|
+
undeploy = JSON.parse(fs.readFileSync(UNDEPLOY_JSON_PATH, 'utf8'));
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(undeploy)) undeploy = [];
|
|
45
|
+
|
|
46
|
+
let changed = false;
|
|
47
|
+
for (const entry of UNDEPLOY_ENTRIES) {
|
|
48
|
+
if (!undeploy.includes(entry)) {
|
|
49
|
+
undeploy.push(entry);
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (changed) {
|
|
55
|
+
fs.writeFileSync(UNDEPLOY_JSON_PATH, JSON.stringify(undeploy, null, 4) + '\n');
|
|
56
|
+
LOG.info(`Updated ${UNDEPLOY_JSON_PATH} with old .hdbtable entries`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -37,7 +37,7 @@ function collectTrackedPropertiesWithDynamicLocalization(serviceName, m) {
|
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (!dynamicLocalizationProperties.some((prop) => prop.property === basePropertyName && prop.entity === base.baseRef)) {
|
|
40
|
+
if (!dynamicLocalizationProperties.some((prop) => prop.property === basePropertyName && prop.entity === base.baseRef) && m.definitions[target + '.texts']) {
|
|
41
41
|
dynamicLocalizationProperties.push({
|
|
42
42
|
property: basePropertyName,
|
|
43
43
|
entity: base.baseRef,
|
|
@@ -21,7 +21,7 @@ function entityKey4(entity) {
|
|
|
21
21
|
keys.push({ ref: [k] });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
if (keys.length <= 1) return keys;
|
|
24
|
+
if (keys.length <= 1) return keys.map((k) => ({ ...k, cast: { type: 'cds.String' } }));
|
|
25
25
|
|
|
26
26
|
const xpr = [];
|
|
27
27
|
for (let i = 0; i < keys.length; i++) {
|
|
@@ -46,9 +46,9 @@ function enhanceChangeViewWithTimeZones(changeView, m) {
|
|
|
46
46
|
? compositeKeyExprHANA
|
|
47
47
|
: isDeploy2Check('postgres') && m.meta.creator.match(/v6/)
|
|
48
48
|
? compositeKeyExprPG
|
|
49
|
-
: cds.env.requires?.db
|
|
49
|
+
: cds.env.requires?.db?.kind === 'sqlite' && !cds.build
|
|
50
50
|
? compositeKeyExprSqlite
|
|
51
|
-
: cds.env.requires?.db
|
|
51
|
+
: cds.env.requires?.db?.kind === 'postgres' && (!cds.build || cds.env.profiles.includes('pg'))
|
|
52
52
|
? compositeKeyExprPG
|
|
53
53
|
: compositeKeyExprHANA;
|
|
54
54
|
const timezoneProperties = collectTrackedPropertiesWithTimezone(m);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the .hdbmigrationtable content for migrating sap.changelog.Changes from v1 to v2.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function getMigrationTableSQL() {
|
|
6
|
+
return `== version=2
|
|
7
|
+
COLUMN TABLE sap_changelog_Changes (
|
|
8
|
+
ID NVARCHAR(36) NOT NULL,
|
|
9
|
+
parent_ID NVARCHAR(36),
|
|
10
|
+
attribute NVARCHAR(127),
|
|
11
|
+
valueChangedFrom NVARCHAR(5000),
|
|
12
|
+
valueChangedTo NVARCHAR(5000),
|
|
13
|
+
valueChangedFromLabel NVARCHAR(5000),
|
|
14
|
+
valueChangedToLabel NVARCHAR(5000),
|
|
15
|
+
entity NVARCHAR(150),
|
|
16
|
+
entityKey NVARCHAR(5000),
|
|
17
|
+
objectID NVARCHAR(5000),
|
|
18
|
+
modification NVARCHAR(6),
|
|
19
|
+
valueDataType NVARCHAR(5000),
|
|
20
|
+
createdAt TIMESTAMP,
|
|
21
|
+
createdBy NVARCHAR(255),
|
|
22
|
+
transactionID BIGINT,
|
|
23
|
+
PRIMARY KEY(ID)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
== migration=2
|
|
27
|
+
RENAME COLUMN sap_changelog_Changes.entityID TO objectID;
|
|
28
|
+
|
|
29
|
+
ALTER TABLE sap_changelog_Changes ADD (parent_ID NVARCHAR(36), valueChangedFromLabel NVARCHAR(5000), valueChangedToLabel NVARCHAR(5000), transactionID BIGINT);
|
|
30
|
+
|
|
31
|
+
-- Adjust entityKey structure
|
|
32
|
+
RENAME COLUMN sap_changelog_Changes.keys TO entityKey;
|
|
33
|
+
UPDATE SAP_CHANGELOG_CHANGES
|
|
34
|
+
SET entityKey =
|
|
35
|
+
CASE
|
|
36
|
+
WHEN LOCATE(entityKey, '=') > 0 THEN
|
|
37
|
+
TRIM(SUBSTRING(entityKey, LOCATE(entityKey, '=') + 1))
|
|
38
|
+
ELSE
|
|
39
|
+
NULL
|
|
40
|
+
END;
|
|
41
|
+
|
|
42
|
+
-- Copy changelog_ID into transactionID
|
|
43
|
+
UPDATE SAP_CHANGELOG_CHANGES SET transactionID = CAST(SECONDS_BETWEEN(createdAt, TO_TIMESTAMP('1970-01-01 00:00:00')) * -1000 AS BIGINT);
|
|
44
|
+
|
|
45
|
+
-- Column migration for attribute, entity and modification
|
|
46
|
+
ALTER TABLE sap_changelog_Changes ADD (attribute_tmp NVARCHAR(127), entity_tmp NVARCHAR(150), modification_tmp NVARCHAR(6));
|
|
47
|
+
|
|
48
|
+
-- Copy data into temp columns
|
|
49
|
+
UPDATE sap_changelog_Changes SET attribute_tmp = attribute;
|
|
50
|
+
UPDATE sap_changelog_Changes SET entity_tmp = entity;
|
|
51
|
+
UPDATE sap_changelog_Changes SET modification_tmp = modification;
|
|
52
|
+
|
|
53
|
+
ALTER TABLE sap_changelog_Changes DROP (attribute, entity, modification);
|
|
54
|
+
|
|
55
|
+
ALTER TABLE sap_changelog_Changes ADD (attribute NVARCHAR(127), entity NVARCHAR(150), modification NVARCHAR(6));
|
|
56
|
+
|
|
57
|
+
-- Restore data from temp columns
|
|
58
|
+
UPDATE sap_changelog_Changes SET attribute = attribute_tmp;
|
|
59
|
+
UPDATE sap_changelog_Changes SET entity = entity_tmp;
|
|
60
|
+
UPDATE sap_changelog_Changes SET modification = modification_tmp;
|
|
61
|
+
|
|
62
|
+
ALTER TABLE sap_changelog_Changes DROP (attribute_tmp, entity_tmp, modification_tmp);
|
|
63
|
+
|
|
64
|
+
-- Drop columns that are no longer needed
|
|
65
|
+
ALTER TABLE sap_changelog_Changes DROP (serviceEntity, parentEntityID, parentKey, serviceEntityPath, changeLog_ID);
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { getMigrationTableSQL };
|
package/lib/hana/register.js
CHANGED
|
@@ -3,6 +3,12 @@ const cds = require('@sap/cds');
|
|
|
3
3
|
const { prepareCSNForTriggers, generateTriggersForEntities, ensureUndeployJsonHasTriggerPattern } = require('../utils/trigger-utils.js');
|
|
4
4
|
const { getLabelTranslations } = require('../localization.js');
|
|
5
5
|
|
|
6
|
+
const MIGRATION_TABLE_PATH = cds.utils.path.join('db', 'src', 'sap.changelog.Changes.hdbmigrationtable');
|
|
7
|
+
|
|
8
|
+
function hasMigrationTable() {
|
|
9
|
+
return cds.utils.fs.existsSync(MIGRATION_TABLE_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
function registerHDICompilerHook() {
|
|
7
13
|
const _hdi_migration = cds.compiler.to.hdi.migration;
|
|
8
14
|
cds.compiler.to.hdi.migration = function (csn, options, beforeImage) {
|
|
@@ -38,20 +44,37 @@ function registerHDICompilerHook() {
|
|
|
38
44
|
);
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
const config = cds.env.requires?.['change-tracking'];
|
|
48
|
+
|
|
49
|
+
// Generate restore backlinks procedure if enabled via feature flag
|
|
50
|
+
if (config?.procedureForRestoringBacklinks) {
|
|
51
|
+
const { generateRestoreBacklinksProcedure } = require('./restoreProcedure.js');
|
|
52
|
+
const procedure = generateRestoreBacklinksProcedure(runtimeCSN, hierarchy, entities);
|
|
53
|
+
if (procedure) data.push(procedure);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Auto-detect migration table created by `cds add change-tracking`
|
|
57
|
+
if (hasMigrationTable()) {
|
|
58
|
+
csn.definitions['sap.changelog.Changes']['@cds.persistence.journal'] = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
const ret = _hdi_migration(csn, options, beforeImage);
|
|
42
62
|
ret.definitions = ret.definitions.concat(triggers).concat(data);
|
|
43
63
|
return ret;
|
|
44
64
|
};
|
|
45
65
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
// When a migration table file exists in db/src/, strip compiler-generated changesets for sap.changelog.Changes
|
|
67
|
+
// Prevent auto-generation of additional migration steps by the build
|
|
68
|
+
const _compile_to_hana = cds.compile.to.hana;
|
|
69
|
+
cds.compile.to.hana = function (csn, o, beforeCsn) {
|
|
70
|
+
if (hasMigrationTable() && beforeCsn) {
|
|
71
|
+
// Remove the Changes entity from the beforeImage so the compiler
|
|
72
|
+
const beforeClone = structuredClone(beforeCsn);
|
|
73
|
+
delete beforeClone.definitions?.['sap.changelog.Changes'];
|
|
74
|
+
return _compile_to_hana.call(this, csn, o, beforeClone);
|
|
75
|
+
}
|
|
76
|
+
return _compile_to_hana.call(this, csn, o, beforeCsn);
|
|
77
|
+
};
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
module.exports = { registerHDICompilerHook };
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
const utils = require('../utils/change-tracking.js');
|
|
2
|
+
const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
|
|
3
|
+
const { compositeKeyExpr } = require('./sql-expressions.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a HANA stored procedure that restores parent backlinks for composition changes.
|
|
7
|
+
*
|
|
8
|
+
* The procedure:
|
|
9
|
+
* 1. Finds all change entries for composition child entities that have no parent_ID set
|
|
10
|
+
* 2. Uses the child data table to resolve the parent entity key via FK lookup
|
|
11
|
+
* 3. Creates a parent composition entry (valueDataType='cds.Composition') if one doesn't exist
|
|
12
|
+
* 4. Updates child entries to set parent_ID pointing to the parent composition entry
|
|
13
|
+
* 5. Links composition entries to their grandparent composition entries (for deep hierarchies)
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
function generateRestoreBacklinksProcedure(runtimeCSN, hierarchy, entities) {
|
|
17
|
+
const compositions = _collectCompositionInfo(runtimeCSN, hierarchy, entities);
|
|
18
|
+
if (compositions.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
const blocks = compositions.map((comp) => _generateCompositionBlock(comp));
|
|
21
|
+
|
|
22
|
+
const procedureSQL = `PROCEDURE "SAP_CHANGELOG_RESTORE_BACKLINKS" ()
|
|
23
|
+
LANGUAGE SQLSCRIPT
|
|
24
|
+
SQL SECURITY INVOKER
|
|
25
|
+
AS
|
|
26
|
+
BEGIN
|
|
27
|
+
${blocks.join('\n')}
|
|
28
|
+
END`;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'SAP_CHANGELOG_RESTORE_BACKLINKS',
|
|
32
|
+
sql: procedureSQL,
|
|
33
|
+
suffix: '.hdbprocedure'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _collectCompositionInfo(runtimeCSN, hierarchy, entities) {
|
|
38
|
+
const result = [];
|
|
39
|
+
|
|
40
|
+
for (const [childEntityName, hierarchyInfo] of hierarchy) {
|
|
41
|
+
const { parent: parentEntityName, compositionField } = hierarchyInfo;
|
|
42
|
+
if (!parentEntityName || !compositionField) continue;
|
|
43
|
+
|
|
44
|
+
const childEntity = runtimeCSN.definitions[childEntityName];
|
|
45
|
+
const parentEntity = runtimeCSN.definitions[parentEntityName];
|
|
46
|
+
if (!childEntity || !parentEntity) continue;
|
|
47
|
+
|
|
48
|
+
// Check if this entity is actually tracked (in our entities list)
|
|
49
|
+
const isTracked = entities.some((e) => e.dbEntityName === childEntityName);
|
|
50
|
+
if (!isTracked) continue;
|
|
51
|
+
|
|
52
|
+
// Get the FK binding from child to parent
|
|
53
|
+
const parentMergedAnnotations = entities.find((e) => e.dbEntityName === parentEntityName)?.mergedAnnotations;
|
|
54
|
+
const compositionParentInfo = getCompositionParentInfo(childEntity, parentEntity, parentMergedAnnotations);
|
|
55
|
+
if (!compositionParentInfo) continue;
|
|
56
|
+
|
|
57
|
+
const { parentKeyBinding } = compositionParentInfo;
|
|
58
|
+
|
|
59
|
+
// Skip composition of one - they have reverse FK direction and different handling
|
|
60
|
+
if (parentKeyBinding.type === 'compositionOfOne') continue;
|
|
61
|
+
|
|
62
|
+
const childKeys = utils.extractKeys(childEntity.keys);
|
|
63
|
+
const parentKeys = utils.extractKeys(parentEntity.keys);
|
|
64
|
+
const rootObjectIDs = utils.getObjectIDs(parentEntity, runtimeCSN, parentMergedAnnotations?.entityAnnotation);
|
|
65
|
+
|
|
66
|
+
// Collect child entity's objectIDs for restoring objectID on orphaned child entries
|
|
67
|
+
const childMergedAnnotations = entities.find((e) => e.dbEntityName === childEntityName)?.mergedAnnotations;
|
|
68
|
+
const childObjectIDs = utils.getObjectIDs(childEntity, runtimeCSN, childMergedAnnotations?.entityAnnotation);
|
|
69
|
+
|
|
70
|
+
// Collect grandparent info for deep hierarchies (e.g., Level2 -> Level1 -> Root)
|
|
71
|
+
const grandParentEntityName = hierarchyInfo?.grandParent ?? null;
|
|
72
|
+
const grandParentEntity = grandParentEntityName ? runtimeCSN.definitions[grandParentEntityName] : null;
|
|
73
|
+
const grandParentMergedAnnotations = grandParentEntityName ? entities.find((e) => e.dbEntityName === grandParentEntityName)?.mergedAnnotations : null;
|
|
74
|
+
const grandParentCompositionField = hierarchyInfo?.grandParentCompositionField ?? null;
|
|
75
|
+
const grandParentCompositionInfo = getGrandParentCompositionInfo(parentEntity, grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField);
|
|
76
|
+
|
|
77
|
+
// If there's a grandparent, collect its keys, table name, and objectIDs for entry creation
|
|
78
|
+
let grandParentKeys, grandParentTableName, grandParentObjectIDs;
|
|
79
|
+
if (grandParentCompositionInfo && grandParentEntity) {
|
|
80
|
+
grandParentKeys = utils.extractKeys(grandParentEntity.keys);
|
|
81
|
+
grandParentTableName = utils.transformName(grandParentEntityName);
|
|
82
|
+
grandParentObjectIDs = utils.getObjectIDs(grandParentEntity, runtimeCSN, grandParentMergedAnnotations?.entityAnnotation);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
result.push({
|
|
86
|
+
childEntityName,
|
|
87
|
+
parentEntityName,
|
|
88
|
+
compositionField,
|
|
89
|
+
childTableName: utils.transformName(childEntityName),
|
|
90
|
+
parentTableName: utils.transformName(parentEntityName),
|
|
91
|
+
fkFields: parentKeyBinding,
|
|
92
|
+
childKeys,
|
|
93
|
+
parentKeys,
|
|
94
|
+
rootObjectIDs,
|
|
95
|
+
childObjectIDs,
|
|
96
|
+
grandParentCompositionInfo,
|
|
97
|
+
grandParentKeys,
|
|
98
|
+
grandParentTableName,
|
|
99
|
+
grandParentObjectIDs
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Builds the JOIN condition between Changes.entityKey and the child data table.
|
|
108
|
+
* Handles both v2 format (HIERARCHY_COMPOSITE_ID for multi-key) and v1 migrated format (single key only).
|
|
109
|
+
*/
|
|
110
|
+
function _buildChildKeyJoinCondition(childKeys, alias, changesAlias) {
|
|
111
|
+
const compositeExpr = compositeKeyExpr(childKeys.map((k) => `${alias}.${k}`));
|
|
112
|
+
|
|
113
|
+
// Single key: straightforward join
|
|
114
|
+
if (childKeys.length <= 1) {
|
|
115
|
+
return `${changesAlias}.ENTITYKEY = ${compositeExpr}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Composite keys: support both v2 format (HIERARCHY_COMPOSITE_ID) and v1 migrated format (single ID only)
|
|
119
|
+
// v1 migrated data may have stored only the last key segment as entityKey
|
|
120
|
+
const lastKey = childKeys[childKeys.length - 1];
|
|
121
|
+
return `(${changesAlias}.ENTITYKEY = ${compositeExpr} OR ${changesAlias}.ENTITYKEY = TO_NVARCHAR(${alias}.${lastKey}))`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generates the SQL block for a single composition relationship.
|
|
126
|
+
*/
|
|
127
|
+
function _generateCompositionBlock(comp) {
|
|
128
|
+
const {
|
|
129
|
+
childEntityName,
|
|
130
|
+
parentEntityName,
|
|
131
|
+
compositionField,
|
|
132
|
+
childTableName,
|
|
133
|
+
parentTableName,
|
|
134
|
+
fkFields,
|
|
135
|
+
childKeys,
|
|
136
|
+
parentKeys,
|
|
137
|
+
rootObjectIDs,
|
|
138
|
+
childObjectIDs,
|
|
139
|
+
grandParentCompositionInfo,
|
|
140
|
+
grandParentKeys,
|
|
141
|
+
grandParentTableName,
|
|
142
|
+
grandParentObjectIDs
|
|
143
|
+
} = comp;
|
|
144
|
+
|
|
145
|
+
// Build JOIN condition handling both v2 composite keys and v1 migrated simple keys
|
|
146
|
+
const childKeyJoinStep1 = _buildChildKeyJoinCondition(childKeys, 'child_data', 'c');
|
|
147
|
+
const childKeyJoinStep2 = _buildChildKeyJoinCondition(childKeys, 'child_data', 'c2');
|
|
148
|
+
|
|
149
|
+
// Expression to compute the parent's entity key from the child data table's FK columns
|
|
150
|
+
const parentKeyFromChild = compositeKeyExpr(fkFields.map((fk) => `child_data.${fk}`));
|
|
151
|
+
|
|
152
|
+
// ObjectID expression for the parent composition entry
|
|
153
|
+
// Only use objectIDs that are direct columns on the parent table (not association paths requiring JOINs)
|
|
154
|
+
const simpleObjectIDs = rootObjectIDs?.filter((oid) => !oid.name.includes('.')) ?? [];
|
|
155
|
+
let objectIDExpr;
|
|
156
|
+
if (simpleObjectIDs.length > 0) {
|
|
157
|
+
const parts = simpleObjectIDs.map((oid) => `COALESCE(TO_NVARCHAR((SELECT ${oid.name} FROM ${parentTableName} WHERE ${parentKeys.map((pk) => `${pk} = grp.PARENT_ENTITYKEY`).join(' AND ')})), '')`);
|
|
158
|
+
const concatExpr = parts.length > 1 ? parts.join(" || ', ' || ") : parts[0];
|
|
159
|
+
objectIDExpr = `COALESCE(NULLIF(${concatExpr}, ''), grp.PARENT_ENTITYKEY)`;
|
|
160
|
+
} else {
|
|
161
|
+
objectIDExpr = 'grp.PARENT_ENTITYKEY';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Child objectID expression for restoring objectID on orphaned child entries
|
|
165
|
+
// Uses @changelog fields if available, otherwise falls back to the child's entity key
|
|
166
|
+
const simpleChildObjectIDs = childObjectIDs?.filter((oid) => !oid.name.includes('.')) ?? [];
|
|
167
|
+
const childEntityKeyExpr = compositeKeyExpr(childKeys.map((k) => `child_data.${k}`));
|
|
168
|
+
let childObjectIDExpr;
|
|
169
|
+
if (simpleChildObjectIDs.length > 0) {
|
|
170
|
+
const childParts = simpleChildObjectIDs.map((oid) => `COALESCE(TO_NVARCHAR(child_data.${oid.name}), '<empty>')`);
|
|
171
|
+
const childConcatExpr = childParts.length > 1 ? childParts.join(" || ', ' || ") : childParts[0];
|
|
172
|
+
childObjectIDExpr = `COALESCE(NULLIF(${childConcatExpr}, ''), ${childEntityKeyExpr})`;
|
|
173
|
+
} else {
|
|
174
|
+
childObjectIDExpr = childEntityKeyExpr;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Modification: 'create' if the parent entity was created in the same tx, 'update' otherwise
|
|
178
|
+
const modificationExpr = `CASE WHEN EXISTS (
|
|
179
|
+
SELECT 1 FROM SAP_CHANGELOG_CHANGES
|
|
180
|
+
WHERE entity = '${parentEntityName}'
|
|
181
|
+
AND entityKey = grp.PARENT_ENTITYKEY
|
|
182
|
+
AND modification = 'create'
|
|
183
|
+
AND transactionID = grp.TRANSACTIONID
|
|
184
|
+
) THEN 'create' ELSE 'update' END`;
|
|
185
|
+
|
|
186
|
+
let block = `
|
|
187
|
+
-- ============================================================================
|
|
188
|
+
-- Restore backlinks: ${childEntityName} -> ${parentEntityName}.${compositionField}
|
|
189
|
+
-- ============================================================================
|
|
190
|
+
|
|
191
|
+
-- Step 1: Create parent composition entries where missing
|
|
192
|
+
INSERT INTO SAP_CHANGELOG_CHANGES
|
|
193
|
+
(ID, parent_ID, attribute, entity, entityKey, objectID, createdAt, createdBy, valueDataType, modification, transactionID)
|
|
194
|
+
SELECT
|
|
195
|
+
SYSUUID,
|
|
196
|
+
NULL,
|
|
197
|
+
'${compositionField}',
|
|
198
|
+
'${parentEntityName}',
|
|
199
|
+
grp.PARENT_ENTITYKEY,
|
|
200
|
+
${objectIDExpr},
|
|
201
|
+
grp.MIN_CREATEDAT,
|
|
202
|
+
grp.CREATEDBY,
|
|
203
|
+
'cds.Composition',
|
|
204
|
+
${modificationExpr},
|
|
205
|
+
grp.TRANSACTIONID
|
|
206
|
+
FROM (
|
|
207
|
+
SELECT
|
|
208
|
+
${parentKeyFromChild} AS PARENT_ENTITYKEY,
|
|
209
|
+
c.TRANSACTIONID,
|
|
210
|
+
MIN(c.CREATEDAT) AS MIN_CREATEDAT,
|
|
211
|
+
MIN(c.CREATEDBY) AS CREATEDBY
|
|
212
|
+
FROM SAP_CHANGELOG_CHANGES c
|
|
213
|
+
INNER JOIN ${childTableName} child_data
|
|
214
|
+
ON ${childKeyJoinStep1}
|
|
215
|
+
WHERE c.entity = '${childEntityName}'
|
|
216
|
+
AND c.parent_ID IS NULL
|
|
217
|
+
AND c.valueDataType != 'cds.Composition'
|
|
218
|
+
AND NOT EXISTS (
|
|
219
|
+
SELECT 1 FROM SAP_CHANGELOG_CHANGES p
|
|
220
|
+
WHERE p.entity = '${parentEntityName}'
|
|
221
|
+
AND p.attribute = '${compositionField}'
|
|
222
|
+
AND p.valueDataType = 'cds.Composition'
|
|
223
|
+
AND p.transactionID = c.transactionID
|
|
224
|
+
AND p.entityKey = ${parentKeyFromChild}
|
|
225
|
+
)
|
|
226
|
+
GROUP BY ${parentKeyFromChild}, c.TRANSACTIONID, c.CREATEDBY
|
|
227
|
+
) grp;
|
|
228
|
+
|
|
229
|
+
-- Step 2: Link orphaned child entries to their parent composition entry and restore objectID
|
|
230
|
+
MERGE INTO SAP_CHANGELOG_CHANGES AS c
|
|
231
|
+
USING (
|
|
232
|
+
SELECT c2.ID AS CHILD_ID, p.ID AS PARENT_ID, ${childObjectIDExpr} AS CHILD_OBJECTID
|
|
233
|
+
FROM SAP_CHANGELOG_CHANGES c2
|
|
234
|
+
INNER JOIN ${childTableName} child_data
|
|
235
|
+
ON ${childKeyJoinStep2}
|
|
236
|
+
INNER JOIN SAP_CHANGELOG_CHANGES p
|
|
237
|
+
ON p.entity = '${parentEntityName}'
|
|
238
|
+
AND p.attribute = '${compositionField}'
|
|
239
|
+
AND p.valueDataType = 'cds.Composition'
|
|
240
|
+
AND p.transactionID = c2.transactionID
|
|
241
|
+
AND p.entityKey = ${parentKeyFromChild}
|
|
242
|
+
WHERE c2.entity = '${childEntityName}'
|
|
243
|
+
AND c2.parent_ID IS NULL
|
|
244
|
+
AND c2.valueDataType != 'cds.Composition'
|
|
245
|
+
) AS matched
|
|
246
|
+
ON c.ID = matched.CHILD_ID
|
|
247
|
+
WHEN MATCHED THEN UPDATE SET c.parent_ID = matched.PARENT_ID, c.objectID = matched.CHILD_OBJECTID;`;
|
|
248
|
+
|
|
249
|
+
// Create grandparent composition entries and link to them (for deep hierarchies)
|
|
250
|
+
if (grandParentCompositionInfo) {
|
|
251
|
+
const { grandParentEntityName, grandParentCompositionFieldName, grandParentKeyBinding } = grandParentCompositionInfo;
|
|
252
|
+
|
|
253
|
+
// Build expression to resolve the grandparent entity key from the parent data table's FK columns
|
|
254
|
+
const grandParentKeyFromParent = compositeKeyExpr(grandParentKeyBinding.map((fk) => `parent_data.${fk}`));
|
|
255
|
+
|
|
256
|
+
// Build grandparent objectID expression
|
|
257
|
+
const simpleGPObjectIDs = grandParentObjectIDs?.filter((oid) => !oid.name.includes('.')) ?? [];
|
|
258
|
+
let gpObjectIDExpr;
|
|
259
|
+
if (simpleGPObjectIDs.length > 0) {
|
|
260
|
+
const gpParts = simpleGPObjectIDs.map((oid) => `COALESCE(TO_NVARCHAR((SELECT ${oid.name} FROM ${grandParentTableName} WHERE ${grandParentKeys.map((pk) => `${pk} = grp2.GP_ENTITYKEY`).join(' AND ')})), '')`);
|
|
261
|
+
const gpConcatExpr = gpParts.length > 1 ? gpParts.join(" || ', ' || ") : gpParts[0];
|
|
262
|
+
gpObjectIDExpr = `COALESCE(NULLIF(${gpConcatExpr}, ''), grp2.GP_ENTITYKEY)`;
|
|
263
|
+
} else {
|
|
264
|
+
gpObjectIDExpr = 'grp2.GP_ENTITYKEY';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Modification for grandparent: 'create' if grandparent was created in same tx, 'update' otherwise
|
|
268
|
+
const gpModificationExpr = `CASE WHEN EXISTS (
|
|
269
|
+
SELECT 1 FROM SAP_CHANGELOG_CHANGES
|
|
270
|
+
WHERE entity = '${grandParentEntityName}'
|
|
271
|
+
AND entityKey = grp2.GP_ENTITYKEY
|
|
272
|
+
AND modification = 'create'
|
|
273
|
+
AND transactionID = grp2.TRANSACTIONID
|
|
274
|
+
) THEN 'create' ELSE 'update' END`;
|
|
275
|
+
|
|
276
|
+
block += `
|
|
277
|
+
|
|
278
|
+
-- Step 3a: Create grandparent composition entries where missing
|
|
279
|
+
INSERT INTO SAP_CHANGELOG_CHANGES
|
|
280
|
+
(ID, parent_ID, attribute, entity, entityKey, objectID, createdAt, createdBy, valueDataType, modification, transactionID)
|
|
281
|
+
SELECT
|
|
282
|
+
SYSUUID,
|
|
283
|
+
NULL,
|
|
284
|
+
'${grandParentCompositionFieldName}',
|
|
285
|
+
'${grandParentEntityName}',
|
|
286
|
+
grp2.GP_ENTITYKEY,
|
|
287
|
+
${gpObjectIDExpr},
|
|
288
|
+
grp2.MIN_CREATEDAT,
|
|
289
|
+
grp2.CREATEDBY,
|
|
290
|
+
'cds.Composition',
|
|
291
|
+
${gpModificationExpr},
|
|
292
|
+
grp2.TRANSACTIONID
|
|
293
|
+
FROM (
|
|
294
|
+
SELECT
|
|
295
|
+
${grandParentKeyFromParent} AS GP_ENTITYKEY,
|
|
296
|
+
comp2.TRANSACTIONID,
|
|
297
|
+
MIN(comp2.CREATEDAT) AS MIN_CREATEDAT,
|
|
298
|
+
MIN(comp2.CREATEDBY) AS CREATEDBY
|
|
299
|
+
FROM SAP_CHANGELOG_CHANGES comp2
|
|
300
|
+
INNER JOIN ${parentTableName} parent_data
|
|
301
|
+
ON comp2.ENTITYKEY = ${compositeKeyExpr(parentKeys.map((pk) => `parent_data.${pk}`))}
|
|
302
|
+
WHERE comp2.entity = '${parentEntityName}'
|
|
303
|
+
AND comp2.attribute = '${compositionField}'
|
|
304
|
+
AND comp2.valueDataType = 'cds.Composition'
|
|
305
|
+
AND comp2.parent_ID IS NULL
|
|
306
|
+
AND NOT EXISTS (
|
|
307
|
+
SELECT 1 FROM SAP_CHANGELOG_CHANGES gp
|
|
308
|
+
WHERE gp.entity = '${grandParentEntityName}'
|
|
309
|
+
AND gp.attribute = '${grandParentCompositionFieldName}'
|
|
310
|
+
AND gp.valueDataType = 'cds.Composition'
|
|
311
|
+
AND gp.transactionID = comp2.transactionID
|
|
312
|
+
AND gp.entityKey = ${grandParentKeyFromParent}
|
|
313
|
+
)
|
|
314
|
+
GROUP BY ${grandParentKeyFromParent}, comp2.TRANSACTIONID, comp2.CREATEDBY
|
|
315
|
+
) grp2;
|
|
316
|
+
|
|
317
|
+
-- Step 3b: Link composition entries to their grandparent composition entries
|
|
318
|
+
MERGE INTO SAP_CHANGELOG_CHANGES AS comp
|
|
319
|
+
USING (
|
|
320
|
+
SELECT comp2.ID AS COMP_ID, gp.ID AS GRANDPARENT_ID
|
|
321
|
+
FROM SAP_CHANGELOG_CHANGES comp2
|
|
322
|
+
INNER JOIN ${parentTableName} parent_data
|
|
323
|
+
ON comp2.ENTITYKEY = ${compositeKeyExpr(parentKeys.map((pk) => `parent_data.${pk}`))}
|
|
324
|
+
INNER JOIN SAP_CHANGELOG_CHANGES gp
|
|
325
|
+
ON gp.entity = '${grandParentEntityName}'
|
|
326
|
+
AND gp.attribute = '${grandParentCompositionFieldName}'
|
|
327
|
+
AND gp.valueDataType = 'cds.Composition'
|
|
328
|
+
AND gp.transactionID = comp2.transactionID
|
|
329
|
+
AND gp.entityKey = ${grandParentKeyFromParent}
|
|
330
|
+
WHERE comp2.entity = '${parentEntityName}'
|
|
331
|
+
AND comp2.attribute = '${compositionField}'
|
|
332
|
+
AND comp2.valueDataType = 'cds.Composition'
|
|
333
|
+
AND comp2.parent_ID IS NULL
|
|
334
|
+
) AS matched
|
|
335
|
+
ON comp.ID = matched.COMP_ID
|
|
336
|
+
WHEN MATCHED THEN UPDATE SET comp.parent_ID = matched.GRANDPARENT_ID;`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return block;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = { generateRestoreBacklinksProcedure };
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
const utils = require('../utils/change-tracking.js');
|
|
2
2
|
const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
|
|
3
|
-
|
|
4
|
-
const HANAService = require('@cap-js/hana');
|
|
5
|
-
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
6
3
|
const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
|
|
7
4
|
|
|
8
|
-
const TriggerCQN2SQL = createTriggerCQN2SQL(HANAService.CQN2SQL);
|
|
9
5
|
let HANACQN2SQL;
|
|
10
6
|
|
|
11
7
|
function toSQL(query, model) {
|
|
8
|
+
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
12
9
|
if (!HANACQN2SQL) {
|
|
10
|
+
const { CQN2SQL } = require('@cap-js/hana');
|
|
11
|
+
const TriggerCQN2SQL = createTriggerCQN2SQL(CQN2SQL);
|
|
13
12
|
HANACQN2SQL = new TriggerCQN2SQL();
|
|
14
13
|
}
|
|
15
14
|
const sqlCQN = cqn4sql(query, model);
|
|
@@ -27,7 +26,7 @@ function getElementSkipCondition(entityName, elementName) {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
function compositeKeyExpr(parts) {
|
|
30
|
-
if (parts.length <= 1) return parts[0]
|
|
29
|
+
if (parts.length <= 1) return `TO_NVARCHAR(${parts[0]})`;
|
|
31
30
|
return `HIERARCHY_COMPOSITE_ID(${parts.join(', ')})`;
|
|
32
31
|
}
|
|
33
32
|
|
package/lib/postgres/register.js
CHANGED
|
@@ -26,18 +26,6 @@ function registerPostgresCompilerHook() {
|
|
|
26
26
|
|
|
27
27
|
return ddl;
|
|
28
28
|
});
|
|
29
|
-
|
|
30
|
-
// REVISIT: Remove once time casting is fixed in cds-dbs
|
|
31
|
-
cds.on('serving', async () => {
|
|
32
|
-
if (cds.env.requires?.db.kind !== 'postgres') return;
|
|
33
|
-
const db = await cds.connect.to('db');
|
|
34
|
-
db.before('*', () => {
|
|
35
|
-
db.class.CQN2SQL.OutputConverters.Date = (e) => `to_date(${e}::text, 'YYYY-MM-DD')`;
|
|
36
|
-
db.class.CQN2SQL.OutputConverters.Time = (e) => `to_timestamp(${e}::text, 'HH24:MI:SS')::TIME`;
|
|
37
|
-
db.class.CQN2SQL.OutputConverters.DateTime = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')::timestamp`;
|
|
38
|
-
db.class.CQN2SQL.OutputConverters.Timestamp = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')::timestamp`;
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
29
|
}
|
|
42
30
|
|
|
43
31
|
async function deployPostgresLabels() {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
2
|
const utils = require('../utils/change-tracking.js');
|
|
3
3
|
const config = cds.env.requires['change-tracking'];
|
|
4
|
-
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
5
4
|
const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
|
|
6
5
|
const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
|
|
7
6
|
|
|
8
7
|
const _cqn2sqlCache = new WeakMap();
|
|
9
8
|
|
|
10
9
|
function toSQL(query, model) {
|
|
10
|
+
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
11
11
|
let cqn2sql = _cqn2sqlCache.get(model);
|
|
12
12
|
if (!cqn2sql) {
|
|
13
13
|
const Service = require('@cap-js/postgres');
|
package/lib/skipHandlers.js
CHANGED
|
@@ -7,7 +7,7 @@ const { setSkipSessionVariables, resetSkipSessionVariables, resetAutoSkipForServ
|
|
|
7
7
|
* Register db handlers for setting/resetting session variables on INSERT/UPDATE/DELETE.
|
|
8
8
|
*/
|
|
9
9
|
function registerSessionVariableHandlers() {
|
|
10
|
-
cds.db
|
|
10
|
+
cds.db?.before(['INSERT', 'UPDATE', 'DELETE'], async (req) => {
|
|
11
11
|
const model = cds.context?.model ?? cds.model;
|
|
12
12
|
const collectedEntities = model.collectEntities || (model.collectEntities = collectEntities(model).collectedEntities);
|
|
13
13
|
if (!req.target || req.target.name.endsWith('.drafts')) return;
|
|
@@ -16,7 +16,7 @@ function registerSessionVariableHandlers() {
|
|
|
16
16
|
setSkipSessionVariables(req, srv, collectedEntities);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
cds.db
|
|
19
|
+
cds.db?.after(['INSERT', 'UPDATE', 'DELETE'], async (_, req) => {
|
|
20
20
|
if (!req.target || req.target.name.endsWith('.drafts')) return;
|
|
21
21
|
|
|
22
22
|
// Reset auto-skip variable if it was set
|
|
@@ -2,10 +2,10 @@ const utils = require('../utils/change-tracking.js');
|
|
|
2
2
|
const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
|
|
3
3
|
const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
|
|
4
4
|
|
|
5
|
-
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
6
5
|
const _cqn2sqlCache = new WeakMap();
|
|
7
6
|
|
|
8
7
|
function toSQL(query, model) {
|
|
8
|
+
const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
|
|
9
9
|
let cqn2sql = _cqn2sqlCache.get(model);
|
|
10
10
|
if (!cqn2sql) {
|
|
11
11
|
const SQLiteService = require('@cap-js/sqlite');
|
|
@@ -166,6 +166,12 @@ function extractTrackedColumns(entity, model = cds.context?.model ?? cds.model,
|
|
|
166
166
|
const entry = { name: name, type: col.type };
|
|
167
167
|
|
|
168
168
|
if (isAssociation) {
|
|
169
|
+
// Check if association target has a table
|
|
170
|
+
const targetEntity = model.definitions[col.target];
|
|
171
|
+
if (!targetEntity || targetEntity['@cds.persistence.skip'] === true) {
|
|
172
|
+
LOG.warn(`Skipped @changelog for ${name} on entity ${entity.name}: Association target "${col.target}" is annotated with @cds.persistence.skip!`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
169
175
|
entry.target = col.target;
|
|
170
176
|
// Use the resolved changelog annotation (which could be from override)
|
|
171
177
|
if (Array.isArray(changelogAnnotation) && changelogAnnotation.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/change-tracking",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.6",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"tag": "beta"
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"change-tracking": {
|
|
46
46
|
"model": "@cap-js/change-tracking",
|
|
47
47
|
"maxDisplayHierarchyDepth": 3,
|
|
48
|
-
"preserveDeletes": false
|
|
48
|
+
"preserveDeletes": false,
|
|
49
|
+
"procedureForRestoringBacklinks": true
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
},
|