@sap/xsodata 7.4.5 → 8.0.0

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
@@ -8,6 +8,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## [8.0.0] - 2022-08-04
12
+
13
+ * HANA Cloud database support added
14
+ * Public synonym support added
15
+ * Update module dependencies
16
+
17
+ ## [7.5.5] - 2022-04-21
18
+
19
+ * On calc view: Stable sort order with $skip and $top usage for subsequent requests via "order by" of key fields in SQL statement
20
+
21
+ ## [7.5.4] - 2022-04-21
22
+
23
+ Build infrastructure fix
24
+
25
+ ## [7.5.1] - [7.5.3] - 2022-04-20
26
+
27
+ Build infrastructure fixes
28
+
29
+ ## [7.5.0] - 2022-03-22
30
+
31
+ * Support of node version 16
32
+ * Update module dependencies
33
+ * Update of system query options and xsodata-settings help text
34
+ * Minor changes: Error reporting
35
+
11
36
  ## [7.4.5] - 2021-12-15
12
37
 
13
38
  * Suppress $metadata annotation <code>sap:aggregation-role="dimension"</code> on calculation view property if it is used as description property referenced by annotation <code>sap:text</code> by another property of the calulation view. The annotation is only supressed if the corresponding xsodata-file has <code>settings</code> containing this: <code>noDimensionAnnoOnTextProperty true;</code>
package/lib/db/connect.js CHANGED
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+
2
3
  /**
3
4
  * Add <db> attribute to <context>
4
5
  * Uses <context>.<handlerConfiguration.dbConfiguration>
@@ -60,13 +61,48 @@ function loadDbVersion(context, asyncDone) {
60
61
  context.logger.info('xsodata', 'db version: ' + version);
61
62
  context.gModel.setDbVersion(version);
62
63
  } else {
63
- context.gModel.setDbVersion(null); // don't try reload
64
+ context.gModel.setDbVersion(null); // don't try reload
65
+ }
66
+
67
+ return asyncDone(null, context);
68
+ });
69
+ }
70
+
71
+ function setHanaCloudContext(context, asyncDone) {
72
+
73
+ return context.db.client.exec('select version from "SYS"."M_DATABASE"', function (err, rows) {
74
+ if (err) {
75
+ return asyncDone(new SqlError(context, err), context);
76
+ }
77
+
78
+ if (rows && rows[0] && rows[0].VERSION) {
79
+ const version = rows[0].VERSION;
80
+ context.logger.info('xsodata', 'db version: ' + version);
81
+ context.db.isHanaCloudDb = isHanaCloudDb(context, rows[0].VERSION);
82
+ context.logger.info('xsodata', `isHanaCloud: ${context.db.isHanaCloudDb} (DB version based)`);
83
+ } else {
84
+ // 06/2022:
85
+ // Currently all SQL from Hana Cloud work also on Hana Service, i.e. take that version as default;
86
+ // could be changed in the future, i.e. using the context setting of 'context.db.isHanaCloudDb'
87
+ context.db.isHanaCloudDb = true;
88
+ context.logger.info('xsodata', `isHanaCloud: ${context.db.isHanaCloudDb} (default)`);
64
89
  }
65
90
 
66
91
  return asyncDone(null, context);
67
92
  });
68
93
  }
69
94
 
95
+ function isHanaCloudDb(context, version) {
96
+ const regex = /^([0-9])+\..*$/;
97
+ let result = version.match(regex);
98
+
99
+ if (result && result[1]) {
100
+ return (result[1] >= 4);
101
+ } else {
102
+ context.logger.error(`DB Version parsing failed: ${version}`);
103
+ return true; // default: Hana Cloud
104
+ }
105
+ }
70
106
 
71
107
  function getDbClientType(dbClient) {
72
108
  if (dbClient._settings) {
@@ -97,6 +133,8 @@ function prepareConnection(context, asyncDone) {
97
133
  // Set the isolation level
98
134
  executeList.push(utils.tryAndMeasure(execSQL, 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ', 'execSQL (isolation level)'));
99
135
  executeList.push(loadDbVersion);
136
+ // Set Hana Cloud indicator in context
137
+ executeList.push(setHanaCloudContext);
100
138
 
101
139
  async.waterfall(
102
140
  executeList,
@@ -163,9 +201,6 @@ function _connectInternal(context, asyncDone) {
163
201
 
164
202
 
165
203
  try {
166
- context.logger.silly('connect - db', 'use hdb.createClient');
167
-
168
-
169
204
  let db_module = null;
170
205
 
171
206
  if (context.handlerConfiguration.dbClient === 'hdb') {
@@ -173,11 +208,13 @@ function _connectInternal(context, asyncDone) {
173
208
  hdb_module = require('hdb');
174
209
  }
175
210
  db_module = hdb_module;
211
+ context.logger.silly('connect - db', 'use hdb.createClient');
176
212
  } else {
177
213
  if (!hana_client_module) {
178
214
  hana_client_module = require('@sap/hana-client');
179
215
  }
180
216
  db_module = hana_client_module;
217
+ context.logger.silly('connect - db', 'use hana-client');
181
218
  }
182
219
 
183
220
 
@@ -250,6 +287,7 @@ exports.connect = function (context, asyncDone) {
250
287
  }
251
288
  context.logger.debug('connect - db', 'connection is usable');
252
289
  context.db.connectionInitialized = true;
290
+ context.logger.debug('connect - context setting - Is Hana Cloud used?', `${context.db.isHanaCloudDb}`);
253
291
  return asyncDone(null, context);
254
292
 
255
293
  });
@@ -92,6 +92,13 @@ function DbSegment(kind, entityType, nr) {
92
92
  * @type {Array<String>}
93
93
  */
