@cap-js/db-service 2.8.2 → 2.10.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 +35 -0
- package/lib/InsertResults.js +6 -3
- package/lib/SQLService.js +88 -50
- package/lib/cql-functions.js +1 -1
- package/lib/cqn2pql.js +116 -0
- package/lib/cqn2sql.js +131 -48
- package/lib/cqn4sql.js +588 -180
- package/lib/infer/index.js +77 -16
- package/lib/infer/join-tree.js +8 -6
- package/lib/utils.js +29 -0
- package/package.json +2 -2
package/lib/cqn2sql.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
const cds = require('@sap/cds')
|
|
2
2
|
const cds_infer = require('./infer')
|
|
3
3
|
const cqn4sql = require('./cqn4sql')
|
|
4
|
+
const { resolveTable } = require('./utils')
|
|
5
|
+
|
|
4
6
|
const _simple_queries = cds.env.features.sql_simple_queries
|
|
5
7
|
const _strict_booleans = _simple_queries < 2
|
|
8
|
+
// REVISIT: make string the default in next major
|
|
9
|
+
const _count_as_string = cds.env.features.count_as_string
|
|
10
|
+
const _count = _count_as_string ? { func: 'count', cast: { type: 'cds.String' } } : { func: 'count' }
|
|
6
11
|
|
|
7
12
|
const { Readable } = require('stream')
|
|
8
13
|
|
|
9
|
-
const DEBUG = cds.
|
|
10
|
-
const LOG_SQL = cds.log('sql')
|
|
11
|
-
const LOG_SQLITE = cds.log('sqlite')
|
|
14
|
+
const DEBUG = cds.log('sql|sqlite')
|
|
12
15
|
|
|
13
16
|
class CQN2SQLRenderer {
|
|
14
17
|
/**
|
|
@@ -26,7 +29,8 @@ class CQN2SQLRenderer {
|
|
|
26
29
|
if (cds.env.sql.names === 'quoted') {
|
|
27
30
|
this.class.prototype.name = (name, query) => {
|
|
28
31
|
const e = name.id || name
|
|
29
|
-
|
|
32
|
+
const entity = query?._target || this.model?.definitions[e]
|
|
33
|
+
return (!entity?.['@cds.persistence.skip'] && entity?.['@cds.persistence.name']) || e
|
|
30
34
|
}
|
|
31
35
|
this.class.prototype.quote = (s) => `"${String(s).replace(/"/g, '""')}"`
|
|
32
36
|
}
|
|
@@ -76,6 +80,7 @@ class CQN2SQLRenderer {
|
|
|
76
80
|
*/
|
|
77
81
|
render(q, vars) {
|
|
78
82
|
const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ...
|
|
83
|
+
if (q._with) this._with = q._with
|
|
79
84
|
/**
|
|
80
85
|
* @type {string} the rendered SQL string
|
|
81
86
|
*/
|
|
@@ -90,13 +95,12 @@ class CQN2SQLRenderer {
|
|
|
90
95
|
if (vars && Object.keys(vars).length && !this.values?.length) this.values = vars
|
|
91
96
|
const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
if (DEBUG && (LOG_SQL._debug || LOG_SQLITE._debug)) {
|
|
98
|
+
if (DEBUG._debug) {
|
|
95
99
|
let values = sanitize_values && (this.entries || this.values?.length > 0) ? ['***'] : this.entries || this.values || []
|
|
96
100
|
if (values && !Array.isArray(values)) {
|
|
97
101
|
values = [values]
|
|
98
102
|
}
|
|
99
|
-
DEBUG(this.sql, values)
|
|
103
|
+
DEBUG.debug(this.sql, values)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
return this
|
|
@@ -257,13 +261,15 @@ class CQN2SQLRenderer {
|
|
|
257
261
|
|
|
258
262
|
// REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
|
|
259
263
|
if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where
|
|
260
|
-
|
|
264
|
+
|
|
261
265
|
let sql = `SELECT`
|
|
262
266
|
if (distinct) sql += ` DISTINCT`
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
if (recurse) sql += this.SELECT_recurse(q)
|
|
268
|
+
else {
|
|
269
|
+
sql += ` ${this.SELECT_columns(q)}`
|
|
270
|
+
if (!_empty(from)) sql += ` FROM ${this.from(from, q)}`
|
|
271
|
+
else sql += this.from_dummy()
|
|
272
|
+
}
|
|
267
273
|
if (!recurse && !_empty(where)) sql += ` WHERE ${this.where(where)}`
|
|
268
274
|
if (!recurse && !_empty(groupBy)) sql += ` GROUP BY ${this.groupBy(groupBy)}`
|
|
269
275
|
if (!recurse && !_empty(having)) sql += ` HAVING ${this.having(having)}`
|
|
@@ -296,7 +302,7 @@ class CQN2SQLRenderer {
|
|
|
296
302
|
|
|
297
303
|
// `where` needs to be wrapped to also support `where == ['exists', { SELECT }]` which is not allowed in `START WHERE`
|
|
298
304
|
const clone = q.clone()
|
|
299
|
-
clone.columns
|
|
305
|
+
clone.SELECT.columns = keys
|
|
300
306
|
clone.SELECT.recurse = undefined
|
|
301
307
|
clone.SELECT.limit = undefined
|
|
302
308
|
clone.SELECT.expand = undefined // omits JSON
|
|
@@ -347,10 +353,16 @@ class CQN2SQLRenderer {
|
|
|
347
353
|
for (const name in target.elements) {
|
|
348
354
|
const ref = { ref: [name] }
|
|
349
355
|
const element = target.elements[name]
|
|
350
|
-
if (element.virtual || element.
|
|
351
|
-
if (
|
|
356
|
+
if (element.virtual || element.isAssociation) continue
|
|
357
|
+
if (name in availableComputedColumns) continue
|
|
352
358
|
if (name.toUpperCase() in reservedColumnNames) ref.as = `$$${name}$$`
|
|
353
|
-
|
|
359
|
+
// This only supports calculated elements within the scope of the own entity
|
|
360
|
+
if ('value' in element) {
|
|
361
|
+
const requested = columnsFiltered.find(c => this.column_name(c) === element.name)
|
|
362
|
+
if (requested) columnsIn.push(requested)
|
|
363
|
+
else continue
|
|
364
|
+
}
|
|
365
|
+
else columnsIn.push(ref)
|
|
354
366
|
const foreignkey4 = element._foreignKey4
|
|
355
367
|
if (
|
|
356
368
|
from.args ||
|
|
@@ -380,7 +392,7 @@ class CQN2SQLRenderer {
|
|
|
380
392
|
)
|
|
381
393
|
|
|
382
394
|
if (orderBy) {
|
|
383
|
-
orderBy = orderBy.map(r => {
|
|
395
|
+
orderBy = orderBy.filter(o => o.ref).map(r => {
|
|
384
396
|
let col = r.ref.at(-1)
|
|
385
397
|
if (col.toUpperCase() in reservedColumnNames) col = `$$${col}$$`
|
|
386
398
|
if (!columnsIn.find(c => this.column_name(c) === col)) {
|
|
@@ -497,13 +509,19 @@ class CQN2SQLRenderer {
|
|
|
497
509
|
}
|
|
498
510
|
}
|
|
499
511
|
|
|
512
|
+
const columnsQuery = cds.ql(q).clone()
|
|
513
|
+
columnsQuery.SELECT.columns = columns.map(x => {
|
|
514
|
+
if (x.element && 'value' in x.element) return { element: x.element, ref: [this.column_name(x)] }
|
|
515
|
+
return x
|
|
516
|
+
})
|
|
517
|
+
const recurseColumns = this.SELECT_columns(columnsQuery)
|
|
500
518
|
// Only apply result join if the columns contain a references which doesn't start with the source alias
|
|
501
519
|
if (from.args && columns.find(c => c.ref?.[0] === alias)) {
|
|
502
520
|
graph.as = alias
|
|
503
|
-
return this.from(setStableFrom(from, graph))
|
|
521
|
+
return ` ${recurseColumns} FROM ${this.from(setStableFrom(from, graph))}`
|
|
504
522
|
}
|
|
505
523
|
|
|
506
|
-
return `(${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
|
|
524
|
+
return ` ${recurseColumns} FROM (${this.SELECT(graph)})${alias ? ` AS ${this.quote(alias)}` : ''} `
|
|
507
525
|
|
|
508
526
|
function collectDistanceTo(where, innot = false) {
|
|
509
527
|
for (let i = 0; i < where.length; i++) {
|
|
@@ -640,7 +658,7 @@ class CQN2SQLRenderer {
|
|
|
640
658
|
|
|
641
659
|
SELECT_count(q) {
|
|
642
660
|
const countQuery = cds.ql.clone(q, {
|
|
643
|
-
columns: [
|
|
661
|
+
columns: [_count],
|
|
644
662
|
one: 0, limit: 0, orderBy: 0, expand: 0, count: 0
|
|
645
663
|
})
|
|
646
664
|
countQuery.as = q.as + '@odata.count'
|
|
@@ -730,6 +748,38 @@ class CQN2SQLRenderer {
|
|
|
730
748
|
return this.xpr({ xpr })
|
|
731
749
|
}
|
|
732
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Renders a transformed where clause that maps the query target view to the source table
|
|
753
|
+
* @param {import('./infer/cqn').source} alias
|
|
754
|
+
* @param {import('./infer/cqn').predicate} where
|
|
755
|
+
* @param {import('./infer/cqn').query} q
|
|
756
|
+
* @returns SQL
|
|
757
|
+
*/
|
|
758
|
+
where_resolved(alias, where, q) {
|
|
759
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
760
|
+
if (transitions.target === transitions.queryTarget) return this.where(where)
|
|
761
|
+
|
|
762
|
+
// view and table column refs to be matched
|
|
763
|
+
const viewCols = []
|
|
764
|
+
const tableCols = []
|
|
765
|
+
|
|
766
|
+
// Only match key columns when possible
|
|
767
|
+
const elements = q._target.keys || q._target.elements
|
|
768
|
+
for (const c in elements) {
|
|
769
|
+
if (
|
|
770
|
+
c in elements
|
|
771
|
+
&& transitions.mapping.has(c)
|
|
772
|
+
&& this.physical_column(elements, c)
|
|
773
|
+
) {
|
|
774
|
+
viewCols.push({ ref: [c] })
|
|
775
|
+
tableCols.push(transitions.mapping.get(c))
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return tableCols.length > 0
|
|
779
|
+
? this.where([{ list: tableCols }, 'in', SELECT.from(q._target).alias(alias).columns(viewCols).where(where)])
|
|
780
|
+
: this.where(where)
|
|
781
|
+
}
|
|
782
|
+
|
|
733
783
|
/**
|
|
734
784
|
* Renders a HAVING clause into generic SQL
|
|
735
785
|
* @param {import('./infer/cqn').predicate} xpr
|
|
@@ -835,15 +885,20 @@ class CQN2SQLRenderer {
|
|
|
835
885
|
if (!elements && !INSERT.entries?.length) {
|
|
836
886
|
return // REVISIT: mtx sends an insert statement without entries and no reference entity
|
|
837
887
|
}
|
|
888
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
838
889
|
const columns = elements
|
|
839
|
-
? ObjectKeys(elements).filter(c =>
|
|
890
|
+
? ObjectKeys(elements).filter(c => this.physical_column(elements, c)
|
|
891
|
+
&& (c = transitions.mapping.get(c)?.ref?.[0] || c)
|
|
892
|
+
&& c in transitions.target.elements
|
|
893
|
+
&& this.physical_column(transitions.target.elements, c)
|
|
894
|
+
)
|
|
840
895
|
: ObjectKeys(INSERT.entries[0])
|
|
841
896
|
|
|
842
897
|
/** @type {string[]} */
|
|
843
898
|
this.columns = columns
|
|
844
899
|
|
|
845
900
|
const alias = INSERT.into.as
|
|
846
|
-
const entity = this.
|
|
901
|
+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
|
|
847
902
|
if (!elements) {
|
|
848
903
|
this.entries = INSERT.entries.map(e => columns.map(c => e[c]))
|
|
849
904
|
const param = this.param.bind(this, { ref: ['?'] })
|
|
@@ -865,8 +920,8 @@ class CQN2SQLRenderer {
|
|
|
865
920
|
}
|
|
866
921
|
|
|
867
922
|
const extractions = this._managed = this.managed(columns.map(c => ({ name: c })), elements)
|
|
868
|
-
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(c))
|
|
869
|
-
}) SELECT ${extractions.map(c => c.insert)} FROM json_each(?)`)
|
|
923
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
|
|
924
|
+
}) SELECT ${extractions.slice(0, columns.length).map(c => c.insert)} FROM json_each(?)`)
|
|
870
925
|
}
|
|
871
926
|
|
|
872
927
|
async *INSERT_entries_stream(entries, binaryEncoding = 'base64') {
|
|
@@ -972,7 +1027,7 @@ class CQN2SQLRenderer {
|
|
|
972
1027
|
*/
|
|
973
1028
|
INSERT_rows(q) {
|
|
974
1029
|
const { INSERT } = q
|
|
975
|
-
const entity = this.
|
|
1030
|
+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
|
|
976
1031
|
const alias = INSERT.into.as
|
|
977
1032
|
const elements = q.elements || q._target?.elements
|
|
978
1033
|
const columns = this.columns = INSERT.columns || cds.error`Cannot insert rows without columns or elements`
|
|
@@ -997,7 +1052,8 @@ class CQN2SQLRenderer {
|
|
|
997
1052
|
.slice(0, columns.length)
|
|
998
1053
|
.map(c => c.converter(c.extract))
|
|
999
1054
|
|
|
1000
|
-
|
|
1055
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
1056
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))
|
|
1001
1057
|
}) SELECT ${extraction} FROM json_each(?)`)
|
|
1002
1058
|
}
|
|
1003
1059
|
|
|
@@ -1018,20 +1074,24 @@ class CQN2SQLRenderer {
|
|
|
1018
1074
|
*/
|
|
1019
1075
|
INSERT_select(q) {
|
|
1020
1076
|
const { INSERT } = q
|
|
1021
|
-
const entity = this.
|
|
1077
|
+
const entity = q._target ? this.table_name(q) : INSERT.into.ref[0]
|
|
1022
1078
|
const alias = INSERT.into.as
|
|
1079
|
+
const src = this.cqn4sql(INSERT.from)
|
|
1023
1080
|
const elements = q.elements || q._target?.elements || {}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1081
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
1082
|
+
let columns = (this.columns = (INSERT.columns || src.SELECT.columns?.map(c => this.column_name(c)) || ObjectKeys(src.elements) || ObjectKeys(elements))
|
|
1083
|
+
.filter(c => this.physical_column(elements, c)
|
|
1084
|
+
&& (c = transitions.mapping.get(c)?.ref?.[0] || c)
|
|
1085
|
+
&& c in transitions.target.elements
|
|
1086
|
+
&& this.physical_column(transitions.target.elements, c)
|
|
1087
|
+
))
|
|
1027
1088
|
|
|
1028
|
-
const src = this.cqn4sql(INSERT.from)
|
|
1029
1089
|
const extractions = this._managed = this.managed(columns.map(c => ({ name: c, sql: `NEW.${this.quote(c)}` })), elements)
|
|
1030
1090
|
const sql = extractions.length > columns.length
|
|
1031
1091
|
? `SELECT ${extractions.map(c => `${c.insert} AS ${this.quote(c.name)}`)} FROM (${this.SELECT(src)}) AS NEW`
|
|
1032
1092
|
: this.SELECT(src)
|
|
1033
1093
|
if (extractions.length > columns.length) columns = this.columns = extractions.map(c => c.name)
|
|
1034
|
-
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(c))}) ${sql}`
|
|
1094
|
+
this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql}`
|
|
1035
1095
|
this.entries = [this.values]
|
|
1036
1096
|
return this.sql
|
|
1037
1097
|
}
|
|
@@ -1085,7 +1145,7 @@ class CQN2SQLRenderer {
|
|
|
1085
1145
|
.join(' AND ')
|
|
1086
1146
|
|
|
1087
1147
|
let columns = this.columns // this.columns is computed as part of this.INSERT
|
|
1088
|
-
const entity = this.
|
|
1148
|
+
const entity = q._target ? this.table_name(q) : this.name(UPSERT.into.ref[0], q)
|
|
1089
1149
|
if (UPSERT.entries || UPSERT.rows || UPSERT.values) {
|
|
1090
1150
|
const managed = this._managed.slice(0, columns.length)
|
|
1091
1151
|
|
|
@@ -1121,7 +1181,8 @@ class CQN2SQLRenderer {
|
|
|
1121
1181
|
else return true
|
|
1122
1182
|
}).map(c => `${this.quote(c)} = excluded.${this.quote(c)}`)
|
|
1123
1183
|
|
|
1124
|
-
|
|
1184
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
1185
|
+
return (this.sql = `INSERT INTO ${this.quote(entity)} (${columns.map(c => this.quote(transitions.mapping.get(c)?.ref?.[0] || c))}) ${sql
|
|
1125
1186
|
} WHERE TRUE ON CONFLICT(${keys.map(c => this.quote(c))}) DO ${updateColumns.length ? `UPDATE SET ${updateColumns}` : 'NOTHING'}`)
|
|
1126
1187
|
}
|
|
1127
1188
|
|
|
@@ -1134,29 +1195,36 @@ class CQN2SQLRenderer {
|
|
|
1134
1195
|
*/
|
|
1135
1196
|
UPDATE(q) {
|
|
1136
1197
|
const { entity, with: _with, data, where } = q.UPDATE
|
|
1198
|
+
const transitions = this.srv.resolve.transitions(q)
|
|
1137
1199
|
const elements = q._target?.elements
|
|
1138
|
-
let sql = `UPDATE ${this.quote(this.
|
|
1200
|
+
let sql = `UPDATE ${this.quote(this.table_name(q))}`
|
|
1139
1201
|
if (entity.as) sql += ` AS ${this.quote(entity.as)}`
|
|
1140
1202
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1203
|
+
const _add = (data, sql4) => {
|
|
1204
|
+
for (let col in data) {
|
|
1205
|
+
const c = transitions.mapping.get(col)?.ref?.[0] || col
|
|
1206
|
+
const columnExistsInDatabase = elements
|
|
1207
|
+
&& this.physical_column(elements, col)
|
|
1208
|
+
&& c in transitions.target.elements
|
|
1209
|
+
&& this.physical_column(transitions.target.elements, c)
|
|
1148
1210
|
if (!elements || columnExistsInDatabase) {
|
|
1149
|
-
columns.push({ name: c, sql: sql4(data[
|
|
1211
|
+
columns.push({ name: c, sql: sql4(data[col], col) })
|
|
1150
1212
|
}
|
|
1151
1213
|
}
|
|
1152
1214
|
}
|
|
1153
1215
|
|
|
1216
|
+
let columns = []
|
|
1217
|
+
if (data) _add(data, val => this.val({ val }))
|
|
1218
|
+
if (_with) _add(_with, x => this.expr(x))
|
|
1219
|
+
|
|
1154
1220
|
const extraction = this.managed(columns, elements)
|
|
1155
|
-
.filter((c, i) =>
|
|
1156
|
-
|
|
1221
|
+
.filter((c, i) => {
|
|
1222
|
+
if (transitions.mapping.get(c.name)?.ref?.length > 1) return false
|
|
1223
|
+
return columns[i] || c.onUpdate
|
|
1224
|
+
}).map((c, i) => `${this.quote(transitions.mapping.get(c.name)?.ref?.[0] || c.name)}=${!columns[i] ? c.onUpdate : c.sql}`)
|
|
1157
1225
|
|
|
1158
1226
|
sql += ` SET ${extraction}`
|
|
1159
|
-
if (where) sql += ` WHERE ${this.
|
|
1227
|
+
if (where) sql += ` WHERE ${this.where_resolved(entity.as, where, q)}`
|
|
1160
1228
|
return (this.sql = sql)
|
|
1161
1229
|
}
|
|
1162
1230
|
|
|
@@ -1168,8 +1236,9 @@ class CQN2SQLRenderer {
|
|
|
1168
1236
|
* @returns {string} SQL
|
|
1169
1237
|
*/
|
|
1170
1238
|
DELETE(q) {
|
|
1171
|
-
const { DELETE: {
|
|
1172
|
-
let sql = `DELETE FROM ${this.
|
|
1239
|
+
const { DELETE: { where, from } } = q
|
|
1240
|
+
let sql = `DELETE FROM ${this.quote(this.table_name(q))}`
|
|
1241
|
+
if (from.as) sql += ` AS ${this.quote(from.as)}`
|
|
1173
1242
|
if (where) sql += ` WHERE ${this.where(where)}`
|
|
1174
1243
|
return (this.sql = sql)
|
|
1175
1244
|
}
|
|
@@ -1382,6 +1451,16 @@ class CQN2SQLRenderer {
|
|
|
1382
1451
|
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.func || col.ref.at(-1)
|
|
1383
1452
|
}
|
|
1384
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* Calculates the Database table name of the query target
|
|
1456
|
+
* @param {import('./infer/cqn').Query} query
|
|
1457
|
+
* @returns {string} Database table name
|
|
1458
|
+
*/
|
|
1459
|
+
table_name(q) {
|
|
1460
|
+
const table = resolveTable(q._target)
|
|
1461
|
+
return this.name(table.name, { _target: table })
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1385
1464
|
/**
|
|
1386
1465
|
* Calculates the Database name of the given name
|
|
1387
1466
|
* @param {string|import('./infer/cqn').ref} name
|
|
@@ -1489,6 +1568,10 @@ class CQN2SQLRenderer {
|
|
|
1489
1568
|
})
|
|
1490
1569
|
}
|
|
1491
1570
|
|
|
1571
|
+
physical_column(elements, c) {
|
|
1572
|
+
return elements[c] && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1492
1575
|
managed_extract(name, element, converter) {
|
|
1493
1576
|
const { UPSERT, INSERT } = this.cqn
|
|
1494
1577
|
const extract = !(INSERT?.entries || UPSERT?.entries) && (INSERT?.rows || UPSERT?.rows)
|