94
94
  this._SelectedPropertiesOrdered = null;
95
+
96
+ /**
97
+ * For CV: (real) key names with alias
98
+ * @type {Array<String,string>}
99
+ */
100
+ this._aliasedKeyPropertiesOnCalcView = [];
101
+
95
102
  /**
96
103
  * Store names existing of navigation properties
97
104
  * @type {Array<String>}
@@ -593,6 +600,50 @@ DbSegment.prototype.getKeyProperties0123ForSelectAs0123 = function (noTable) {
593
600
  return ret;
594
601
  };
595
602
 
603
+ DbSegment.prototype.getKeyProperties0123ForSelectOnCalcViewAs0123 = function (noTable) {
604
+ let ret = [];
605
+ let keyNames;
606
+ let keyName;
607
+ let property;
608
+ let selectProperty;
609
+ let propertyType;
610
+ let i;
611
+ let j = -1;
612
+
613
+ keyNames = this.entityType.keyNamesOrdered;
614
+
615
+ for (i = 0; i < keyNames.length; i++) {
616
+ keyName = keyNames[i];
617
+ property = this.entityType.propertiesMap[keyName];
618
+ selectProperty = null;
619
+
620
+ if (property.KIND !== EntityType.entityKind.inputParameters) {
621
+ j = j + 1; // for real key properties use numbering: 0,1,2, ... (skip CV input params)
622
+ propertyType = property.aggregate ? null : property.DATA_TYPE_NAME;
623
+ selectProperty = new sqlStatement.SelectProperty(noTable ? null : this._Alias, keyName, propertyType, j.toString());
624
+ ret.push(selectProperty);
625
+ }
626
+ }
627
+ return ret;
628
+ };
629
+
630
+ DbSegment.prototype.setAliasedKeyPropertiesOnCalcView = function(selectProperties) {
631
+ this._aliasedKeyPropertiesOnCalcView = selectProperties;
632
+ };
633
+
634
+ DbSegment.prototype.hasAliasedKeyPropertiesOnCalcView = function() {
635
+ return this._aliasedKeyPropertiesOnCalcView.length !== 0;
636
+ };
637
+
638
+ DbSegment.prototype.getAliasedKeyPropertyOnCalcView = function(propertyName) {
639
+ for (let i = 0; i < this._aliasedKeyPropertiesOnCalcView.length; i++) {
640
+ if (this._aliasedKeyPropertiesOnCalcView[i].property === propertyName) {
641
+ return this._aliasedKeyPropertiesOnCalcView[i];
642
+ }
643
+ }
644
+ return null;
645
+ };
646
+
596
647
  /**
597
648
  * Returns a selectProperty for usage in the select part of sql statements
598
649
  * The property are named "0","1","2",... and have as type the type of the corresponding key ( ordered by position
@@ -649,7 +700,30 @@ DbSegment.prototype.getKeyProperties0123ForOrderBy = function (noTable) {
649
700
  return ret;
650
701
  };
651
702
 
652
- DbSegment.prototype.getKeyPropertiesNotSelectedForSelect = function (noTable) {
703
+ DbSegment.prototype.getKeyProperties0123ForOrderByCalcView = function (noTable) {
704
+ let ret = [];
705
+ let keyName;
706
+ let property;
707
+ let j = -1;
708
+
709
+ let keyNames = this.entityType.keyNamesOrdered;
710
+
711
+ for (let i = 0; i < keyNames.length; i++) {
712
+
713
+ keyName = keyNames[i];
714
+ property = this.entityType.propertiesMap[keyName];
715
+
716
+ if (property.KIND !== EntityType.entityKind.inputParameters) {
717
+ j = j + 1;
718
+ let newSortOrder = new typedObjects.SortOrder(new typedObjects.Property(j.toString(), noTable ? null : this._Alias), 'ASC');
719
+ newSortOrder.setPropertyName(property.COLUMN_NAME);
720
+ ret.push(newSortOrder);
721
+ }
722
+ }
723
+ return ret;
724
+ };
725
+
726
+ DbSegment.prototype.getKeyPropertiesNotSelectedForSelect = function (noTable = undefined, inputParameters = null) {
653
727
  let ret = [];
654
728
  let keyNames;
655
729
  let i;
@@ -659,6 +733,18 @@ DbSegment.prototype.getKeyPropertiesNotSelectedForSelect = function (noTable) {
659
733
 
660
734
  keyNames = this.entityType.keyNamesOrdered;
661
735
 
736
+ // remove input params from key list
737
+ if (inputParameters !== null) {
738
+ for (let inputParameterName in inputParameters) {
739
+ if (inputParameters.hasOwnProperty(inputParameterName)) {
740
+ let indexKeyName = keyNames.indexOf(inputParameterName);
741
+ if (indexKeyName > -1) {
742
+ keyNames.splice(indexKeyName, 1);
743
+ }
744
+ }
745
+ }
746
+ }
747
+
662
748
  for (i = 0; i < keyNames.length; i++) {
663
749
  keyName = keyNames[i];
664
750
  key = this.entityType.propertiesMap[keyName];
@@ -43,6 +43,12 @@ function EntityType(entityType, settings) {
43
43
  this.path = this._entityType.path;
44
44
  this.keys = determineKeys(this._entityType);
45
45
 
46
+ // virtual table / virtual view data (includes synonyms + virtual tables)
47
+ this._isVirtual = false;
48
+ this._virtualObjectSchema = null;
49
+ this._virtualObjectName = null;
50
+ this._remoteSource = null;
51
+
46
52
  // in .xsodata as "entityname" --> null
47
53
  // in .xsodata as "entityname" concurrencytoken; --> {}
48
54
  // in .xsodata as "entityname" concurrencytoken ("KEY"); --> { key : true }
@@ -107,6 +113,12 @@ function EntityType(entityType, settings) {
107
113
  this._admindata = prepareAdmindata(this._settings.admindata, this._entityType.admindata);
108
114
  }
109
115
 
116
+ EntityType.prototype.setVirtualInfo = function(objectSchema, objectName, remoteSource = null) {
117
+ this._isVirtual = true;
118
+ this._virtualObjectSchema = objectSchema;
119
+ this._virtualObjectName = objectName;
120
+ this._remoteSource = remoteSource;
121
+ };
110
122
 
111
123
  function mergeAdmindataTuples(into, data) {
112
124
  for (const tuple of data) {
@@ -44,11 +44,15 @@ function loadKeysForTable(context, entityType, cb) {
44
44
  return cb(null, entityType);
45
45
  };
46
46
 
47
- var schema = entityType.schema || context.defaultSchema;
48
-
49
- var sql =
50
- "SELECT * from CONSTRAINTS " +
51
- "WHERE SCHEMA_NAME = '" + schema + "' AND TABLE_NAME = '" + entityType.tableName + "' AND IS_PRIMARY_KEY = 'TRUE' ORDER BY position";
47
+ let sql = "";
48
+ if (entityType._isVirtual) {
49
+ sql =
50
+ "select * from " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\".\"SYS\".\"CONSTRAINTS\"" : "\"SYS\".\"CONSTRAINTS\" " ) +
51
+ "WHERE SCHEMA_NAME = '" + entityType._virtualObjectSchema + "' AND TABLE_NAME = '" + entityType._virtualObjectName + "' AND IS_PRIMARY_KEY = 'TRUE' ORDER BY position";
52
+ } else {
53
+ let schema = entityType.schema || context.defaultSchema;
54
+ sql = "SELECT * from CONSTRAINTS WHERE SCHEMA_NAME = '" + schema + "' AND TABLE_NAME = '" + entityType.tableName + "' AND IS_PRIMARY_KEY = 'TRUE' ORDER BY position";
55
+ }
52
56
  dataCollector2.executeSqlDirectly(context, sql, processRows);
53
57
  }
54
58
 
@@ -104,13 +108,13 @@ function loadTableInfo(context, entityType, tableStoreType, cb) {
104
108
  }
105
109
  };
106
110
 
107
- context.logger.silly("model", "loadTableInfo 2");
108
- var schema = entityType.schema || context.defaultSchema;
109
- // entityType.table is either a real table or a SQL view
110
- // So we need to lookup both: sys.table_columns and sys.view_columns
111
- // sys.columns is not usable due to access restrictions
112
- var sql =
111
+ let schema = "";
112
+ let parameters = [];
113
113
 
114
+ // entityType.table is either a real table or a SQL view;
115
+ // so we need to lookup both: sys.table_columns and sys.view_columns (if set: check via "remote source");
116
+ // sys.columns is not usable due to access restrictions
117
+ let sql =
114
118
  "SELECT " +
115
119
  " TO_BIGINT(" + model.entityKind.table + ") kind, " +
116
120
  " table_name, " +
@@ -121,7 +125,7 @@ function loadTableInfo(context, entityType, tableStoreType, cb) {
121
125
  " length, " +
122
126
  " scale, " +
123
127
  " default_value " +
124
- "FROM TABLE_COLUMNS " +
128
+ "FROM " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"SYS\".\"TABLE_COLUMNS\"" : "TABLE_COLUMNS ") +
125
129
  "WHERE schema_name = ? AND " +
126
130
  " table_name = ? " +
127
131
  "UNION ALL " +
@@ -135,13 +139,17 @@ function loadTableInfo(context, entityType, tableStoreType, cb) {
135
139
  " length, " +
136
140
  " scale, " +
137
141
  " default_value " +
138
- "FROM VIEW_COLUMNS " +
142
+ "FROM " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"SYS\".\"VIEW_COLUMNS\"" : "VIEW_COLUMNS ") +
139
143
  "WHERE schema_name = ? AND " +
140
144
  " view_name = ? " +
141
145
  "ORDER BY position";
142
146
 
143
- var parameters = [schema, entityType.tableName, schema, entityType.tableName];
144
-
147
+ if (entityType._isVirtual) {
148
+ parameters = [entityType._virtualObjectSchema, entityType._virtualObjectName, entityType._virtualObjectSchema, entityType._virtualObjectName];
149
+ } else {
150
+ schema = entityType.schema || context.defaultSchema;
151
+ parameters = [schema, entityType.tableName, schema, entityType.tableName];
152
+ }
145
153
  context.logger.silly("model", "sql loadTableInfo: " + sql);
146
154
  context.logger.silly("model", "sql parameters: " + parameters);
147
155
  dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, processRows);
@@ -151,26 +159,26 @@ function loadCalcViewInfo(context, entityType, cb) {
151
159
  /*
152
160
  Remark : it is not possible(or at least i was not able to) to execute a JOIN in a MDX SELECT
153
161
  */
154
- var schema = entityType.schema || context.defaultSchema;
155
- var sql = 'select ' +
156
- 'SCHEMA_NAME,' +
157
- 'QUALIFIED_NAME,' +
158
- 'CATALOG_NAME,' +
159
- 'CUBE_NAME,' +
160
- 'COLUMN_NAME,' + // JOIN ON
161
- 'MEASURE_AGGREGATOR,' + // 1, 2, 3, 4 in sync with entityType.CV_AGGREGATE_FUNCTIONS
162
- 'MEASURE_AGGREGATABLE, ' +
163
- 'SEMANTIC_TYPE, ' +
164
- 'COLUMN_CAPTION, ' +
165
- 'DIMENSION_TYPE, ' +
166
- 'UNIT_COLUMN_NAME, ' +
167
- 'DESC_NAME, ' +
168
- '"ORDER" ' +
169
- "from _SYS_BI.BIMC_DIMENSION_VIEW_HDI " +
162
+ let schema = entityType.schema || context.defaultSchema;
163
+ let sql =
164
+ 'select ' +
165
+ 'SCHEMA_NAME,' +
166
+ 'QUALIFIED_NAME,' +
167
+ 'CATALOG_NAME,' +
168
+ 'CUBE_NAME,' +
169
+ 'COLUMN_NAME,' + // JOIN ON
170
+ 'MEASURE_AGGREGATOR,' + // 1, 2, 3, 4, 5 in sync with entityType.CV_AGGREGATE_FUNCTIONS
171
+ 'MEASURE_AGGREGATABLE, ' +
172
+ 'SEMANTIC_TYPE, ' +
173
+ 'COLUMN_CAPTION, ' +
174
+ 'DIMENSION_TYPE, ' +
175
+ 'UNIT_COLUMN_NAME, ' +
176
+ 'DESC_NAME, ' +
177
+ '"ORDER" ' +
178
+ "from " + (entityType._remoteSource ? "\"" + entityType._remoteSource + "\"" + ".\"_SYS_BI\".\"BIMC_DIMENSION_VIEW_HDI\"" : "_SYS_BI.BIMC_DIMENSION_VIEW_HDI ") +
170
179
  "where SCHEMA_NAME = ? AND QUALIFIED_NAME = ? " +
171
180
  'order by "ORDER"';
172
181
 
173
-
174
182
  function processRows(err, rows) {
175
183
  if (logMetadata) {
176
184
  context.logger.silly('metadataReader', 'rows\n' + JSON.stringify(rows, null, 4));
@@ -232,27 +240,37 @@ function loadCalcViewInfo(context, entityType, cb) {
232
240
  The current HANA version has a bug, and prepared statements are not working in mdx select.
233
241
  Hence i replaced prepared statement with direct sql statement.
234
242
  */
235
- var parameters = [schema, entityType.tableName];
243
+
244
+ let parameters = [];
245
+ if (entityType._isVirtual) {
246
+ // CV information must be read from the remote database;
247
+ // no such concept of "virtual" CV data in local database as it is the case for tables
248
+ parameters = [entityType._virtualObjectSchema, entityType._virtualObjectName];
249
+ } else {
250
+ parameters = [schema, entityType.tableName];
251
+ }
236
252
  dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, processRows);
237
253
  }
238
254
 
239
255
  function loadInputParameters(parameters, entityType, context, cb) {
240
- // At the time this sql was developed SCHEMA_NAME == NULL
241
- var sql = 'select distinct ' +
242
- 'VARIABLE_NAME, ' +
243
- 'COLUMN_TYPE_D, ' +
244
- 'COLUMN_SQL_TYPE, ' +
245
- 'MANDATORY, ' +
246
- 'DESCRIPTION, ' +
247
- 'DEFAULT_VALUE, ' +
248
- 'SEMANTIC_TYPE, ' +
249
- 'SELECTION_TYPE, ' +
250
- 'MULTILINE, ' +
251
- '"ORDER" ' +
252
- 'from _SYS_BI.BIMC_VARIABLE_VIEW_HDI where QUALIFIED_NAME = ? order by "ORDER"';
256
+
257
+ let sql =
258
+ 'select distinct ' +
259
+ 'VARIABLE_NAME, ' +
260
+ 'COLUMN_TYPE_D, ' +
261
+ 'COLUMN_SQL_TYPE, ' +
262
+ 'MANDATORY, ' +
263
+ 'DESCRIPTION, ' +
264
+ 'DEFAULT_VALUE, ' +
265
+ 'SEMANTIC_TYPE, ' +
266
+ 'SELECTION_TYPE, ' +
267
+ 'MULTILINE, ' +
268
+ '"ORDER" ' +
269
+ 'from ' + (entityType._remoteSource ? '"' + entityType._remoteSource + '"' + '"_SYS_BI"."BIMC_VARIABLE_VIEW_HDI"' : '_SYS_BI.BIMC_VARIABLE_VIEW_HDI ') +
270
+ 'where QUALIFIED_NAME = ? order by "ORDER"';
253
271
 
254
272
  context.logger.debug('metadataReader', 'loadInputParameters: ' + entityType.tableName);
255
- context.logger.silly("metadataReader", "sql loadInputParameters: " + sql);
273
+ context.logger.debug("metadataReader", "sql loadInputParameters: " + sql);
256
274
 
257
275
  dataCollector2.executeSqlAsPreparedStatement(context, sql, [entityType.tableName], (err, rows) => {
258
276
 
@@ -459,17 +477,127 @@ function loadEntityType(context, entityType, cbErr) {
459
477
  * @param {Function} callback - callback function, which is called when the operation is completed.
460
478
  */
461
479
  function determineStoreType(context, entityType, callback) {
462
- var schema = entityType.schema || context.defaultSchema,
480
+
481
+ let schema = entityType.schema || context.defaultSchema,
463
482
  sql = 'select IS_COLUMN_TABLE as "IS_COLUMN_TYPE" from TABLES where SCHEMA_NAME = ? AND TABLE_NAME = ? ' +
464
- 'union select IS_COLUMN_VIEW as "IS_COLUMN_TYPE" from VIEWS where SCHEMA_NAME = ? AND VIEW_NAME = ?',
483
+ 'union select IS_COLUMN_VIEW as "IS_COLUMN_TYPE" from VIEWS where SCHEMA_NAME = ? AND VIEW_NAME = ?',
465
484
  tableName = entityType.tableName,
466
485
  parameters = [schema, tableName, schema, tableName];
467
486
 
468
- context.logger.silly("model", "sql determineStoreType: " + sql);
469
- context.logger.silly("model", "sql determineStoreType parameters: " + parameters);
487
+ context.logger.debug("model", "sql determineStoreType: " + sql);
488
+ context.logger.debug("model", "sql determineStoreType parameters: " + parameters);
470
489
 
471
490
  dataCollector2.executeSqlAsPreparedStatement(context, sql, parameters, (error, rows) => {
472
- return processTableType(error, rows, context, tableName, callback);
491
+
492
+ if (error) {
493
+ return callback(new SqlError(context, error));
494
+ }
495
+
496
+ // if no error and no rows are returned: check for public synonym;
497
+ // if a virtual table is used (includes remote tables and remote views / CVs), we got a row back here!
498
+ if (rows.length === 0) {
499
+ context.logger.debug("model", `No table/view information found for ${tableName}. Try public synonym.`);
500
+ processSynonymTableType(context, entityType, tableName, (synonymErr, synonymFound, tableStoreType) => {
501
+ if (synonymErr) {
502
+ return callback(synonymErr);
503
+ }
504
+ if (synonymFound) {
505
+ return callback(null, tableStoreType);
506
+ }
507
+ return callback(new NotFound(`No table/view or synonym data found for table ${tableName}.`, context));
508
+ });
509
+ } else {
510
+ // check for virtual table; add VT data for entity type, if existing
511
+ let virtualTableSQL = "select * from SYS.VIRTUAL_TABLES where schema_name = ? and table_name = ?";
512
+ let virtualTableParameters = [schema, tableName];
513
+ context.logger.info("model", `Check for virtual table for ${tableName}`);
514
+ dataCollector2.executeSqlAsPreparedStatement(context, virtualTableSQL, virtualTableParameters, (sqlError, virtualTableRows) => {
515
+ if (sqlError) {
516
+ context.logger.error("model", `Failed read data for virtual table: ${tableName}` + sqlError);
517
+ return callback(new SqlError(context, sqlError));
518
+ }
519
+ if (virtualTableRows.length !== 0) {
520
+ entityType.setVirtualInfo(
521
+ virtualTableRows[0].REMOTE_OWNER_NAME,
522
+ virtualTableRows[0].REMOTE_OBJECT_NAME,
523
+ virtualTableRows[0].REMOTE_SOURCE_NAME );
524
+ context.logger.info("model", `(Remote) Virtual table information for table ${tableName}`);
525
+ context.logger.info("model", `Remote schema: ${virtualTableRows[0].REMOTE_OWNER_NAME}`);
526
+ context.logger.info("model", `Remote object: ${virtualTableRows[0].REMOTE_OBJECT_NAME}`);
527
+ context.logger.info("model", `Remote source: ${virtualTableRows[0].REMOTE_SOURCE_NAME}`);
528
+ } else {
529
+ context.logger.info("model", `No virtual table information exists for ${tableName}`);
530
+ }
531
+ // use data already found from TABLES / VIEWS for setting store type
532
+ return processTableType(error, rows, context, tableName, callback);
533
+ });
534
+ }
535
+ });
536
+ }
537
+
538
+ function processSynonymTableType(context, entityType, tableName, callback) {
539
+
540
+ const synonymSql = `select * from SYNONYMS where SCHEMA_NAME = 'PUBLIC' AND SYNONYM_NAME = '${tableName}'`;
541
+
542
+ dataCollector2.executeSqlDirectly(context, synonymSql, (synonymErr, synonymRows) => {
543
+ if (synonymErr) {
544
+ context.logger.error("model", "Failed to read synonym: " + synonymErr);
545
+ return callback(new SqlError(context, synonymErr), false, null);
546
+ }
547
+ if (synonymRows.length === 0) {
548
+ return callback(null, false, null); // not a synonym
549
+ }
550
+ if (synonymRows.length !== 1) {
551
+ context.logger.error("model", "No (unique) synonym data found for " + tableName + " table.");
552
+ return callback(new NotFound("No (unique) synonym data found for " + tableName + " table.", context), false, null);
553
+ }
554
+
555
+ // check for local or remote synonym
556
+ if (synonymRows[0].OBJECT_SCHEMA === '_SYS_LDB') { // remote synonym
557
+
558
+ const virtualTableName = synonymRows[0].OBJECT_NAME;
559
+ const virtualTableSql = `select * from VIRTUAL_TABLES where SCHEMA_NAME = '_SYS_LDB' and TABLE_NAME = '${virtualTableName}'`;
560
+
561
+ context.logger.info("model", `Checking virtual tables for remote synonym information for table ${virtualTableName}`);
562
+
563
+ // get remote source
564
+ dataCollector2.executeSqlDirectly(context, virtualTableSql, (virtualTableErr, virtualTableRows) => {
565
+ if (virtualTableErr) {
566
+ context.logger.error("model", `Failed to read virtual table ${virtualTableName} for synonym ${tableName}: ${virtualTableErr}`);
567
+ return callback(new SqlError(context, virtualTableErr), false, null);
568
+ }
569
+
570
+ context.logger.info("model", `(Remote) Virtual table information for synonym ${tableName} via table ${virtualTableName}`);
571
+ context.logger.info("model", `Remote schema: ${virtualTableRows[0].REMOTE_OWNER_NAME}`);
572
+ context.logger.info("model", `Remote object: ${virtualTableRows[0].REMOTE_OBJECT_NAME}`);
573
+ context.logger.info("model", `Remote source: ${virtualTableRows[0].REMOTE_SOURCE_NAME}`);
574
+
575
+ entityType.setVirtualInfo(
576
+ virtualTableRows[0].REMOTE_OWNER_NAME, // remote schema name
577
+ virtualTableRows[0].REMOTE_OBJECT_NAME, // remote object name
578
+ virtualTableRows[0].REMOTE_SOURCE_NAME); // remote source
579
+
580
+ // in SYNONYMS, each remote object has always the setting IS_COLUMN_OBJECT === FALSE (keep it as is)
581
+ let isColumnStoreType = synonymRows[0].IS_COLUMN_OBJECT;
582
+ let tableStoreType = isColumnStoreType === "TRUE" ? "column" : "row";
583
+ return callback(null, true, tableStoreType);
584
+ });
585
+
586
+ } else { // local synonym
587
+
588
+ context.logger.info("model", `(Local) Synonym information for ${synonymRows[0].SYNONYM_NAME}`);
589
+ context.logger.info("model", `Local schema: ${synonymRows[0].OBJECT_SCHEMA}`);
590
+ context.logger.info("model", `Local object: ${synonymRows[0].OBJECT_NAME}`);
591
+
592
+ entityType.setVirtualInfo(
593
+ synonymRows[0].OBJECT_SCHEMA, // local schema name
594
+ synonymRows[0].OBJECT_NAME); // local object name
595
+
596
+ // in SYNONYMS, a local object has the "correct" store type setting
597
+ let isColumnObjectType = synonymRows[0].IS_COLUMN_OBJECT;
598
+ let tableStoreType = isColumnObjectType === "TRUE" ? "column" : "row";
599
+ return callback(null, true, tableStoreType);
600
+ }
473
601
  });
474
602
  }
475
603
 
@@ -487,7 +615,7 @@ function processTableType(error, dbRows, context, tableName, callback) {
487
615
  tableStoreType;
488
616
 
489
617
  if (error) {
490
- return callback(new SqlError(context, error));
618
+ return (error instanceof SqlError) ? callback(error) : callback(new SqlError(context, error));
491
619
  }
492
620
 
493
621
  if (dbRows.length === 0) {
@@ -497,6 +625,8 @@ function processTableType(error, dbRows, context, tableName, callback) {
497
625
  context.logger.silly("processTableType", "rows:\n" + JSON.stringify(dbRows, null, 4));
498
626
  }
499
627
 
628
+ // if found table is a virtual (remote) table, the IS_COLUMN_TYPE is always set to FALSE,
629
+ // but this setting does not affect SQL processing (joins on temp. tables)
500
630
  isColumnStoreType = dbRows[0].IS_COLUMN_TYPE;
501
631
 
502
632
  // HANA returns "TRUE"/"FALSE" as a string and not as a boolean value
@@ -592,6 +722,9 @@ exports.loadModelMetadata = function (context, asyncDone) {
592
722
  }
593
723
  },
594
724
  function (err) {
725
+ if (err) {
726
+ context.logger.error('model', `Error on loadEntityTypes for ${context.uriTree.xsoFile}:` + err);
727
+ }
595
728
  context.logger.silly('model', 'loadEntityTypes finished for ' + context.uriTree.xsoFile);
596
729
  return asyncDone(err, context);
597
730
  }
@@ -182,7 +182,7 @@ function checkSchema(context, asyncDone) {
182
182
  entityTypes = parsedXsodata.service.entityTypes;
183
183
 
184
184
 
185
- // Run through entityTypes and check if navigates peroperty id provided
185
+ // Run through entityTypes and check if navigates property id provided
186
186
  for (key in entityTypes) {
187
187
  if (entityTypes.hasOwnProperty(key) === true) {
188
188
 
@@ -148,15 +148,15 @@ function processChangeSet(batchContext, part, asyncDone) {
148
148
 
149
149
  utils.try(collectContentIds),
150
150
 
151
- utils.try(processChangeSetPartsCreateTable),
151
+ utils.try(processChangeSetPartsCreateTable), // for *ALL* operations of current changeset: create all temp. tables once at beginning
152
152
 
153
- utils.try(processChangeSetParts),
153
+ utils.try(processChangeSetParts), // execute all operations
154
154
 
155
- utils.try(processChangeSetPartsPreCommit),
155
+ utils.try(processChangeSetPartsPreCommit), // mainly custom-exit processing (pre-commit exit)
156
156
 
157
- utils.try(processChangeSetPartsCommit),
157
+ utils.try(processChangeSetPartsCommit), // all changes of operations are committed
158
158
 
159
- utils.try(processChangeSetPartsPostCommit)
159
+ utils.try(processChangeSetPartsPostCommit) // mainly custom-exit processing (post-commit exit)
160
160
 
161
161
  ], function (err, batchContext) {
162
162
  if (!err) {
@@ -220,7 +220,7 @@ function processAppWithState(app, state, batchContext, asyncDone) {
220
220
 
221
221
  function processPart(batchContext, part, asyncDone) {
222
222
  batchContext.logger.silly('batchProcessor', 'processPart');
223
- if (part.type === 'app') {
223
+ if (part.type === 'app') { // processing a batch single operation (not in changeset)
224
224
  async.waterfall(
225
225
  [
226
226
  utils.injectContext(batchContext),
@@ -288,7 +288,7 @@ exports.process = function (context, asyncDone) {
288
288
  var batchContext = {
289
289
  parentContext: context,
290
290
  logger: context.logger,
291
- callRegisteredStep : context.callRegisteredStep, // pass to inner context
291
+ callRegisteredStep : context.callRegisteredStep, // pass to inner context
292
292
  inChangeSet: false,
293
293
  batchData: null,
294
294
  batchParsed: null,
@@ -21,15 +21,15 @@ exports.processRequest = function (context, asyncDone) {
21
21
  } else if (oData.kind === odataUri.URI_KIND_MetaData) {
22
22
  Measurement.measureWithCB(metadataProcessor.process, context, asyncDone, 'metadataProcessor.process');
23
23
  } else if (oData.kind === odataUri.URI_KIND_Batch) {
24
- Measurement.measureWithCB(batchProcessor.process, context, asyncDone, 'batchProcessor.process');
24
+ Measurement.measureWithCB(batchProcessor.process, context, asyncDone, 'batchProcessor.process'); // $batch-request itself
25
25
  } else if (oData.kind === odataUri.URI_KIND_Resource) {
26
26
 
27
27
  if (!context.batchContext) {
28
- //normal no batch request processing
28
+ // regular request processing
29
29
  Measurement.measureWithCB(resourceProcessor.process, context, asyncDone, 'resourceProcessor.process');
30
30
  } else {
31
- var batchContext = context.batchContext;
32
- if (batchContext.status === batchRunState.createTables) {
31
+ var batchContext = context.batchContext; // $batch-request: operation processing (based on status)
32
+ if (batchContext.status === batchRunState.createTables) { // on batch changes: all temp. tables of all operations are generated at beginning batch
33
33
  Measurement.measureWithCB(resourceProcessor.processInBatchCreateTables, context, asyncDone, 'processInBatchCreateTables');
34
34
  }else if (batchContext.status === batchRunState.execution) {
35
35
  Measurement.measureWithCB(resourceProcessor.processInBatch, context, asyncDone, 'processInBatch');