diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1d949bf34..fa3ae269c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - "db-service": "1.8.0", - "sqlite": "1.6.0", - "postgres": "1.7.0", - "hana": "0.2.0" + "db-service": "1.9.1", + "sqlite": "1.7.1", + "postgres": "1.8.0", + "hana": "0.4.0" } diff --git a/db-service/CHANGELOG.md b/db-service/CHANGELOG.md index ea14f99b0..f1391067c 100644 --- a/db-service/CHANGELOG.md +++ b/db-service/CHANGELOG.md @@ -4,6 +4,38 @@ - The format is based on [Keep a Changelog](http://keepachangelog.com/). - This project adheres to [Semantic Versioning](http://semver.org/). +## [1.9.1](https://github.com/cap-js/cds-dbs/compare/db-service-v1.9.0...db-service-v1.9.1) (2024-05-16) + + +### Fixed + +* dont mistake non-key access with foreign key ([#642](https://github.com/cap-js/cds-dbs/issues/642)) ([2cd2349](https://github.com/cap-js/cds-dbs/commit/2cd234994d6a9e99765e56f7548a42a35279a790)) + +## [1.9.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.8.0...db-service-v1.9.0) (2024-05-08) + + +### Added + +* Add missing `func` cqn structures ([#629](https://github.com/cap-js/cds-dbs/issues/629)) ([9d7539a](https://github.com/cap-js/cds-dbs/commit/9d7539ab0fc7e70a6a00c0bd9cb4b3e362976e16)) + + +### Fixed + +* **`order by`:** reject non-fk traversals of own columns in order by ([#599](https://github.com/cap-js/cds-dbs/issues/599)) ([3288d63](https://github.com/cap-js/cds-dbs/commit/3288d63f0ee6a96580a3b2138ecb24a944371cf1)) +* Align all quote functions with @sap/cds-compiler ([#619](https://github.com/cap-js/cds-dbs/issues/619)) ([42e9828](https://github.com/cap-js/cds-dbs/commit/42e9828baf11ec55281ea634ce56ce93e6741b91)) +* assign artificial alias if selecting from anonymous subquery ([#608](https://github.com/cap-js/cds-dbs/issues/608)) ([e1a7711](https://github.com/cap-js/cds-dbs/commit/e1a77119f0a5241cfe4f50a37a473f2325ba5bde)) +* avoid spread operator ([#630](https://github.com/cap-js/cds-dbs/issues/630)) ([a39fb65](https://github.com/cap-js/cds-dbs/commit/a39fb65f9419fe60e0324741039d004b40082903)) +* flat update with arbitrary where clauses ([#598](https://github.com/cap-js/cds-dbs/issues/598)) ([f108798](https://github.com/cap-js/cds-dbs/commit/f108798c6c8035f9cdd0b9c6b8f334f1454c2faa)) +* improved `=` and `!=` with val `null` ([#626](https://github.com/cap-js/cds-dbs/issues/626)) ([cbcfe3b](https://github.com/cap-js/cds-dbs/commit/cbcfe3b15e8ebcf7e844dc5406e4bc228d4c94c9)) +* Improved placeholders and limit clause ([#567](https://github.com/cap-js/cds-dbs/issues/567)) ([d5d5dbb](https://github.com/cap-js/cds-dbs/commit/d5d5dbb7219bcef6134440715cf756fdd439f076)) +* multiple result responses ([#602](https://github.com/cap-js/cds-dbs/issues/602)) ([bf0bed4](https://github.com/cap-js/cds-dbs/commit/bf0bed4549fe816e35481b0c9a7547a522a5a593)) +* only consider persisted columns for simple operations ([#592](https://github.com/cap-js/cds-dbs/issues/592)) ([6e31bda](https://github.com/cap-js/cds-dbs/commit/6e31bda1bb15b1770b75c8971773806a26f7d452)) + + +### Changed + +* require `>= sap/cds@7.9.0` ([#627](https://github.com/cap-js/cds-dbs/issues/627)) ([f4d09e2](https://github.com/cap-js/cds-dbs/commit/f4d09e27c3b07dd88925e196aefc1087d8357f7a)) + ## [1.8.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.7.0...db-service-v1.8.0) (2024-04-12) diff --git a/db-service/lib/SQLService.js b/db-service/lib/SQLService.js index 9604eb5a3..9fc9f9f38 100644 --- a/db-service/lib/SQLService.js +++ b/db-service/lib/SQLService.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib'), +const cds = require('@sap/cds'), DEBUG = cds.debug('sql|db') const { Readable } = require('stream') const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView') @@ -135,7 +135,7 @@ class SQLService extends DatabaseService { if (query._streaming) { this._changeToStreams(cqn.SELECT.columns, rows, true, true) if (!rows.length) return - + const result = rows[0] // stream is always on position 0. Further properties like etag are inserted later. let [key, val] = Object.entries(result)[0] diff --git a/db-service/lib/common/DatabaseService.js b/db-service/lib/common/DatabaseService.js index bce71ec6b..766c06f68 100644 --- a/db-service/lib/common/DatabaseService.js +++ b/db-service/lib/common/DatabaseService.js @@ -1,7 +1,7 @@ const SessionContext = require('./session-context') const ConnectionPool = require('./generic-pool') const infer = require('../infer') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') /** @typedef {unknown} DatabaseDriver */ diff --git a/db-service/lib/common/session-context.js b/db-service/lib/common/session-context.js index 23628951b..e84d5f059 100644 --- a/db-service/lib/common/session-context.js +++ b/db-service/lib/common/session-context.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') class SessionContext { constructor(ctx) { diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 37472c8b7..f198e63b1 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const cds_infer = require('./infer') const cqn4sql = require('./cqn4sql') @@ -165,12 +165,14 @@ class CQN2SQLRenderer { /** @type {Object} */ static TypeMap = { // Utilizing cds.linked inheritance + UUID: () => `NVARCHAR(36)`, String: e => `NVARCHAR(${e.length || 5000})`, Binary: e => `VARBINARY(${e.length || 5000})`, - Int64: () => 'BIGINT', - Int32: () => 'INTEGER', + UInt8: () => 'TINYINT', Int16: () => 'SMALLINT', - UInt8: () => 'SMALLINT', + Int32: () => 'INT', + Int64: () => 'BIGINT', + Integer: () => 'INT', Integer64: () => 'BIGINT', LargeString: () => 'NCLOB', LargeBinary: () => 'BLOB', @@ -178,12 +180,11 @@ class CQN2SQLRenderer { Composition: () => false, array: () => 'NCLOB', // HANA types - /* Disabled as these types are linked to normal cds types - 'cds.hana.TINYINT': () => 'REAL', + 'cds.hana.TINYINT': () => 'TINYINT', 'cds.hana.REAL': () => 'REAL', 'cds.hana.CHAR': e => `CHAR(${e.length || 1})`, 'cds.hana.ST_POINT': () => 'ST_POINT', - 'cds.hana.ST_GEOMETRY': () => 'ST_GEO',*/ + 'cds.hana.ST_GEOMETRY': () => 'ST_GEOMETRY', } // DROP Statements ------------------------------------------------ @@ -211,7 +212,7 @@ class CQN2SQLRenderer { if (from?.join && !q.SELECT.columns) { throw new Error('CQN query using joins must specify the selected columns.') } - + // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped) if (!where && from?.ref?.length === 1 && from.ref[0]?.where) where = from.ref[0]?.where let columns = this.SELECT_columns(q) @@ -290,7 +291,7 @@ class CQN2SQLRenderer { return `extensions__->${this.string('$."' + x.element.name + '"')} as ${x.as || x.element.name}` } /////////////////////////////////////////////////////////////////////////////////////// - let sql = this.expr(x) + let sql = this.expr({ param: false, __proto__: x }) let alias = this.column_alias4(x, q) if (alias) sql += ' as ' + this.quote(alias) return sql @@ -302,7 +303,7 @@ class CQN2SQLRenderer { * @returns {string} */ column_alias4(x) { - return typeof x.as === 'string' ? x.as : x.func + return typeof x.as === 'string' ? x.as : x.func || x.val } /** @@ -387,7 +388,7 @@ class CQN2SQLRenderer { */ limit({ rows, offset }) { if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)') - return !offset ? rows.val : `${rows.val} OFFSET ${offset.val}` + return !offset ? this.val(rows) : `${this.val(rows)} OFFSET ${this.val(offset)}` } /** @@ -799,8 +800,8 @@ class CQN2SQLRenderer { if (x.param) return wrap(this.param(x)) if ('ref' in x) return wrap(this.ref(x)) if ('val' in x) return wrap(this.val(x)) - if ('xpr' in x) return wrap(this.xpr(x)) if ('func' in x) return wrap(this.func(x)) + if ('xpr' in x) return wrap(this.xpr(x)) if ('list' in x) return wrap(this.list(x)) if ('SELECT' in x) return wrap(`(${this.SELECT(x)})`) else throw cds.error`Unsupported expr: ${x}` @@ -832,18 +833,32 @@ class CQN2SQLRenderer { operator(x, i, xpr) { // Translate = to IS NULL for rhs operand being NULL literal - if (x === '=') return xpr[i + 1]?.val === null ? 'is' : '=' + if (x === '=') return xpr[i + 1]?.val === null + ? _inline_null(xpr[i + 1]) || 'is' + : '=' // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ... // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL - if (x === '==') return xpr[i + 1]?.val === null ? 'is' : _not_null(i - 1) && _not_null(i + 1) ? '=' : this.is_not_distinct_from_ + if (x === '==') return xpr[i + 1]?.val === null + ? _inline_null(xpr[i + 1]) || 'is' + : _not_null(i - 1) && _not_null(i + 1) + ? '=' + : this.is_not_distinct_from_ // Translate != to IS NULL for rhs operand being NULL literal, otherwise... // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL - if (x === '!=') return xpr[i + 1]?.val === null ? 'is not' : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_ + if (x === '!=') return xpr[i + 1]?.val === null + ? _inline_null(xpr[i + 1]) || 'is not' + : _not_null(i - 1) && _not_null(i + 1) + ? '<>' + : this.is_distinct_from_ else return x + function _inline_null(n) { + n.param = false + } + /** Checks if the operand at xpr[i+-1] can be NULL. @returns true if not */ function _not_null(i) { const operand = xpr[i] @@ -884,27 +899,34 @@ class CQN2SQLRenderer { } /** - * Renders a value into the correct SQL syntax of a placeholder for a prepared statement + * Renders a value into the correct SQL syntax or a placeholder for a prepared statement * @param {import('./infer/cqn').val} param0 * @returns {string} SQL */ val({ val, param }) { switch (typeof val) { case 'function': throw new Error('Function values not supported.') - case 'undefined': return 'NULL' + case 'undefined': val = null + break case 'boolean': return `${val}` - case 'number': return `${val}` // REVISIT for HANA case 'object': - if (val === null) return 'NULL' - if (val instanceof Date) val = val.toJSON() // returns null if invalid - else if (val instanceof Readable); // go on with default below - else if (Buffer.isBuffer(val)); // go on with default below - else if (is_regexp(val)) val = val.source - else val = JSON.stringify(val) - case 'string': // eslint-disable-line no-fallthrough + if (val !== null) { + if (val instanceof Date) val = val.toJSON() // returns null if invalid + else if (val instanceof Readable); // go on with default below + else if (Buffer.isBuffer(val)); // go on with default below + else if (is_regexp(val)) val = val.source + else val = JSON.stringify(val) + } } - if (!this.values || param === false) return this.string(val) - else this.values.push(val) + if (!this.values || param === false) { + switch (typeof val) { + case 'string': return this.string(val) + case 'object': return 'NULL' + default: + return `${val}` + } + } + this.values.push(val) return '?' } @@ -914,9 +936,32 @@ class CQN2SQLRenderer { * @param {import('./infer/cqn').func} param0 * @returns {string} SQL */ - func({ func, args }) { - args = (args || []).map(e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) })) - return this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})` + func({ func, args, xpr }) { + const wrap = e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) }) + args = args || [] + if (Array.isArray(args)) { + args = args.map(wrap) + } else if (typeof args === 'object') { + const org = args + const wrapped = { + toString: () => { + const ret = [] + for (const prop in org) { + ret.push(`${this.quote(prop)} => ${wrapped[prop]}`) + } + return ret.join(',') + } + } + for (const prop in args) { + wrapped[prop] = wrap(args[prop]) + } + args = wrapped + } else { + cds.error`Invalid arguments provided for function '${func}' (${args})` + } + const fn = this.class.Functions[func]?.apply(this.class.Functions, args) || `${func}(${args})` + if (xpr) return `${fn} ${this.xpr({ xpr })}` + return fn } /** @@ -968,8 +1013,7 @@ class CQN2SQLRenderer { quote(s) { if (typeof s !== 'string') return '"' + s + '"' if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"' - // Column names like "Order" clash with "ORDER" keyword so toUpperCase is required - if (s in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"' + if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s + '"' return s } diff --git a/db-service/lib/cqn4sql.js b/db-service/lib/cqn4sql.js index edc5e42a8..c79ca64a3 100644 --- a/db-service/lib/cqn4sql.js +++ b/db-service/lib/cqn4sql.js @@ -1,6 +1,6 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { computeColumnsToBeSearched } = require('./search') const infer = require('./infer') @@ -488,21 +488,23 @@ function cqn4sql(originalQuery, model) { } function getTransformedColumn(col) { - if (col.xpr) { - const xpr = { xpr: getTransformedTokenStream(col.xpr) } - if (col.cast) xpr.cast = col.cast - return xpr - } else if (col.func) { - const func = { + let ret + if (col.func) { + ret = { func: col.func, - args: col.args && getTransformedTokenStream(col.args), + args: getTransformedFunctionArgs(col.args), as: col.func, // may be overwritten by the explicit alias } - if (col.cast) func.cast = col.cast - return func - } else { - return copy(col) } + if (col.xpr) { + ret ??= {} + ret.xpr = getTransformedTokenStream(col.xpr) + } + if (ret) { + if (col.cast) ret.cast = col.cast + return ret + } + return copy(col) } function handleEmptyColumns(columns) { @@ -537,7 +539,7 @@ function cqn4sql(originalQuery, model) { } else if (val) { res = { val } } else if (func) { - res = { args: getTransformedTokenStream(value.args, baseLink), func: value.func } + res = { args: getTransformedFunctionArgs(value.args, baseLink), func: value.func } } if (!omitAlias) res.as = column.as || column.name || column.flatName return res @@ -931,7 +933,7 @@ function cqn4sql(originalQuery, model) { let transformedColumn if (col.SELECT) transformedColumn = transformSubquery(col) else if (col.xpr) transformedColumn = { xpr: getTransformedTokenStream(col.xpr) } - else if (col.func) transformedColumn = { args: getTransformedTokenStream(col.args), func: col.func } + else if (col.func) transformedColumn = { args: getTransformedFunctionArgs(col.args), func: col.func } // val else transformedColumn = copy(col) if (col.sort) transformedColumn.sort = col.sort @@ -1427,15 +1429,13 @@ function cqn4sql(originalQuery, model) { } } else if (token.SELECT) { result = transformSubquery(token) - } else if (token.xpr) { - result.xpr = getTransformedTokenStream(token.xpr, $baseLink) - } else if (token.func && token.args) { - result.args = token.args.map(t => { - if (!t.val) - // this must not be touched - return getTransformedTokenStream([t], $baseLink)[0] - return t - }) + } else { + if (token.xpr) { + result.xpr = getTransformedTokenStream(token.xpr, $baseLink) + } + if (token.func && token.args) { + result.args = getTransformedFunctionArgs(token.args, $baseLink) + } } transformedTokenStream.push(result) @@ -2142,6 +2142,27 @@ function cqn4sql(originalQuery, model) { return getLastStringSegment(inferred.$combinedElements[node.ref[0].id || node.ref[0]]?.[0].index) } } + function getTransformedFunctionArgs(args, $baseLink = null) { + let result = null + if (Array.isArray(args)) { + result = args.map(t => { + if (!t.val) + // this must not be touched + return getTransformedTokenStream([t], $baseLink)[0] + return t + }) + } else if (typeof args === 'object') { + result = {} + for (const prop in args) { + const t = args[prop] + if (!t.val) + // this must not be touched + result[prop] = getTransformedTokenStream([t], $baseLink)[0] + else result[prop] = t + } + } + return result + } } module.exports = Object.assign(cqn4sql, { @@ -2154,10 +2175,10 @@ module.exports = Object.assign(cqn4sql, { function calculateElementName(token) { const nonJoinRelevantAssoc = [...token.$refLinks].findIndex(l => l.definition.isAssociation && l.onlyForeignKeyAccess) let name - if (nonJoinRelevantAssoc) + if (nonJoinRelevantAssoc !== -1) // calculate fk name name = token.ref.slice(nonJoinRelevantAssoc).join('_') - else name = token.$refLinks[token.$refLinks.length - 1].definition.name + else name = getFullName(token.$refLinks[token.$refLinks.length - 1].definition) return name } @@ -2226,6 +2247,7 @@ function setElementOnColumns(col, element) { writable: true, }) } + const getName = col => col.as || col.ref?.at(-1) const idOnly = ref => ref.id || ref const is_regexp = x => x?.constructor?.name === 'RegExp' // NOTE: x instanceof RegExp doesn't work in repl diff --git a/db-service/lib/deep-queries.js b/db-service/lib/deep-queries.js index aa42f0872..7d993fa95 100644 --- a/db-service/lib/deep-queries.js +++ b/db-service/lib/deep-queries.js @@ -1,8 +1,22 @@ const cds = require('@sap/cds') -const { compareJson } = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson') const { _target_name4 } = require('./SQLService') const InsertResult = require('../lib/InsertResults') +// REVISIT: remove old path with cds^8 +let _compareJson +const compareJson = (...args) => { + if (!_compareJson) { + try { + // new path + _compareJson = require('@sap/cds/libx/_runtime/common/utils/compareJson').compareJson + } catch { + // old path + _compareJson = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson').compareJson + } + } + return _compareJson(...args) +} + const handledDeep = Symbol('handledDeep') /** @@ -248,7 +262,7 @@ const _getDeepQueries = (diff, target, root = false) => { queries.push(cqn) } - queries.push(...subQueries) + for (const q of subQueries) queries.push(q) } const insertQueries = new Map() diff --git a/db-service/lib/infer/index.js b/db-service/lib/infer/index.js index 32fccffb1..5a64c0349 100644 --- a/db-service/lib/infer/index.js +++ b/db-service/lib/infer/index.js @@ -1,6 +1,6 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const JoinTree = require('./join-tree') const { pseudos } = require('./pseudos') @@ -125,7 +125,8 @@ function infer(originalQuery, model) { } else if (from.SELECT) { const subqueryInFrom = infer(from, model) // we need the .elements in the sources // if no explicit alias is provided, we make up one - const subqueryAlias = from.as || subqueryInFrom.joinTree.addNextAvailableTableAlias('__select__', subqueryInFrom.outerQueries) + const subqueryAlias = + from.as || subqueryInFrom.joinTree.addNextAvailableTableAlias('__select__', subqueryInFrom.outerQueries) querySources[subqueryAlias] = { definition: from } } else if (typeof from === 'string') { // TODO: Create unique alias, what about duplicates? @@ -165,7 +166,7 @@ function infer(originalQuery, model) { function attachRefLinksToArg(arg, $baseLink = null, expandOrExists = false) { const { ref, xpr, args, list } = arg if (xpr) xpr.forEach(t => attachRefLinksToArg(t, $baseLink, expandOrExists)) - if (args) args.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists)) + if (args) applyToFunctionArgs(args, attachRefLinksToArg, [$baseLink, expandOrExists]) if (list) list.forEach(arg => attachRefLinksToArg(arg, $baseLink, expandOrExists)) if (!ref) return init$refLinks(arg) @@ -305,10 +306,15 @@ function infer(originalQuery, model) { if (queryElements[as]) cds.error`Duplicate definition of element “${as}”` if (col.xpr || col.SELECT) { queryElements[as] = getElementForXprOrSubquery(col) - } else if (col.func) { - col.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional + } + if (col.func) { + if (col.args) { + // {func}.args are optional + applyToFunctionArgs(col.args, inferQueryElement, [false]) + } queryElements[as] = getElementForCast(col) - } else { + } + if (!queryElements[as]) { // either binding parameter (col.param) or value queryElements[as] = col.cast ? getElementForCast(col) : getCdsTypeForVal(col.val) } @@ -494,7 +500,9 @@ function infer(originalQuery, model) { function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) { const { inExists, inExpr, inCalcElement, baseColumn, inInfixFilter } = context || {} if (column.param || column.SELECT) return // parameter references are only resolved into values on execution e.g. :val, :1 or ? - if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression + if (column.args) { + applyToFunctionArgs(column.args, inferQueryElement, [false, $baseLink, context]) + } if (column.list) column.list.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) if (column.xpr) column.xpr.forEach(token => inferQueryElement(token, false, $baseLink, { ...context, inExpr: true })) // e.g. function in expression @@ -631,13 +639,13 @@ function infer(originalQuery, model) { inInfixFilter: true, }) } else if (token.func) { - token.args?.forEach(arg => - inferQueryElement(arg, false, column.$refLinks[i], { - inExists: skipJoinsForFilter, - inExpr: true, - inInfixFilter: true, - }), - ) + if (token.args) { + applyToFunctionArgs(token.args, inferQueryElement, [ + false, + column.$refLinks[i], + { inExists: skipJoinsForFilter, inExpr: true, inInfixFilter: true }, + ]) + } } }) } @@ -885,7 +893,7 @@ function infer(originalQuery, model) { const calcElement = column.$refLinks?.[column.$refLinks.length - 1].definition || column if (alreadySeenCalcElements.has(calcElement)) return else alreadySeenCalcElements.add(calcElement) - const { ref, xpr, func } = calcElement.value + const { ref, xpr } = calcElement.value if (ref || xpr) { baseLink = baseLink || { definition: calcElement.parent, target: calcElement.parent } attachRefLinksToArg(calcElement.value, baseLink, true) @@ -899,8 +907,9 @@ function infer(originalQuery, model) { } mergePathsIntoJoinTree(calcElement.value, basePath) } - if (func) - calcElement.value.args?.forEach(arg => { + + if (calcElement.value.args) { + const processArgument = (arg, calcElement, column) => { inferQueryElement( arg, false, @@ -912,7 +921,12 @@ function infer(originalQuery, model) { ? { $refLinks: column.$refLinks.slice(0, -1), ref: column.ref.slice(0, -1) } : { $refLinks: [], ref: [] } mergePathsIntoJoinTree(arg, basePath) - }) // {func}.args are optional + } + + if (calcElement.value.args) { + applyToFunctionArgs(calcElement.value.args, processArgument, [calcElement, column]) + } + } /** * Calculates all paths from a given ref and merges them into the join tree. @@ -1202,4 +1216,9 @@ function isForeignKeyOf(e, assoc) { } const idOnly = ref => ref.id || ref +function applyToFunctionArgs(funcArgs, cb, cbArgs) { + if (Array.isArray(funcArgs)) funcArgs.forEach(arg => cb(arg, ...cbArgs)) + else if (typeof funcArgs === 'object') Object.keys(funcArgs).forEach(prop => cb(funcArgs[prop], ...cbArgs)) +} + module.exports = infer diff --git a/db-service/lib/infer/join-tree.js b/db-service/lib/infer/join-tree.js index 82b1bfc28..3eb466d23 100644 --- a/db-service/lib/infer/join-tree.js +++ b/db-service/lib/infer/join-tree.js @@ -199,7 +199,7 @@ class JoinTree { } const child = new Node($refLink, node, where, args) if (child.$refLink.definition.isAssociation) { - if (child.where || col.inline) { + if (child.where || child.$refLink.definition.on || col.inline) { // filter is always join relevant // if the column ends up in an `inline` -> each assoc step is join relevant child.$refLink.onlyForeignKeyAccess = false @@ -212,9 +212,11 @@ class JoinTree { const elements = node.$refLink?.definition.isAssociation && (node.$refLink.definition.elements || node.$refLink.definition.foreignKeys) - if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements))) - // foreign key access + if (node.$refLink && (!elements || !(child.$refLink.definition.name in elements))) { + // no foreign key access node.$refLink.onlyForeignKeyAccess = false + col.$refLinks[i - 1] = node.$refLink + } node.children.set(id, child) node = child diff --git a/db-service/package.json b/db-service/package.json index 54b6280aa..e8221c2ff 100644 --- a/db-service/package.json +++ b/db-service/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/db-service", - "version": "1.8.0", + "version": "1.9.1", "description": "CDS base database service", "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service", "repository": { @@ -29,7 +29,7 @@ "test": "jest --silent" }, "peerDependencies": { - "@sap/cds": ">=7.6" + "@sap/cds": ">=7.9" }, "license": "SEE LICENSE" } diff --git a/db-service/test/assocs/unmanaged-assocs.test.js b/db-service/test/assocs/unmanaged-assocs.test.js index b24ec4754..862edae1d 100644 --- a/db-service/test/assocs/unmanaged-assocs.test.js +++ b/db-service/test/assocs/unmanaged-assocs.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') require('../../index') // to extend cds.ql query objects with .forSQL() and alike describe('where exists assoc', () => { diff --git a/db-service/test/bookshop/db/booksWithExpr.cds b/db-service/test/bookshop/db/booksWithExpr.cds index 3fbe22032..e03d41062 100644 --- a/db-service/test/bookshop/db/booksWithExpr.cds +++ b/db-service/test/bookshop/db/booksWithExpr.cds @@ -48,6 +48,7 @@ entity Authors { dateOfDeath : Date; age: Integer = years_between(dateOfBirth, dateOfDeath); + ageNamedParams: Integer = years_between(DOB => dateOfBirth, DOD => dateOfDeath); books : Association to many Books on books.author = $self; address : Association to Addresses; diff --git a/db-service/test/cds-infer/api.test.js b/db-service/test/cds-infer/api.test.js index f0d49f8e4..701238455 100644 --- a/db-service/test/cds-infer/api.test.js +++ b/db-service/test/cds-infer/api.test.js @@ -1,6 +1,6 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const inferred = require('../../lib/infer') function _inferred(q, m = cds.model) { return inferred(q, m) diff --git a/db-service/test/cds-infer/calculated-elements.test.js b/db-service/test/cds-infer/calculated-elements.test.js index abec3665a..c72bf1997 100644 --- a/db-service/test/cds-infer/calculated-elements.test.js +++ b/db-service/test/cds-infer/calculated-elements.test.js @@ -1,7 +1,7 @@ 'use strict' const _inferred = require('../../lib/infer') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Infer types of calculated elements in select list', () => { diff --git a/db-service/test/cds-infer/column.element.test.js b/db-service/test/cds-infer/column.element.test.js index 945442770..81ed48561 100644 --- a/db-service/test/cds-infer/column.element.test.js +++ b/db-service/test/cds-infer/column.element.test.js @@ -3,7 +3,7 @@ // this property holds either the corresponding csn definition to which the column refers // or an object - potentially with type information - for expressions or values. -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test.in(__dirname + '/../bookshop') // IMPORTANT: that has to go before the requires below to avoid loading cds.env before cds.test() const cqn4sql = require('../../lib/cqn4sql') diff --git a/db-service/test/cds-infer/elements.test.js b/db-service/test/cds-infer/elements.test.js index 69a6fbd66..3bee347e4 100644 --- a/db-service/test/cds-infer/elements.test.js +++ b/db-service/test/cds-infer/elements.test.js @@ -1,7 +1,7 @@ 'use strict' // test the calculation of the elements of the query -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test.in(__dirname + '/../bookshop') const inferred = require('../../lib/infer') function _inferred(q, m = cds.model) { diff --git a/db-service/test/cds-infer/negative.test.js b/db-service/test/cds-infer/negative.test.js index 0b67712be..23a72ef1f 100644 --- a/db-service/test/cds-infer/negative.test.js +++ b/db-service/test/cds-infer/negative.test.js @@ -1,6 +1,6 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test.in(__dirname + '/../bookshop') // IMPORTANT: that has to go before the requires below to avoid loading cds.env before cds.test() const cqn4sql = require('../../lib/cqn4sql') diff --git a/db-service/test/cds-infer/nested-projections.test.js b/db-service/test/cds-infer/nested-projections.test.js index 73be79977..ebe82613b 100644 --- a/db-service/test/cds-infer/nested-projections.test.js +++ b/db-service/test/cds-infer/nested-projections.test.js @@ -1,6 +1,6 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const inferred = require('../../lib/infer') function _inferred(q, m = cds.model) { @@ -162,7 +162,7 @@ describe('nested projections', () => { { title, descr, - author. { name } + author.{ name } } as bookInfos }` let { Books } = model.entities diff --git a/db-service/test/cds-infer/source.test.js b/db-service/test/cds-infer/source.test.js index b782bbf48..2b981532a 100644 --- a/db-service/test/cds-infer/source.test.js +++ b/db-service/test/cds-infer/source.test.js @@ -1,7 +1,7 @@ 'use strict' // test the calculation of the sources of the query -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test.in(__dirname + '/../bookshop') const inferred = require('../../lib/infer') function _inferred(q, m = cds.model) { diff --git a/db-service/test/cqn2sql/__snapshots__/create.test.js.snap b/db-service/test/cqn2sql/__snapshots__/create.test.js.snap index 583aedbea..94592f9d8 100644 --- a/db-service/test/cqn2sql/__snapshots__/create.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/create.test.js.snap @@ -2,6 +2,6 @@ exports[`create with select statements Generate SQL from CREATE stmt with entity name 1`] = ` { - "sql": "CREATE TABLE Foo ( ID INTEGER, a NVARCHAR(5000), b NVARCHAR(5000), c NVARCHAR(5000), x INTEGER )", + "sql": "CREATE TABLE Foo ( ID INT, a NVARCHAR(5000), b NVARCHAR(5000), c NVARCHAR(5000), x INT )", } `; diff --git a/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap b/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap index d96cf28d8..98c13e2e9 100644 --- a/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap @@ -12,4 +12,4 @@ exports[`delete test with from ref 1`] = `"DELETE FROM Foo as Foo"`; exports[`delete test with from ref and alias 1`] = `"DELETE FROM Foo as lala"`; -exports[`delete test with from string and where clause 1`] = `"DELETE FROM Foo as Foo WHERE Foo.x < 9"`; +exports[`delete test with from string and where clause 1`] = `"DELETE FROM Foo as Foo WHERE Foo.x < ?"`; diff --git a/db-service/test/cqn2sql/__snapshots__/expression.test.js.snap b/db-service/test/cqn2sql/__snapshots__/expression.test.js.snap index 6825afe6f..9f56aeb01 100644 --- a/db-service/test/cqn2sql/__snapshots__/expression.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/expression.test.js.snap @@ -27,8 +27,11 @@ exports[`expressions ref is like pattern 1`] = ` exports[`expressions ref is regular expression 1`] = ` { - "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x between 1 and 20", - "values": [], + "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x between ? and ?", + "values": [ + 1, + 20, + ], } `; @@ -48,8 +51,11 @@ exports[`expressions ref list with one ref is in list of sub select 1`] = ` exports[`expressions with complex xpr 1`] = ` { - "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x < 9) AND (Foo.x > 1)", - "values": [], + "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x < ?) AND (Foo.x > ?)", + "values": [ + 9, + 1, + ], } `; @@ -62,7 +68,10 @@ exports[`expressions with exists 1`] = ` exports[`expressions with long xpr 1`] = ` { - "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x < 9 AND Foo.x > 1", - "values": [], + "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x < ? AND Foo.x > ?", + "values": [ + 9, + 1, + ], } `; diff --git a/db-service/test/cqn2sql/__snapshots__/function.test.js.snap b/db-service/test/cqn2sql/__snapshots__/function.test.js.snap index bf84e9b4a..bad98b4f0 100644 --- a/db-service/test/cqn2sql/__snapshots__/function.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/function.test.js.snap @@ -11,14 +11,22 @@ exports[`function contains complex 1`] = ` exports[`function fn with .xpr as argument 1`] = ` { - "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE round(Foo.x - 100,3)", - "values": [], + "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE round(Foo.x - ?,?)", + "values": [ + 100, + 3, + ], } `; exports[`function wrap xpr in concat functions in parentheses 1`] = ` { - "sql": "SELECT 2023 || (8 * 2 - 0) as something FROM Foo as Foo", - "values": [], + "sql": "SELECT ? || (? * ? - ?) as something FROM Foo as Foo", + "values": [ + 8, + 2, + 0, + 2023, + ], } `; diff --git a/db-service/test/cqn2sql/__snapshots__/select.test.js.snap b/db-service/test/cqn2sql/__snapshots__/select.test.js.snap index 4b4db3f0e..85bd71b65 100644 --- a/db-service/test/cqn2sql/__snapshots__/select.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/select.test.js.snap @@ -2,23 +2,23 @@ exports[`cqn2sql GROUP BY GROUP BY two columns 1`] = `"SELECT Foo.a,Foo.b FROM Foo as Foo GROUP BY Foo.x,Foo.c"`; -exports[`cqn2sql HAVING clauses with select specific elements with from type string with having clause 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo HAVING Foo.x < 9"`; +exports[`cqn2sql HAVING clauses with select specific elements with from type string with having clause 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo HAVING Foo.x < ?"`; -exports[`cqn2sql LIMIT with limit and offset 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT 1 OFFSET 2"`; +exports[`cqn2sql LIMIT with limit and offset 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT ? OFFSET ?"`; -exports[`cqn2sql LIMIT with limit without offset 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT 1"`; +exports[`cqn2sql LIMIT with limit without offset 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT ?"`; -exports[`cqn2sql ONE one results in limit 1 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT 1"`; +exports[`cqn2sql ONE one results in limit 1 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT ?"`; exports[`cqn2sql ORDER BY ORDER BY alias 1`] = `"SELECT Foo.a,Foo.b,count(Foo.x) as count1 FROM Foo as Foo ORDER BY count1 ASC"`; exports[`cqn2sql ORDER BY ORDER BY with @cds.collate false 1`] = `"SELECT FooCollate.ID,FooCollate.collateString,FooCollate.nonCollateString FROM FooCollate as FooCollate ORDER BY FooCollate.collateString COLLATE NOCASE ASC,FooCollate.nonCollateString ASC"`; -exports[`cqn2sql WHERE EXISTS with nested EXISTS 1`] = `"SELECT T2.ID,T2.a,T2.b,T2.c,T2.x FROM Foo as T2 WHERE exists (SELECT 1 FROM Books as T1 WHERE T1.ID = 1 and exists (SELECT 1 FROM Foo2 as T0 WHERE T0.ID = 11 and T1.ID = T0.a) and T2.ID = T1.ID)"`; +exports[`cqn2sql WHERE EXISTS with nested EXISTS 1`] = `"SELECT T2.ID,T2.a,T2.b,T2.c,T2.x FROM Foo as T2 WHERE exists (SELECT 1 as "1" FROM Books as T1 WHERE T1.ID = ? and exists (SELECT 1 as "1" FROM Foo2 as T0 WHERE T0.ID = ? and T1.ID = T0.a) and T2.ID = T1.ID)"`; -exports[`cqn2sql WHERE entries where one column holds entries smaller than 9 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x < 9"`; +exports[`cqn2sql WHERE entries where one column holds entries smaller than 9 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x < ?"`; -exports[`cqn2sql WHERE entries where one column holds entries which are in list 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x IN (1,2,3)"`; +exports[`cqn2sql WHERE entries where one column holds entries which are in list 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x IN (?,?,?)"`; exports[`cqn2sql WHERE entries where with int reference and param true 1`] = ` { @@ -34,15 +34,15 @@ exports[`cqn2sql WHERE entries where with place holder 1`] = ` } `; -exports[`cqn2sql WHERE select with a nested select in a complex where 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE ( Foo.x + 1 ) < 9 AND Foo.x IN (SELECT Foo2.a FROM Foo as Foo2 WHERE Foo2.x < 9)"`; +exports[`cqn2sql WHERE select with a nested select in a complex where 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE ( Foo.x + ? ) < ? AND Foo.x IN (SELECT Foo2.a FROM Foo as Foo2 WHERE Foo2.x < ?)"`; -exports[`cqn2sql WHERE select with a nested select in where 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE Foo.x IN (SELECT Foo2.a FROM Foo as Foo2 WHERE Foo2.x < 9)"`; +exports[`cqn2sql WHERE select with a nested select in where 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE Foo.x IN (SELECT Foo2.a FROM Foo as Foo2 WHERE Foo2.x < ?)"`; -exports[`cqn2sql WHERE where with partial cqn 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x = 9)"`; +exports[`cqn2sql WHERE where with partial cqn 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x = ?)"`; -exports[`cqn2sql WHERE where with two partial cqn 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x + 9) = 9"`; +exports[`cqn2sql WHERE where with two partial cqn 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x + ?) = ?"`; -exports[`cqn2sql WHERE with contains with multiple arguments 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.a = 0 and ifnull(instr((Foo.a,Foo.b,Foo.c,Foo.x),?),0)"`; +exports[`cqn2sql WHERE with contains with multiple arguments 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.a = ? and ifnull(instr((Foo.a,Foo.b,Foo.c,Foo.x),?),0)"`; exports[`cqn2sql WHERE with contains with one column in where clause 1`] = ` { @@ -55,8 +55,9 @@ exports[`cqn2sql WHERE with contains with one column in where clause 1`] = ` exports[`cqn2sql WHERE with list of values 1`] = ` { - "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.a,Foo.b,1) = (Foo.c,?,Foo.x)", + "sql": "SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.a,Foo.b,?) = (Foo.c,?,Foo.x)", "values": [ + 1, "d", ], } @@ -64,15 +65,15 @@ exports[`cqn2sql WHERE with list of values 1`] = ` exports[`cqn2sql WHERE with select with exist in where condition 1`] = `"SELECT T1.ID,T1.a,T1.b,T1.c,T1.x FROM Foo as T1 WHERE exists (SELECT Foo2.ID,Foo2.name,Foo2.a FROM Foo2 as Foo2)"`; -exports[`cqn2sql aggregation functions with select with count(1) 1`] = `"SELECT count(1) as count FROM Foo as Foo"`; +exports[`cqn2sql aggregation functions with select with count(1) 1`] = `"SELECT count(?) as count FROM Foo as Foo"`; -exports[`cqn2sql aggregation functions with select with different functions without alias in elements 1`] = `"SELECT min(Foo.x) as min,count(1) as count,sum(Foo.x) as sum FROM Foo as Foo"`; +exports[`cqn2sql aggregation functions with select with different functions without alias in elements 1`] = `"SELECT min(Foo.x) as min,count(?) as count,sum(Foo.x) as sum FROM Foo as Foo"`; -exports[`cqn2sql aggregation functions with select with functions in elements new notation 1`] = `"SELECT min(Foo.x) as foo1,Foo.a,count(*) as foo2,count(1) as foo3,sum(Foo.x) as foo4 FROM Foo as Foo"`; +exports[`cqn2sql aggregation functions with select with functions in elements new notation 1`] = `"SELECT min(Foo.x) as foo1,Foo.a,count(*) as foo2,count(?) as foo3,sum(Foo.x) as foo4 FROM Foo as Foo"`; -exports[`cqn2sql aggregation functions with select with functions in where clause new notation 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE max(Foo.x) < 9"`; +exports[`cqn2sql aggregation functions with select with functions in where clause new notation 1`] = `"SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo WHERE max(Foo.x) < ?"`; -exports[`cqn2sql complex combinations AS, sub query 1`] = `"SELECT Foo.a,Foo.b as B,1 as C,Foo.x + 2 as D,(SELECT Foo2.ID,Foo2.a,Foo2.b,Foo2.c,Foo2.x FROM Foo as Foo2) as E FROM Foo as Foo"`; +exports[`cqn2sql complex combinations AS, sub query 1`] = `"SELECT Foo.a,Foo.b as B,1 as C,Foo.x + ? as D,(SELECT Foo2.ID,Foo2.a,Foo2.b,Foo2.c,Foo2.x FROM Foo as Foo2) as E FROM Foo as Foo"`; exports[`cqn2sql complex combinations Exists in object mode in complex where 1`] = ` { @@ -85,12 +86,13 @@ exports[`cqn2sql complex combinations Exists in object mode in complex where 1`] } `; -exports[`cqn2sql complex combinations WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET 1`] = `"SELECT Foo.x + 1 as foo1,Foo.b,Foo.c FROM Foo as Foo WHERE Foo.ID = 111 GROUP BY Foo.x HAVING Foo.x < 9 ORDER BY c ASC LIMIT 11 OFFSET 22"`; +exports[`cqn2sql complex combinations WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET 1`] = `"SELECT Foo.x + ? as foo1,Foo.b,Foo.c FROM Foo as Foo WHERE Foo.ID = ? GROUP BY Foo.x HAVING Foo.x < ? ORDER BY c ASC LIMIT ? OFFSET ?"`; -exports[`cqn2sql functions new notation function with multiple xpr 1`] = `"SELECT replace_regexpr(Foo.a,5,? flag ? in ? with ?) as replaced FROM Foo as Foo"`; +exports[`cqn2sql functions new notation function with multiple xpr 1`] = `"SELECT replace_regexpr(Foo.a,?,? flag ? in ? with ?) as replaced FROM Foo as Foo"`; exports[`cqn2sql functions new notation function with multiple xpr 2`] = ` [ + 5, "A", "i", "ABC-abc-AAA-aaa", @@ -130,7 +132,7 @@ exports[`cqn2sql functions new notation in filter with 3 arg new notation 1`] = } `; -exports[`cqn2sql functions new notation in filter with asterisk as arg new notation 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo HAVING count(*) > 1"`; +exports[`cqn2sql functions new notation in filter with asterisk as arg new notation 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo HAVING count(*) > ?"`; exports[`cqn2sql functions new notation in filter with nested functions new notation 1`] = ` { @@ -142,19 +144,23 @@ exports[`cqn2sql functions new notation in filter with nested functions new nota } `; -exports[`cqn2sql functions new notation in filter with subselect as function param 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.ID = any((SELECT Foo2.ID FROM Foo2 as Foo2 WHERE Foo2.a = 1))"`; +exports[`cqn2sql functions new notation in filter with subselect as function param 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.ID = any((SELECT Foo2.ID FROM Foo2 as Foo2 WHERE Foo2.a = ?))"`; exports[`cqn2sql functions new notation in orderby with 1 arg new notation 1`] = `"SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo ORDER BY lower(Foo.c) DESC"`; -exports[`cqn2sql quoted column aliases select with simple subselect and column aliases 1`] = `"SELECT Foo.a,Foo.b as B,1 as C,Foo.x + 2 as D,(SELECT Foo2.a,Foo2.b as B,false as False,Foo2.x + 2 as Xpr FROM Foo as Foo2) as E FROM Foo as Foo"`; +exports[`cqn2sql quoted column aliases select with simple subselect and column aliases 1`] = `"SELECT Foo.a,Foo.b as B,1 as C,Foo.x + ? as D,(SELECT Foo2.a,Foo2.b as B,false as False,Foo2.x + ? as Xpr FROM Foo as Foo2) as E FROM Foo as Foo"`; -exports[`cqn2sql quoted column aliases select with subselect in exists and column aliases 1`] = `"SELECT T2.id,T2.version,T2.parent_ID FROM Author as T2 WHERE exists (SELECT 1 as One,T1.code as Xpr1 FROM Books as T1 WHERE T1.ID = 1 and exists (SELECT 3 as Three,T0.x + 1 as Xpr2 FROM Foo as T0 WHERE T0.ID = 11 and T1.ID = T0.b))"`; +exports[`cqn2sql quoted column aliases select with subselect in exists and column aliases 1`] = `"SELECT T2.id,T2.version,T2.parent_ID FROM Author as T2 WHERE exists (SELECT 1 as One,T1.code as Xpr1 FROM Books as T1 WHERE T1.ID = ? and exists (SELECT 3 as Three,T0.x + ? as Xpr2 FROM Foo as T0 WHERE T0.ID = ? and T1.ID = T0.b))"`; exports[`cqn2sql quoted column aliases select with subselect with in and column aliases 1`] = ` { - "sql": "SELECT Foo.a as A,? as ABC,Foo.x + 1 as Xpr1 FROM Foo as Foo WHERE ( Foo.x + 1 ) < 9 AND Foo.x IN (SELECT Foo2.a as B,Foo2.x - 4 as Xpr2 FROM Foo as Foo2 WHERE Foo2.x < 9)", + "sql": "SELECT Foo.a as A,'abc' as ABC,Foo.x + ? as Xpr1 FROM Foo as Foo WHERE ( Foo.x + ? ) < ? AND Foo.x IN (SELECT Foo2.a as B,Foo2.x - ? as Xpr2 FROM Foo as Foo2 WHERE Foo2.x < ?)", "values": [ - "abc", + 1, + 1, + 9, + 4, + 9, ], } `; diff --git a/db-service/test/cqn2sql/__snapshots__/update.test.js.snap b/db-service/test/cqn2sql/__snapshots__/update.test.js.snap index e3aac925a..a861c8389 100644 --- a/db-service/test/cqn2sql/__snapshots__/update.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/update.test.js.snap @@ -2,15 +2,21 @@ exports[`.update data alone still works 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1,name=NULL,a=NULL", - "values": [], + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?,a=?", + "values": [ + 1, + null, + null, + ], } `; exports[`.update set enhances data 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET a=2,ID=1,name=?", + "sql": "UPDATE Foo2 AS Foo2 SET a=?,ID=?,name=?", "values": [ + 2, + 1, "'asd'", ], } @@ -18,44 +24,59 @@ exports[`.update set enhances data 1`] = ` exports[`.update set overwrites data 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET a=2,ID=1,name=?,a=6", + "sql": "UPDATE Foo2 AS Foo2 SET a=?,ID=?,name=?,a=?", "values": [ + 2, + 1, "'asd'", + 6, ], } `; exports[`.update test with entity and values with operators 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=42,name=?,a=Foo2.a - 1", + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?,a=Foo2.a - ?", "values": [ + 42, "'asd'", + 1, ], } `; exports[`.update test with entity of type string 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1,name=?,a=2", + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?,a=?", "values": [ + 1, "'asd'", + 2, ], } `; exports[`.update test with entity of type string and where clause 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1,name=?,a=2 WHERE Foo2.a < 9", + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?,a=? WHERE Foo2.a < ?", "values": [ + 1, "'asd'", + 2, + 9, ], } `; exports[`.update test with setting a value to null 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1,name=NULL,a=2 WHERE Foo2.a < 9", - "values": [], + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?,a=? WHERE Foo2.a < ?", + "values": [ + 1, + null, + 2, + 9, + ], } `; @@ -68,8 +89,9 @@ exports[`.update test with subselect - sflight example 1`] = ` exports[`.update virtual and non-existing fields filtered out from with 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1,name=?", + "sql": "UPDATE Foo2 AS Foo2 SET ID=?,name=?", "values": [ + 1, "'asd'", ], } @@ -77,7 +99,9 @@ exports[`.update virtual and non-existing fields filtered out from with 1`] = ` exports[`.update virtual and non-existing filtered out from data 1`] = ` { - "sql": "UPDATE Foo2 AS Foo2 SET ID=1", - "values": [], + "sql": "UPDATE Foo2 AS Foo2 SET ID=?", + "values": [ + 1, + ], } `; diff --git a/db-service/test/cqn2sql/create.test.js b/db-service/test/cqn2sql/create.test.js index 0c7d9d704..73cea65e2 100644 --- a/db-service/test/cqn2sql/create.test.js +++ b/db-service/test/cqn2sql/create.test.js @@ -1,5 +1,5 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) diff --git a/db-service/test/cqn2sql/delete.test.js b/db-service/test/cqn2sql/delete.test.js index bef100c13..2b89725e7 100644 --- a/db-service/test/cqn2sql/delete.test.js +++ b/db-service/test/cqn2sql/delete.test.js @@ -1,5 +1,5 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) diff --git a/db-service/test/cqn2sql/drop.test.js b/db-service/test/cqn2sql/drop.test.js index bf73672ea..c974bf19a 100644 --- a/db-service/test/cqn2sql/drop.test.js +++ b/db-service/test/cqn2sql/drop.test.js @@ -1,5 +1,5 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) diff --git a/db-service/test/cqn2sql/expression.test.js b/db-service/test/cqn2sql/expression.test.js index 13ad6935a..2093690f5 100644 --- a/db-service/test/cqn2sql/expression.test.js +++ b/db-service/test/cqn2sql/expression.test.js @@ -1,9 +1,9 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) -} +} beforeAll(async () => { cds.model = await cds.load(__dirname + '/testModel').then(cds.linked) @@ -38,8 +38,9 @@ describe('expressions', () => { where: [{ ref: ['x'] }, '=', { val: null }], }, } - const { sql } = cqn2sql(cqn) + const { sql, values } = cqn2sql(cqn) expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x IS NULL/i) + expect(values).toEqual([]) }) // We should never have supported that! @@ -50,8 +51,9 @@ describe('expressions', () => { where: [{ val: null }, '=', { ref: ['x'] }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE null = Foo.x/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE \? = Foo.x/i) + expect(values).toEqual([null]) }) // We should never have supported that! @@ -62,8 +64,9 @@ describe('expressions', () => { where: [{ val: null }, '=', { val: null }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE NULL IS NULL/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE \? IS NULL/i) + expect(values).toEqual([null]) }) test('ref != null', () => { @@ -73,8 +76,9 @@ describe('expressions', () => { where: [{ ref: ['x'] }, '!=', { val: null }], }, } - const { sql } = cqn2sql(cqn) + const { sql, values } = cqn2sql(cqn) expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x IS NOT NULL/i) + expect(values).toEqual([]) }) test('val != val', () => { @@ -84,8 +88,9 @@ describe('expressions', () => { where: [{ val: 5 }, '!=', { val: 6 }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE 5 <> 6/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE \? <> \?/i) + expect(values).toEqual([5, 6]) }) test('ref != ref', () => { @@ -109,8 +114,9 @@ describe('expressions', () => { where: [{ val: null }, '!=', { ref: ['x'] }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE null is distinct from Foo.x/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE \? is distinct from Foo.x/i) + expect(values).toEqual([null]) }) test('ref != 5', () => { @@ -120,8 +126,9 @@ describe('expressions', () => { where: [{ ref: ['x'] }, '!=', { val: 5 }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x is distinct from 5/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x is distinct from \?/i) + expect(values).toEqual([5]) }) test('ref <> 5', () => { @@ -131,8 +138,9 @@ describe('expressions', () => { where: [{ ref: ['x'] }, '<>', { val: 5 }], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x <> 5/i) + const { sql, values } = cqn2sql(cqn) + expect(sql).toMatch(/SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x <> \?/i) + expect(values).toEqual([5]) }) test('ref != 5 and more', () => { @@ -142,10 +150,11 @@ describe('expressions', () => { where: [{ ref: ['x'] }, '=', { val: 7 }, 'or', { ref: ['x'] }, '!=', { val: 5 }], }, } - const { sql } = cqn2sql(cqn) + const { sql, values } = cqn2sql(cqn) expect(sql).toMatch( - /SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x = 7 or Foo.x is distinct from 5/i, + /SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE Foo.x = \? or Foo.x is distinct from \?/i, ) + expect(values).toEqual([7, 5]) }) // We don't have to support that @@ -158,8 +167,8 @@ describe('expressions', () => { } const { sql, values } = cqn2sql(cqn) expect({ sql, values }).toEqual({ - sql: 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE 5 is distinct from Foo.x', - values: [], + sql: 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE ? is distinct from Foo.x', + values: [5], }) }) @@ -177,10 +186,11 @@ describe('expressions', () => { ], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toEqual( - 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x is distinct from 5) or (Foo.x is NULL)', - ) + const { sql, values } = cqn2sql(cqn) + expect({ sql, values }).toEqual({ + sql: 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE (Foo.x is distinct from ?) or (Foo.x is NULL)', + values: [5] + }) }) test('ref is like pattern', () => { @@ -365,9 +375,11 @@ describe('expressions', () => { ], }, } - const { sql } = cqn2sql(cqn) - expect(sql).toEqual( - 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE ROW_NUMBER(1) OVER (PARTITION BY Foo.b ORDER BY Foo.x desc)', - ) + + const { sql, values } = cqn2sql(cqn) + expect({ sql, values }).toEqual({ + sql: 'SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE ROW_NUMBER(?) OVER (PARTITION BY Foo.b ORDER BY Foo.x desc)', + values: [1] + }) }) }) diff --git a/db-service/test/cqn2sql/function.test.js b/db-service/test/cqn2sql/function.test.js index bfc9b2f7e..ccb4177b0 100644 --- a/db-service/test/cqn2sql/function.test.js +++ b/db-service/test/cqn2sql/function.test.js @@ -1,8 +1,8 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) -} +} beforeAll(async () => { cds.model = await cds.load(__dirname + '/testModel').then(cds.linked) @@ -211,4 +211,46 @@ describe('function', () => { const { sql } = cqn2sql(cqn) expect(sql).toEqual('SELECT Foo.ID,Foo.a,Foo.b,Foo.c,Foo.x FROM Foo as Foo WHERE current_date') }) + + test('fn with named arguments', () => { + const func = { + func: 'convert_currency', + args: { + amount: { ref: ['a'] }, + source_unit: { ref: ['b'] }, + target_unit: { val: 'USD' }, + } + } + const cqn = { + SELECT: { + columns: [func], + from: { ref: ['Foo'] }, + where: [func], + }, + } + + const { sql, values } = cqn2sql(cqn) + expect({ sql, values }).toEqual({ + sql: 'SELECT convert_currency(amount => Foo.a,source_unit => Foo.b,target_unit => ?) as convert_currency FROM Foo as Foo WHERE convert_currency(amount => Foo.a,source_unit => Foo.b,target_unit => ?)', + values: ['USD', 'USD'], + }) + }) + + test('fn with xpr extension', () => { + const cqn = { + SELECT: { + from: { ref: ['Foo'] }, + columns: [{ + func: 'row_number', + args: [], + xpr: ['over', { xpr: ['partition', 'by', { ref: ['a'] }] }] + }] + }, + } + + const { sql } = cqn2sql(cqn) + expect({ sql }).toEqual({ + sql: 'SELECT row_number() over (partition by Foo.a) as row_number FROM Foo as Foo', + }) + }) }) diff --git a/db-service/test/cqn2sql/insert.test.js b/db-service/test/cqn2sql/insert.test.js index f83d41d3b..7e4b25c6a 100644 --- a/db-service/test/cqn2sql/insert.test.js +++ b/db-service/test/cqn2sql/insert.test.js @@ -1,7 +1,7 @@ 'use strict' const { text } = require('stream/consumers') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') describe('insert', () => { diff --git a/db-service/test/cqn2sql/select.test.js b/db-service/test/cqn2sql/select.test.js index c802968b3..23d3f86aa 100644 --- a/db-service/test/cqn2sql/select.test.js +++ b/db-service/test/cqn2sql/select.test.js @@ -1,10 +1,10 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { - + return _cqn2sql(q, m) -} +} const cqn = require('./cqn.js') // const getExpected = (sql, values) => { @@ -262,8 +262,9 @@ describe('cqn2sql', () => { test('one with additional limit with offset', () => { // Original DB layer expectation is to mix limit and one // One has priority over limit.rows, but limit.offset is still applied - const { sql } = cqn2sql(cqn.oneWithLimit) - expect(sql).toEqual('SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT 1 OFFSET 5') + const { sql, values } = cqn2sql(cqn.oneWithLimit) + expect(sql).toEqual('SELECT Foo.a,Foo.b,Foo.c FROM Foo as Foo LIMIT ? OFFSET ?') + expect(values).toEqual([1, 5]) }) }) diff --git a/db-service/test/cqn2sql/update.test.js b/db-service/test/cqn2sql/update.test.js index 45eecfca5..8cdca1e1f 100644 --- a/db-service/test/cqn2sql/update.test.js +++ b/db-service/test/cqn2sql/update.test.js @@ -1,9 +1,9 @@ 'use strict' -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) -} +} beforeAll(async () => { cds.model = await cds.load(__dirname + '/testModel').then(cds.linked) diff --git a/db-service/test/cqn2sql/upsert.test.js b/db-service/test/cqn2sql/upsert.test.js index 6f5e89df9..4e9c563d5 100644 --- a/db-service/test/cqn2sql/upsert.test.js +++ b/db-service/test/cqn2sql/upsert.test.js @@ -1,11 +1,11 @@ 'use strict' const { text } = require('stream/consumers') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const _cqn2sql = require('../../lib/cqn2sql') function cqn2sql(q, m = cds.model) { return _cqn2sql(q, m) -} +} beforeAll(async () => { cds.model = await cds.load(__dirname + '/testModel').then(cds.linked) diff --git a/db-service/test/cqn4sql/API.test.js b/db-service/test/cqn4sql/API.test.js index e6acdcf37..46d3325c5 100644 --- a/db-service/test/cqn4sql/API.test.js +++ b/db-service/test/cqn4sql/API.test.js @@ -5,7 +5,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Repetitive calls to cqn4sql must work', () => { let model diff --git a/db-service/test/cqn4sql/DELETE.test.js b/db-service/test/cqn4sql/DELETE.test.js index 1af2b3585..fa772a502 100644 --- a/db-service/test/cqn4sql/DELETE.test.js +++ b/db-service/test/cqn4sql/DELETE.test.js @@ -1,7 +1,7 @@ // cqn4sql must flatten and transform where exists shortcuts into subqueries 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('DELETE', () => { diff --git a/db-service/test/cqn4sql/INSERT.test.js b/db-service/test/cqn4sql/INSERT.test.js index 84f657c77..b51c59809 100644 --- a/db-service/test/cqn4sql/INSERT.test.js +++ b/db-service/test/cqn4sql/INSERT.test.js @@ -1,7 +1,7 @@ // not much to do for cqn4sql in case of INSERT/UPSERT 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('INSERT', () => { diff --git a/db-service/test/cqn4sql/UPDATE.test.js b/db-service/test/cqn4sql/UPDATE.test.js index 20c83e37f..5dc37d8f2 100644 --- a/db-service/test/cqn4sql/UPDATE.test.js +++ b/db-service/test/cqn4sql/UPDATE.test.js @@ -1,7 +1,7 @@ // cqn4sql must flatten and transform where exists shortcuts into subqueries 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('UPDATE', () => { diff --git a/db-service/test/cqn4sql/assocs2joins.test.js b/db-service/test/cqn4sql/assocs2joins.test.js index 1347c28e2..a7f028609 100644 --- a/db-service/test/cqn4sql/assocs2joins.test.js +++ b/db-service/test/cqn4sql/assocs2joins.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Unfolding Association Path Expressions to Joins', () => { @@ -1233,6 +1233,36 @@ describe('optimize fk access', () => { expect(cqn4sql(query, model)).to.deep.equal(expected) }) + it('association as key leads to non-key field', () => { + const query = CQL`SELECT from Pupils { + ID + } group by classrooms.classroom.ID, classrooms.classroom.name` + const expected = CQL`SELECT from Pupils as Pupils + left join ClassroomsPupils as classrooms + on classrooms.pupil_ID = Pupils.ID + left join Classrooms as classroom + on classroom.ID = classrooms.classroom_ID + { + Pupils.ID + } group by classroom.ID, classroom.name` + + expect(cqn4sql(query, model)).to.deep.equal(expected) + }) + it('association as key leads to nested non-key field', () => { + const query = CQL`SELECT from Pupils { + ID + } group by classrooms.classroom.ID, classrooms.classroom.info.capacity` + const expected = CQL`SELECT from Pupils as Pupils + left join ClassroomsPupils as classrooms + on classrooms.pupil_ID = Pupils.ID + left join Classrooms as classroom + on classroom.ID = classrooms.classroom_ID + { + Pupils.ID + } group by classroom.ID, classroom.info_capacity` + + expect(cqn4sql(query, model)).to.deep.equal(expected) + }) it('two step path ends in foreign key simple ref', () => { const query = CQL`SELECT from Classrooms { pupils.pupil.ID as studentCount, diff --git a/db-service/test/cqn4sql/basic.test.js b/db-service/test/cqn4sql/basic.test.js index f239cd80b..f9843350a 100644 --- a/db-service/test/cqn4sql/basic.test.js +++ b/db-service/test/cqn4sql/basic.test.js @@ -4,7 +4,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('query clauses', () => { let model diff --git a/db-service/test/cqn4sql/calculated-elements.test.js b/db-service/test/cqn4sql/calculated-elements.test.js index 421bbefcc..145fcac5d 100644 --- a/db-service/test/cqn4sql/calculated-elements.test.js +++ b/db-service/test/cqn4sql/calculated-elements.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Unfolding calculated elements in select list', () => { @@ -54,6 +54,14 @@ describe('Unfolding calculated elements in select list', () => { }` expect(query).to.deep.equal(expected) }) + it('in function with named param', () => { + let query = cqn4sql(CQL`SELECT from booksCalc.Authors { ID, ageNamedParams as f }`, model) + const expected = CQL`SELECT from booksCalc.Authors as Authors { + Authors.ID, + years_between(DOB => Authors.dateOfBirth, DOD => Authors.dateOfDeath) as f + }` + expect(query).to.deep.equal(expected) + }) it('calc elem is function', () => { let query = cqn4sql(CQL`SELECT from booksCalc.Books { ID, ctitle }`, model) @@ -361,7 +369,7 @@ describe('Unfolding calculated elements in select list', () => { { Books.ID, ( - SELECT from booksCalc.Authors as author + SELECT from booksCalc.Authors as author left join booksCalc.Addresses as address on address.ID = author.address_ID { author.firstName || ' ' || author.lastName as name, @@ -384,7 +392,7 @@ describe('Unfolding calculated elements in select list', () => { author.firstName || ' ' || author.lastName as author_name, address.street || ', ' || address.city as author_addressText, ( - SELECT from booksCalc.Authors as author2 + SELECT from booksCalc.Authors as author2 left join booksCalc.Addresses as address2 on address2.ID = author2.address_ID { author2.firstName || ' ' || author2.lastName as name, @@ -405,7 +413,7 @@ describe('Unfolding calculated elements in select list', () => { { Books.ID, ( - SELECT from booksCalc.Authors as author2 + SELECT from booksCalc.Authors as author2 left join booksCalc.Addresses as address2 on address2.ID = author2.address_ID { author2.firstName || ' ' || author2.lastName as name, @@ -510,14 +518,11 @@ describe('Unfolding calculated elements in select list', () => { }) it('wildcard select from subquery', () => { - let query = cqn4sql( - CQL`SELECT from ( SELECT FROM booksCalc.Simple { * } )`, - model, - ) + let query = cqn4sql(CQL`SELECT from ( SELECT FROM booksCalc.Simple { * } )`, model) const expected = CQL` SELECT from ( SELECT from booksCalc.Simple as Simple - left join booksCalc.Simple as my on my.ID = Simple.my_ID + left join booksCalc.Simple as my on my.ID = Simple.my_ID { Simple.ID, Simple.name, @@ -544,7 +549,7 @@ describe('Unfolding calculated elements in select list', () => { const expected = CQL` SELECT from ( SELECT from booksCalc.Simple as Simple - left join booksCalc.Simple as my2 on my2.ID = Simple.my_ID + left join booksCalc.Simple as my2 on my2.ID = Simple.my_ID { Simple.ID, Simple.name, @@ -606,7 +611,7 @@ describe('Unfolding calculated elements in select list', () => { authorName, authorFullName, authorAge - } + } `, model, ) @@ -738,7 +743,7 @@ describe('Unfolding calculated elements in select list', () => { CQL` SELECT from booksCalc.Authors { books { * } excluding { length, width, height, stock, price} - } + } `, model, ) @@ -785,7 +790,7 @@ describe('Unfolding calculated elements in select list', () => { CQL` SELECT from booksCalc.Authors { books { * } excluding { length, width, height, stock, price, youngAuthorName} - } + } `, model, ) @@ -878,7 +883,7 @@ describe('Unfolding calculated elements in other places', () => { let query = cqn4sql(CQL`SELECT from booksCalc.Authors[name like 'A%'].books[storageVolume < 4] { ID }`, model) const expected = CQL`SELECT from booksCalc.Books as books { books.ID - } where exists (select 1 from booksCalc.Authors as Authors + } where exists (select 1 from booksCalc.Authors as Authors where Authors.ID = books.author_ID and (Authors.firstName || ' ' || Authors.lastName) like 'A%') and (books.stock * ((books.length * books.width) * books.height)) < 4 diff --git a/db-service/test/cqn4sql/column.element.test.js b/db-service/test/cqn4sql/column.element.test.js index 79343202b..0982d2dcd 100644 --- a/db-service/test/cqn4sql/column.element.test.js +++ b/db-service/test/cqn4sql/column.element.test.js @@ -2,7 +2,7 @@ // for convenience, we attach a non-enumerable property 'element' onto each column with a ref // this property holds the corresponding csn definition to which the column refers -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test.in(__dirname + '/../bookshop') // IMPORTANT: that has to go before the requires below to avoid loading cds.env before cds.test() const cqn4sql = require('../../lib/cqn4sql') @@ -135,7 +135,7 @@ describe('assign element onto columns with flat model', () => { expect(query.SELECT.columns[2]).to.have.property('element').that.eqls(AssocWithStructuredKey.elements.toStructuredKey_struct_mid_anotherLeaf.__proto__) } else { expect(query.SELECT.columns[1]).to.have.property('element').that.eqls(AssocWithStructuredKey.elements.toStructuredKey_struct_mid_leaf) - expect(query.SELECT.columns[2]).to.have.property('element').that.eqls(AssocWithStructuredKey.elements.toStructuredKey_struct_mid_anotherLeaf) + expect(query.SELECT.columns[2]).to.have.property('element').that.eqls(AssocWithStructuredKey.elements.toStructuredKey_struct_mid_anotherLeaf) } diff --git a/db-service/test/cqn4sql/compare-structs.test.js b/db-service/test/cqn4sql/compare-structs.test.js index 4d63569a0..fd07d3b83 100644 --- a/db-service/test/cqn4sql/compare-structs.test.js +++ b/db-service/test/cqn4sql/compare-structs.test.js @@ -7,7 +7,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('compare structures', () => { diff --git a/db-service/test/cqn4sql/expand.test.js b/db-service/test/cqn4sql/expand.test.js index 6f1a2b56f..d1f3d4f97 100644 --- a/db-service/test/cqn4sql/expand.test.js +++ b/db-service/test/cqn4sql/expand.test.js @@ -4,7 +4,7 @@ const _cqn4sql = require('../../lib/cqn4sql') function cqn4sql(q, model = cds.model) { return _cqn4sql(q, model) } -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const transitive_ = !cds.unfold || 'transitive_localized_views' in cds.env.sql && cds.env.sql.transitive_localized_views !== false diff --git a/db-service/test/cqn4sql/flattening.test.js b/db-service/test/cqn4sql/flattening.test.js index c6cf9221c..d5815f92f 100644 --- a/db-service/test/cqn4sql/flattening.test.js +++ b/db-service/test/cqn4sql/flattening.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const _inferred = require('../../lib/infer') diff --git a/db-service/test/cqn4sql/functions.test.js b/db-service/test/cqn4sql/functions.test.js index 9fb961f11..4889d1949 100644 --- a/db-service/test/cqn4sql/functions.test.js +++ b/db-service/test/cqn4sql/functions.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('functions', () => { let model @@ -53,6 +53,116 @@ describe('functions', () => { }) }) + describe('with named parameters', () => { + it('in column', () => { + const q = CQL`SELECT from bookshop.Books { + getAuthorsName( author => author.name, book => title ) as foo + } ` + const qx = CQL` + SELECT from bookshop.Books as Books left join bookshop.Authors as author on author.ID = Books.author_ID + { + getAuthorsName( author => author.name, book => Books.title ) as foo + }` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + it('in infix filter', () => { + const q = CQL`SELECT from bookshop.Books { + author[ 'King' = getAuthorsName( author => ID ) ].ID as foo + } ` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID and + 'King' = getAuthorsName( author => author.ID ) + { + author.ID as foo + }` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + it('in where', () => { + const q = CQL`SELECT from bookshop.Books { + ID + } where getAuthorsName( author => author.name ) = 'King'` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID + { + Books.ID + } where getAuthorsName( author => author.name ) = 'King'` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + + it('in order by', () => { + const q = CQL`SELECT from bookshop.Books { + ID + } order by getAuthorsName( author => author.name )` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID + { + Books.ID + } order by getAuthorsName( author => author.name )` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + + it('in group by', () => { + const q = CQL`SELECT from bookshop.Books { + ID + } group by getAuthorsName( author => author.name )` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID + { + Books.ID + } group by getAuthorsName( author => author.name )` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + + it('in having', () => { + const q = CQL`SELECT from bookshop.Books { + ID + } having getAuthorsName( author => author.name ) = 'King'` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID + { + Books.ID + } having getAuthorsName( author => author.name ) = 'King'` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + + it('in xpr', () => { + const q = CQL`SELECT from bookshop.Books { + ID + } where ('Stephen ' + getAuthorsName( author => author.name )) = 'Stephen King'` + const qx = CQL` + SELECT from bookshop.Books as Books + left join bookshop.Authors as author on author.ID = Books.author_ID + { + Books.ID + } where ('Stephen ' + getAuthorsName( author => author.name )) = 'Stephen King'` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + it('in from', () => { + const q = CQL`SELECT from bookshop.Books[getAuthorsName( author => author.ID ) = 1] { + ID + }` + const qx = CQL` + SELECT from bookshop.Books as Books + { + Books.ID + } where getAuthorsName( author => Books.author_ID ) = 1` + const res = cqn4sql(q, model) + expect(res).to.deep.equal(qx) + }) + }) + describe('without arguments', () => { it('function in filter in order by', () => { let query = { diff --git a/db-service/test/cqn4sql/inline.test.js b/db-service/test/cqn4sql/inline.test.js index 5e5525959..3241179f1 100644 --- a/db-service/test/cqn4sql/inline.test.js +++ b/db-service/test/cqn4sql/inline.test.js @@ -4,7 +4,7 @@ const _cqn4sql = require('../../lib/cqn4sql') function cqn4sql(q, model = cds.model) { return _cqn4sql(q, model) } -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('inline', () => { let model diff --git a/db-service/test/cqn4sql/localized.test.js b/db-service/test/cqn4sql/localized.test.js index 5a6337de1..757317638 100644 --- a/db-service/test/cqn4sql/localized.test.js +++ b/db-service/test/cqn4sql/localized.test.js @@ -2,7 +2,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const transitive_ = !cds.unfold || 'transitive_localized_views' in cds.env.sql && cds.env.sql.transitive_localized_views !== false diff --git a/db-service/test/cqn4sql/not-persisted.test.js b/db-service/test/cqn4sql/not-persisted.test.js index 467e1895f..19769cb20 100644 --- a/db-service/test/cqn4sql/not-persisted.test.js +++ b/db-service/test/cqn4sql/not-persisted.test.js @@ -13,7 +13,7 @@ If the path exists within an `xpr` we do not filter it out, but process it as a 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test let model beforeAll(async () => { @@ -137,7 +137,7 @@ describe('virtual fields', () => { }`, model, ) - expect(query).to.deep.equal(CQL`SELECT from bookshop.Foo as Foo + expect(query).to.deep.equal(CQL`SELECT from bookshop.Foo as Foo left join bookshop.Foo as toFoo on toFoo.ID = Foo.toFoo_ID { Foo.ID, @@ -168,7 +168,7 @@ describe('paths with @cds.persistence.skip', () => { it('ignores column if assoc in path expression has target ”@cds.persistence.skip” in order by / group by', () => { const q = CQL`SELECT from bookshop.NotSkipped { ID - } group by skipped.notSkipped.text + } group by skipped.notSkipped.text order by skipped.notSkipped.text` const qx = CQL`SELECT from bookshop.NotSkipped as NotSkipped { diff --git a/db-service/test/cqn4sql/not-supported.test.js b/db-service/test/cqn4sql/not-supported.test.js index 468595c0c..af3a9e7b5 100644 --- a/db-service/test/cqn4sql/not-supported.test.js +++ b/db-service/test/cqn4sql/not-supported.test.js @@ -1,7 +1,7 @@ // here we can collect features which are not (yet) supported 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const _inferred = require('../../lib/infer') diff --git a/db-service/test/cqn4sql/path-in-from.test.js b/db-service/test/cqn4sql/path-in-from.test.js index 14cffb0c5..bae42f3ef 100644 --- a/db-service/test/cqn4sql/path-in-from.test.js +++ b/db-service/test/cqn4sql/path-in-from.test.js @@ -1,6 +1,6 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('infix filter on entities', () => { let model diff --git a/db-service/test/cqn4sql/pseudo-variable-replacement.test.js b/db-service/test/cqn4sql/pseudo-variable-replacement.test.js index c1d3ce1b2..6ad198e6f 100644 --- a/db-service/test/cqn4sql/pseudo-variable-replacement.test.js +++ b/db-service/test/cqn4sql/pseudo-variable-replacement.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Pseudo Variables', () => { diff --git a/db-service/test/cqn4sql/replacements.test.js b/db-service/test/cqn4sql/replacements.test.js index 6195a77a5..220f310d4 100644 --- a/db-service/test/cqn4sql/replacements.test.js +++ b/db-service/test/cqn4sql/replacements.test.js @@ -4,7 +4,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('in where', () => { let model diff --git a/db-service/test/cqn4sql/search.test.js b/db-service/test/cqn4sql/search.test.js index 4584284a6..b20763706 100644 --- a/db-service/test/cqn4sql/search.test.js +++ b/db-service/test/cqn4sql/search.test.js @@ -1,6 +1,6 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Replace attribute search by search predicate', () => { diff --git a/db-service/test/cqn4sql/structure-access.test.js b/db-service/test/cqn4sql/structure-access.test.js index aaed53237..287390a7a 100644 --- a/db-service/test/cqn4sql/structure-access.test.js +++ b/db-service/test/cqn4sql/structure-access.test.js @@ -1,7 +1,7 @@ // access of structured elements with dot notation 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test // "... to flat fields" is not entirely true, as we also have tests with paths ending on a structure // -> move them to separate section? diff --git a/db-service/test/cqn4sql/table-alias.test.js b/db-service/test/cqn4sql/table-alias.test.js index 29df94fce..bed39dd34 100644 --- a/db-service/test/cqn4sql/table-alias.test.js +++ b/db-service/test/cqn4sql/table-alias.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('table alias access', () => { let model @@ -285,9 +285,9 @@ describe('table alias access', () => { { ![FROM].title as group, } - where ![FROM].title = 'foo' + where ![FROM].title = 'foo' group by ![FROM].title - having ![FROM].title = 'foo' + having ![FROM].title = 'foo' order by ![FROM].title `) }) @@ -504,9 +504,9 @@ describe('table alias access', () => { const expected = CQL` SELECT from - (SELECT - SimpleBook.ID, - SimpleBook.title, + (SELECT + SimpleBook.ID, + SimpleBook.title, SimpleBook.author_ID from bookshop.SimpleBook as SimpleBook order by SimpleBook.title @@ -522,7 +522,7 @@ describe('table alias access', () => { expect(JSON.parse(JSON.stringify(res))).to.deep.equal(expected) }) it('same as above but descriptors like "asc", "desc" etc. must be kept', () => { - const query = CQL`SELECT from bookshop.Books { + const query = CQL`SELECT from bookshop.Books { title, title as foo, author.name as author @@ -820,7 +820,7 @@ describe('table alias access', () => { expect(JSON.parse(JSON.stringify(query))).to.deep.equal(CQL`SELECT from bookshop.Books as Books { Books.ID, (SELECT from bookshop.Books as Books2 { Books2.author_ID, - (SELECT from bookshop.Books as Books3 { + (SELECT from bookshop.Books as Books3 { (SELECT from bookshop.Authors as author { (SELECT from bookshop.Books as books4 { books4.ID @@ -919,7 +919,7 @@ describe('table alias access', () => { SELECT from bookshop.Books as Books { sum(Books.stock) as totalStock, Books.ID, - Books.stock, + Books.stock, Books.dedication_addressee_ID, Books.dedication_text, Books.dedication_sub_foo, diff --git a/db-service/test/cqn4sql/tupleExpansion.test.js b/db-service/test/cqn4sql/tupleExpansion.test.js index 3b2a0df36..02e0c6a8c 100644 --- a/db-service/test/cqn4sql/tupleExpansion.test.js +++ b/db-service/test/cqn4sql/tupleExpansion.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test // TODO test for unsupported comparison ops describe('Structural comparison', () => { diff --git a/db-service/test/cqn4sql/where-exists.test.js b/db-service/test/cqn4sql/where-exists.test.js index 61e430714..771ec711d 100644 --- a/db-service/test/cqn4sql/where-exists.test.js +++ b/db-service/test/cqn4sql/where-exists.test.js @@ -1,6 +1,6 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test const transitive_ = !cds.unfold || 'transitive_localized_views' in cds.env.sql && cds.env.sql.transitive_localized_views !== false diff --git a/db-service/test/cqn4sql/wildcards.test.js b/db-service/test/cqn4sql/wildcards.test.js index b198ae794..5b80f4778 100644 --- a/db-service/test/cqn4sql/wildcards.test.js +++ b/db-service/test/cqn4sql/wildcards.test.js @@ -1,7 +1,7 @@ 'use strict' const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test // TODO: UCSN -> order is different compared to odata model diff --git a/db-service/test/cqn4sql/with-parameters.test.js b/db-service/test/cqn4sql/with-parameters.test.js index b79da548f..9da846b27 100644 --- a/db-service/test/cqn4sql/with-parameters.test.js +++ b/db-service/test/cqn4sql/with-parameters.test.js @@ -6,7 +6,7 @@ const { SELECT } = require('@sap/cds/lib/ql/cds-ql') const cqn4sql = require('../../lib/cqn4sql') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('entities and views with parameters', () => { let model diff --git a/db-service/test/etc/cds.clone.test.js b/db-service/test/etc/cds.clone.test.js index 2c1933592..6507ba316 100644 --- a/db-service/test/etc/cds.clone.test.js +++ b/db-service/test/etc/cds.clone.test.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test describe('Cloning queries', () => { diff --git a/hana/CHANGELOG.md b/hana/CHANGELOG.md index fc9364dce..d887fc225 100644 --- a/hana/CHANGELOG.md +++ b/hana/CHANGELOG.md @@ -4,6 +4,47 @@ - The format is based on [Keep a Changelog](http://keepachangelog.com/). - This project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.0](https://github.com/cap-js/cds-dbs/compare/hana-v0.3.0...hana-v0.4.0) (2024-05-16) + + +### Added + +* Allow hex engine to be used ([#641](https://github.com/cap-js/cds-dbs/issues/641)) ([bca0c01](https://github.com/cap-js/cds-dbs/commit/bca0c012f8dfe0fcf526db2a6197eb86d7d4c8cc)) + + +### Fixed + +* Improve comparator check for combined and nested expressions ([#632](https://github.com/cap-js/cds-dbs/issues/632)) ([8e1cb4b](https://github.com/cap-js/cds-dbs/commit/8e1cb4b030ac84ffc9b13b52d6dac7850f300c9a)) +* Support multi byte characters ([#639](https://github.com/cap-js/cds-dbs/issues/639)) ([4cfa77f](https://github.com/cap-js/cds-dbs/commit/4cfa77f437c50afffec39e45ff795c732dfbe10a)) + + +### Changed + +* `@sap/hana-client` optional peer dependency ([#631](https://github.com/cap-js/cds-dbs/issues/631)) ([89d7149](https://github.com/cap-js/cds-dbs/commit/89d7149b5c6dc86315e8a0d767d0e95c12dcc55f)) + +## [0.3.0](https://github.com/cap-js/cds-dbs/compare/hana-v0.2.0...hana-v0.3.0) (2024-05-08) + + +### Added + +* select decimals as strings if cds.env.features.string_decimals ([#616](https://github.com/cap-js/cds-dbs/issues/616)) ([39addbf](https://github.com/cap-js/cds-dbs/commit/39addbfe01da757d86a4d65e62eda86e59fc9b87)) + + +### Fixed + +* Add multi `concat` function ([#624](https://github.com/cap-js/cds-dbs/issues/624)) ([df436fe](https://github.com/cap-js/cds-dbs/commit/df436fec3e137dee81f4a5ed69e551fc7c92700e)) +* Align all quote functions with @sap/cds-compiler ([#619](https://github.com/cap-js/cds-dbs/issues/619)) ([42e9828](https://github.com/cap-js/cds-dbs/commit/42e9828baf11ec55281ea634ce56ce93e6741b91)) +* Change `sql` property to `query` for errors ([#611](https://github.com/cap-js/cds-dbs/issues/611)) ([585577a](https://github.com/cap-js/cds-dbs/commit/585577a9817e7749fb71958c26c4bfa20981c663)) +* Disconnect HANA tenant when deleted ([#589](https://github.com/cap-js/cds-dbs/issues/589)) ([a107db9](https://github.com/cap-js/cds-dbs/commit/a107db9dc0ce610ba07a4562e94cfd22a9f8c182)) +* Align "not found" behavior ([#603](https://github.com/cap-js/cds-dbs/issues/603)) ([54d2efb](https://github.com/cap-js/cds-dbs/commit/54d2efb00cfa4b5f188dc01bd350f3ccaca8986b)) +* Allow custom fuzzy search cqn ([#620](https://github.com/cap-js/cds-dbs/issues/620)) ([80383f0](https://github.com/cap-js/cds-dbs/commit/80383f0e5aa3a81592e804c02ce6253bd4e7d16e)) +* Allow HANA to use != and == inside xpr combinations ([#607](https://github.com/cap-js/cds-dbs/issues/607)) ([c578e9f](https://github.com/cap-js/cds-dbs/commit/c578e9fd530ddd0de6e693b2bfe777935e935772)) +* Reference column alias in order by ([#615](https://github.com/cap-js/cds-dbs/issues/615)) ([7cd3a26](https://github.com/cap-js/cds-dbs/commit/7cd3a26943e9babdee385916d33e6ae16f48bd5d)) +* Remove encoding from hana-client streams ([#623](https://github.com/cap-js/cds-dbs/issues/623)) ([fed8f6f](https://github.com/cap-js/cds-dbs/commit/fed8f6f36c5d97b664852a79050ce0a5e35a5c6d)) +* Support associations with static values ([#604](https://github.com/cap-js/cds-dbs/issues/604)) ([05babcf](https://github.com/cap-js/cds-dbs/commit/05babcf7581b651b74b3f5eb1ebcb45dea706b06)) +* improved `=` and `!=` with val `null` ([#626](https://github.com/cap-js/cds-dbs/issues/626)) ([cbcfe3b](https://github.com/cap-js/cds-dbs/commit/cbcfe3b15e8ebcf7e844dc5406e4bc228d4c94c9)) +* Improved placeholders and limit clause ([#567](https://github.com/cap-js/cds-dbs/issues/567)) ([d5d5dbb](https://github.com/cap-js/cds-dbs/commit/d5d5dbb7219bcef6134440715cf756fdd439f076)) + ## [0.2.0](https://github.com/cap-js/cds-dbs/compare/hana-v0.1.0...hana-v0.2.0) (2024-04-12) diff --git a/hana/cds-plugin.js b/hana/cds-plugin.js index 0a3a92ebf..07c2b4bf8 100644 --- a/hana/cds-plugin.js +++ b/hana/cds-plugin.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') if (!cds.env.fiori.lean_draft) { throw new Error('"@cap-js/hana" only works if cds.fiori.lean_draft is enabled. Please adapt your configuration.') diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 0400b43ed..c09f65978 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -30,6 +30,7 @@ class HANAService extends SQLService { this.on(['BEGIN'], this.onBEGIN) this.on(['COMMIT'], this.onCOMMIT) this.on(['ROLLBACK'], this.onROLLBACK) + this.on(['SELECT', 'INSERT', 'UPSERT', 'UPDATE', 'DELETE'], this.onNOTFOUND) return super.init() } @@ -181,6 +182,19 @@ class HANAService extends SQLService { } } + async onNOTFOUND(req, next) { + try { + return await next() + } catch (err) { + // Ensure that the known entity still exists + if (!this.context.tenant && err.code === 259 && typeof req.query !== 'string') { + // Clear current tenant connection pool + this.disconnect(this.context.tenant) + } + throw err + } + } + // Allow for running complex expand queries in a single statement wrapTemporary(temporary, withclauses, blobs) { const blobColumn = b => `"${b.replace(/"/g, '""')}"` @@ -192,7 +206,7 @@ class HANAService extends SQLService { }) const withclause = withclauses.length ? `WITH ${withclauses} ` : '' - const ret = withclause + (values.length === 1 ? values[0] : 'SELECT * FROM ' + values.map(v => `(${v})`).join(' UNION ALL ') + ' ORDER BY "_path_" ASC') + const ret = withclause + (values.length === 1 ? values[0] : 'SELECT * FROM ' + values.map(v => `(${v})`).join(' UNION ALL ')) + ' ORDER BY "_path_" ASC' DEBUG?.(ret) return ret } @@ -306,7 +320,7 @@ class HANAService extends SQLService { throw new Error('CQN query using joins must specify the selected columns.') } - const { limit, one, orderBy, expand, columns = ['*'], localized, count, parent } = q.SELECT + let { limit, one, orderBy, expand, columns = ['*'], localized, count, parent } = q.SELECT const walkAlias = q => { if (q.args) return q.as || walkAlias(q.args[0]) @@ -339,15 +353,22 @@ class HANAService extends SQLService { if (orderBy) { // Ensure that all columns used in the orderBy clause are exposed - orderBy.forEach(c => { + orderBy = orderBy.map((c, i) => { + if (!c.ref) { + c.as = `$$ORDERBY_${i}$$` + columns.push(c) + return { __proto__: c, ref: [c.as], sort: c.sort } + } if (c.ref?.length === 2) { const ref = c.ref + '' - if (!columns.find(c => c.ref + '' === ref)) { - const clone = { __proto__: c, ref: c.ref } - columns.push(clone) + const match = columns.find(col => col.ref + '' === ref) + if (!match) { + c.as = `$$${c.ref.join('.')}$$` + columns.push(c) } - c.ref = [c.ref[1]] + return { __proto__: c, ref: [this.column_name(match || c)], sort: c.sort } } + return c }) } @@ -453,6 +474,7 @@ class HANAService extends SQLService { let fkeys = x.element._foreignKeys if (typeof fkeys === 'function') fkeys = fkeys.call(x.element) fkeys.forEach(k => { + if (!k?.parentElement?.name) return // not all associations have foreign key references if (!parent.SELECT.columns.find(c => this.column_name(c) === k.parentElement.name)) { parent.SELECT.columns.push({ ref: [parent.as, k.parentElement.name] }) } @@ -460,7 +482,7 @@ class HANAService extends SQLService { x.SELECT.from = { join: 'inner', - args: [{ ref: [parent.alias], as: parent.as }, x.SELECT.from], + args: [x.SELECT.from, { ref: [parent.alias], as: parent.as }], on: x.SELECT.where, as: x.SELECT.from.as, } @@ -513,8 +535,8 @@ class HANAService extends SQLService { // Making each row a maximum size of 2gb instead of the whole result set to be 2gb // Excluding binary columns as they are not supported by FOR JSON and themselves can be 2gb const rawJsonColumn = sql.length - ? `(SELECT ${sql} FROM DUMMY FOR JSON ('format'='no', 'omitnull'='no', 'arraywrap'='no') RETURNS NVARCHAR(2147483647)) AS "_json_"` - : `TO_NCLOB('{}') AS "_json_"` + ? `(SELECT ${sql} FROM JSON_TABLE('[{}]', '$' COLUMNS(I FOR ORDINALITY)) FOR JSON ('format'='no', 'omitnull'='no', 'arraywrap'='no') RETURNS NVARCHAR(2147483647)) AS "_json_"` + : `'{}' AS "_json_"` let jsonColumn = rawJsonColumn if (structures.length) { @@ -707,10 +729,15 @@ class HANAService extends SQLService { ) } + limit({ rows, offset }) { + rows = { param: false, __proto__: rows } + return super.limit({ rows, offset }) + } + where(xpr) { xpr = { xpr } - const suffix = this.is_comparator(xpr) ? '' : ' = TRUE' - return `${this.xpr(xpr)}${suffix}` + const suffix = this.is_comparator(xpr) + return `${this.xpr(xpr)}${suffix ? '' : ` = ${this.val({ val: true })}`}` } having(xpr) { @@ -723,9 +750,9 @@ class HANAService extends SQLService { const compareOperators = { '==': true, '!=': false, + // These operators are not allowed in column expressions /* REVISIT: Only adjust these operators when inside the column expression - '=': null, '>': null, '<': null, '<>': null, @@ -741,9 +768,31 @@ class HANAService extends SQLService { for (let i = 0; i < xpr.length; i++) { let x = xpr[i] if (typeof x === 'string') { - // Convert =, == and != into is (not) null operator where required - x = xpr[i] = super.operator(xpr[i], i, xpr) + // IS (NOT) NULL translation when required + if (x === '=' || x === '!=') { + const left = xpr[i - 1] + const right = xpr[i + 1] + const leftType = left?.element?.type + const rightType = right?.element?.type + // Prevent HANA from throwing and unify nonsense behavior + if (left?.val === null && rightType in lobTypes) { + left.param = false // Force null to be inlined + xpr[i + 1] = { param: false, val: null } // Remove illegal type ref for compare operator + } + if (right?.val === null) { + if ( + !leftType || // Literal translation when left hand type is unknown + leftType in lobTypes + ) { + xpr[i] = x = x === '=' ? 'IS' : 'IS NOT' + right.param = false // Force null to be inlined + } else { + x = x === '=' ? '==' : '!=' + } + } + } + // const effective = x === '=' && xpr[i + 1]?.val === null ? '==' : x // HANA does not support comparators in all clauses (e.g. SELECT 1>0 FROM DUMMY) // HANA does not have an 'IS' or 'IS NOT' operator if (x in compareOperators) { @@ -751,6 +800,7 @@ class HANAService extends SQLService { const left = xpr[i - 1] const right = xpr[i + 1] const ifNull = compareOperators[x] + x = x === '==' ? '=' : x const compare = [left, x, right] @@ -766,7 +816,8 @@ class HANAService extends SQLService { xpr: [ 'CASE', 'WHEN', - ...[left, 'IS', 'NULL', 'AND', right, 'IS', 'NULL'], + // coalesce is used to match the left and right hand types in case one is a placeholder + ...[{ func: 'COALESCE', args: [left, right] }, 'IS', 'NULL'], 'THEN', { val: ifNull }, 'ELSE', @@ -780,7 +831,7 @@ class HANAService extends SQLService { xpr[i - 1] = '' xpr[i] = expression - xpr[i + 1] = '' + xpr[i + 1] = ' = TRUE' } } } @@ -821,7 +872,7 @@ class HANAService extends SQLService { up in logicOperators && !this.is_comparator({ xpr }, i - 1) ) { - return ` = TRUE ${x}` + return ` = ${this.val({ val: true })} ${x}` } if ( (up === 'LIKE' && is_regexp(xpr[i + 1]?.val)) || @@ -854,7 +905,12 @@ class HANAService extends SQLService { if (up in caseOperators) break continue } - if ('xpr' in cur) return this.is_comparator(cur) + if (cur.func?.toUpperCase() === 'CONTAINS' && cur.args?.length > 2) return true + if ('_internal' in cur) return true + if ('xpr' in cur) { + const nested = this.is_comparator(cur) + if (nested) return true + } } return false } @@ -877,7 +933,7 @@ class HANAService extends SQLService { // cds-compiler effectiveName uses toUpperCase for hana dialect, but not for hdbcds if (typeof s !== 'string') return '"' + s + '"' if (s.includes('"')) return '"' + s.replace(/"/g, '""').toUpperCase() + '"' - if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' @./\\]/.test(s)) return '"' + s.toUpperCase() + '"' + if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$#0-9]*$/.test(s)) return '"' + s.toUpperCase() + '"' return s } @@ -970,9 +1026,12 @@ class HANAService extends SQLService { // TypeMap used for the JSON_TABLE column definition static InsertTypeMap = { ...super.TypeMap, + UInt8: () => 'INT', Int16: () => 'INT', + Int64: () => `BIGINT`, UUID: () => `NVARCHAR(36)`, Boolean: () => `NVARCHAR(5)`, + String: e => `NVARCHAR(${(e.length || 5000) * 4})`, LargeString: () => `NVARCHAR(2147483647)`, LargeBinary: () => `NVARCHAR(2147483647)`, Binary: () => `NVARCHAR(2147483647)`, @@ -981,7 +1040,14 @@ class HANAService extends SQLService { // JavaScript types string: () => `NVARCHAR(2147483647)`, - number: () => `DOUBLE` + number: () => `DOUBLE`, + + // HANA types + 'cds.hana.TINYINT': () => 'INT', + 'cds.hana.REAL': () => 'DECIMAL', + 'cds.hana.CHAR': e => `NVARCHAR(${(e.length || 1) * 4})`, + 'cds.hana.ST_POINT': () => 'NVARCHAR(2147483647)', + 'cds.hana.ST_GEOMETRY': () => 'NVARCHAR(2147483647)', } // HANA JSON_TABLE function does not support BOOLEAN types @@ -994,6 +1060,10 @@ class HANAService extends SQLService { Boolean: e => `CASE WHEN ${e} = 'true' THEN TRUE WHEN ${e} = 'false' THEN FALSE END`, Vector: e => `TO_REAL_VECTOR(${e})`, // TODO: Decimal: (expr, element) => element.precision ? `TO_DECIMAL(${expr},${element.precision},${element.scale})` : expr + + // HANA types + 'cds.hana.ST_POINT': e => `CASE WHEN ${e} IS NOT NULL THEN NEW ST_POINT(TO_DOUBLE(JSON_VALUE(${e}, '$.x')), TO_DOUBLE(JSON_VALUE(${e}, '$.y'))) END`, + 'cds.hana.ST_GEOMETRY': e => `TO_GEOMETRY(${e})`, } static OutputConverters = { @@ -1007,8 +1077,13 @@ class HANAService extends SQLService { Vector: e => `TO_NVARCHAR(${e})`, // Reading int64 as string to not loose precision Int64: expr => `TO_NVARCHAR(${expr})`, + // REVISIT: always cast to string in next major // Reading decimal as string to not loose precision - Decimal: expr => `TO_NVARCHAR(${expr})`, + Decimal: cds.env.features.string_decimals ? expr => `TO_NVARCHAR(${expr})` : undefined, + + // HANA types + 'cds.hana.ST_POINT': e => `(SELECT NEW ST_POINT(TO_NVARCHAR(${e})).ST_X() as "x", NEW ST_POINT(TO_NVARCHAR(${e})).ST_Y() as "y" FROM DUMMY WHERE (${e}) IS NOT NULL FOR JSON ('format'='no', 'omitnull'='no', 'arraywrap'='no') RETURNS NVARCHAR(2147483647))`, + 'cds.hana.ST_GEOMETRY': e => `TO_NVARCHAR(${e})`, } } @@ -1043,10 +1118,10 @@ class HANAService extends SQLService { return super.dispatch(req) } - async onCall({ query, data }, name, schema) { - const outParameters = await this._getProcedureMetadata(name, schema) - const ps = await this.prepare(query) - return ps.proc(data, outParameters) + async onCall({ query, data }, name, schema) { + const outParameters = await this._getProcedureMetadata(name, schema) + const ps = await this.prepare(query) + return ps.proc(data, outParameters) } async onPlainSQL(req, next) { @@ -1062,7 +1137,7 @@ class HANAService extends SQLService { throw err } } - + const proc = this._getProcedureNameAndSchema(req.query) if (proc && proc.name) return this.onCall(req, proc.name, proc.schema) @@ -1169,15 +1244,14 @@ class HANAService extends SQLService { } async _getProcedureMetadata(name, schema) { - const query = `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = ${ - schema?.toUpperCase?.() === 'SYS' ? `'SYS'` : 'CURRENT_SCHEMA' + const query = `SELECT PARAMETER_NAME FROM SYS.PROCEDURE_PARAMETERS WHERE SCHEMA_NAME = ${schema?.toUpperCase?.() === 'SYS' ? `'SYS'` : 'CURRENT_SCHEMA' } AND PROCEDURE_NAME = '${name}' AND PARAMETER_TYPE IN ('OUT', 'INOUT') ORDER BY POSITION` - return await super.onPlainSQL({ query, data: [] }) + return await super.onPlainSQL({ query, data: [] }) } _getProcedureNameAndSchema(sql) { // name delimited with "" allows any character - const match = sql + const match = sql .match( /^\s*call \s*(("(?\w+)"\.)?("(?.+)")|(?\w+\.)?(?\w+))\s*\(/i ) @@ -1242,6 +1316,14 @@ const compareOperators = { 'IS NOT': 1, 'EXISTS': 1, 'BETWEEN': 1, + 'CONTAINS': 1, + 'MEMBER OF': 1, + 'LIKE_REGEXPR': 1, +} +const lobTypes = { + 'cds.LargeBinary': 1, + 'cds.LargeString': 1, + 'cds.hana.CLOB': 1, } module.exports = HANAService diff --git a/hana/lib/cql-functions.js b/hana/lib/cql-functions.js index b142f21ff..9ca001b0b 100644 --- a/hana/lib/cql-functions.js +++ b/hana/lib/cql-functions.js @@ -1,7 +1,10 @@ const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/ +const isDate = /^\d{1,4}-\d{1,2}-\d{1,2}$/ const isVal = x => x && 'val' in x const getTimeType = x => isTime.test(x.val) ? 'TIME' : 'TIMESTAMP' const getTimeCast = x => isVal(x) ? `TO_${getTimeType(x)}(${x})` : x +const getDateType = x => isDate.test(x.val) ? 'DATE' : 'TIMESTAMP' +const getDateCast = x => isVal(x) ? `TO_${getDateType(x)}(${x})` : x const StandardFunctions = { tolower: x => `lower(${x})`, @@ -18,7 +21,8 @@ const StandardFunctions = { count: x => `count(${x || '*'})`, countdistinct: x => `count(distinct ${x || '*'})`, average: x => `avg(${x})`, - contains: (...args) => `(CASE WHEN coalesce(locate(${args}),0)>0 THEN TRUE ELSE FALSE END)`, + contains: (...args) => args.length > 2 ? `CONTAINS(${args})` : `(CASE WHEN coalesce(locate(${args}),0)>0 THEN TRUE ELSE FALSE END)`, + concat: (...args) => `(${args.map(a => (a.xpr ? `(${a})` : a)).join(' || ')})`, search: function (ref, arg) { if (!('val' in arg)) throw `HANA only supports single value arguments for $search` const refs = ref.list || [ref], @@ -31,7 +35,9 @@ const StandardFunctions = { }, // Date and Time Functions - day: x => `DAYOFMONTH(${x})`, + year: x => `YEAR(${getDateCast(x)})`, + month: x => `MONTH(${getDateCast(x)})`, + day: x => `DAYOFMONTH(${getDateCast(x)})`, hour: x => `HOUR(${getTimeCast(x)})`, minute: x => `MINUTE(${getTimeCast(x)})`, second: x => `TO_INTEGER(SECOND(${getTimeCast(x)}))`, diff --git a/hana/lib/drivers/base.js b/hana/lib/drivers/base.js index d41e49fd9..1c198f90a 100644 --- a/hana/lib/drivers/base.js +++ b/hana/lib/drivers/base.js @@ -203,9 +203,9 @@ const formatPrivilegeError = function (row) { return `MISSING ${GRANT}${row.PRIVILEGE} ON ${row.SCHEMA_NAME}.${row.OBJECT_NAME}` } -const enhanceError = function (err, stack, sql, message) { +const enhanceError = function (err, stack, query, message) { return Object.assign(err, stack, { - sql, + query, message: message ? message : err.message, }) } diff --git a/hana/lib/drivers/hana-client.js b/hana/lib/drivers/hana-client.js index f63ee7fee..72b49c505 100644 --- a/hana/lib/drivers/hana-client.js +++ b/hana/lib/drivers/hana-client.js @@ -1,7 +1,6 @@ const { Readable, Stream } = require('stream') const hdb = require('@sap/hana-client') -const { StringDecoder } = require('string_decoder') const { driver, prom, handleLevel } = require('./base') const streamUnsafe = false @@ -97,7 +96,7 @@ class HANAClientDriver extends driver { row[col] = i > 3 ? rs.isNull(i) ? null - : Readable.from(streamBlob(rsStreams, rs._rowPosition, i, 'binary')) + : Readable.from(streamBlob(rsStreams, rs._rowPosition, i), { objectMode: false }) : values[i] } @@ -153,7 +152,7 @@ class HANAClientDriver extends driver { if (rs.getRowCount() === 0) return null await prom(rs, 'next')() if (rs.isNull(0)) return null - return Readable.from(streamBlob(rs, undefined, 0, 'binary'), { objectMode: false }) + return Readable.from(streamBlob(rs, undefined, 0), { objectMode: false }) } return Readable.from(rsIterator(rs, one), { objectMode: false }) } @@ -217,7 +216,8 @@ class HANAClientDriver extends driver { if (!curStream) continue for await (const chunk of curStream) { curStream.pause() - await sendParameterData(i, Buffer.from(chunk)) + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + if (buffer.length) await sendParameterData(i, buffer) curStream.resume() } await sendParameterData(i, null) @@ -293,7 +293,9 @@ async function* rsIterator(rs, one) { yield buffer buffer = '' - for await (const chunk of streamBlob(rs, undefined, columnIndex, 'base64', binaryBuffer)) { + const stream = Readable.from(streamBlob(rs, undefined, columnIndex, binaryBuffer), { objectMode: false }) + stream.setEncoding('base64') + for await (const chunk of stream) { yield chunk } buffer += '"' @@ -316,7 +318,7 @@ async function* rsIterator(rs, one) { yield buffer } -async function* streamBlob(rs, rowIndex = -1, columnIndex, encoding, binaryBuffer = Buffer.allocUnsafe(1 << 16)) { +async function* streamBlob(rs, rowIndex = -1, columnIndex, binaryBuffer = Buffer.allocUnsafe(1 << 16)) { const promChain = { resolve: () => { }, reject: () => { } @@ -357,29 +359,18 @@ async function* streamBlob(rs, rowIndex = -1, columnIndex, encoding, binaryBuffe const getData = prom(rs, 'getData') - let decoder = new StringDecoder(encoding) - let blobPosition = 0 while (true) { // REVISIT: Ensure that the data read is divisible by 3 as that allows for base64 encoding - let start = 0 const read = await getData(columnIndex, blobPosition, binaryBuffer, 0, binaryBuffer.byteLength) - if (blobPosition === 0 && binaryBuffer.slice(0, 7).toString() === 'base64,') { - decoder = { - write: encoding === 'base64' ? c => c : chunk => Buffer.from(chunk.toString(), 'base64'), - end: () => Buffer.allocUnsafe(0), - } - start = 7 - } blobPosition += read if (read < binaryBuffer.byteLength) { - yield decoder.write(binaryBuffer.slice(start, read)) + yield binaryBuffer.slice(0, read) break } - yield decoder.write(binaryBuffer.slice(start).toString('base64')) + yield binaryBuffer } - yield decoder.end() } catch (e) { promChain.reject(e) } finally { diff --git a/hana/package.json b/hana/package.json index 9202099e4..9a886f806 100644 --- a/hana/package.json +++ b/hana/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/hana", - "version": "0.2.0", + "version": "0.4.0", "description": "CDS database service for SAP HANA", "homepage": "https://cap.cloud.sap/", "keywords": [ @@ -28,12 +28,17 @@ }, "dependencies": { "hdb": "^0.19.5", - "@cap-js/db-service": "^1.7.0" + "@cap-js/db-service": "^1.9.0" }, "peerDependencies": { "@sap/hana-client": ">=2", "@sap/cds": ">=7.6" }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + } + }, "cds": { "requires": { "kinds": { @@ -50,4 +55,4 @@ } }, "license": "SEE LICENSE" -} \ No newline at end of file +} diff --git a/hana/test/fuzzy.cds b/hana/test/fuzzy.cds new file mode 100644 index 000000000..977c4458a --- /dev/null +++ b/hana/test/fuzzy.cds @@ -0,0 +1 @@ +using {sap.capire.bookshop.Books as Books} from '../../test/bookshop/db/schema.cds'; diff --git a/hana/test/fuzzy.test.js b/hana/test/fuzzy.test.js new file mode 100644 index 000000000..857717c8f --- /dev/null +++ b/hana/test/fuzzy.test.js @@ -0,0 +1,19 @@ +const cds = require('../../test/cds') + +describe('Fuzzy search', () => { + const { expect } = cds.test(__dirname, 'fuzzy.cds') + + test('select', async () => { + const { Books } = cds.entities('sap.capire.bookshop') + const res = await SELECT.from(Books).where({ + func: 'contains', + args: [ + { list: [{ ref: ['title'] }, { ref: ['descr'] }] }, + { val: 'poem' }, + { func: 'FUZZY', args: [{ val: 0.8 }, { val: 'similarCalculationMode=searchCompare' }] } + ] + }) + + expect(res).to.have.property('length').to.be.eq(1) + }) +}) \ No newline at end of file diff --git a/hana/test/param-views.test.js b/hana/test/param-views.test.js index e7e73b3e0..ac613af2b 100644 --- a/hana/test/param-views.test.js +++ b/hana/test/param-views.test.js @@ -19,8 +19,9 @@ describe('Parameterized view', () => { }, // ===== just works queries ===== { + // cast is required as the SQL becomes (? * ?) // all books with <= 22 stock - available: CXL`11 * 2`, + available: CXL`cast(11 * 2 as cds.Integer)`, books: 3, }, { // the book with the least stock diff --git a/package-lock.json b/package-lock.json index a7f448d70..cf84fe0ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,22 +31,22 @@ }, "db-service": { "name": "@cap-js/db-service", - "version": "1.8.0", + "version": "1.9.1", "license": "SEE LICENSE", "engines": { "node": ">=16", "npm": ">=8" }, "peerDependencies": { - "@sap/cds": ">=7.6" + "@sap/cds": ">=7.9" } }, "hana": { "name": "@cap-js/hana", - "version": "0.2.0", + "version": "0.4.0", "license": "SEE LICENSE", "dependencies": { - "@cap-js/db-service": "^1.7.0", + "@cap-js/db-service": "^1.9.0", "hdb": "^0.19.5" }, "engines": { @@ -56,6 +56,11 @@ "peerDependencies": { "@sap/cds": ">=7.6", "@sap/hana-client": ">=2" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + } } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1238,9 +1243,9 @@ } }, "node_modules/@sap/cds": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-7.8.0.tgz", - "integrity": "sha512-eprPzuSZsLnA4O9aU2Xgt1s7g0906fr2rZZQb76vRDwY1j/IXvRV4ijTB4nmy2RCQRMHXVQ2P5RuFCS/P/5cMw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-7.9.0.tgz", + "integrity": "sha512-vjCmTVvaVKGxZoMWWnb0sEZD8JJtANX3lFWTbMppKGpSXeqRDmL4ORdVyAVSroAtIsVcBGvkMqe2XfGtuYos5g==", "dependencies": { "@cap-js/cds-types": "<1", "@sap/cds-compiler": "^4", @@ -1256,9 +1261,9 @@ } }, "node_modules/@sap/cds-compiler": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-4.8.0.tgz", - "integrity": "sha512-C8IkzNfdMIzG136K/VvOmG65d8UnR9lcZF5q/c//Jp2/RZIZvwFvO5HHPre19+YWhuwJXbIMb3kc5x9CW9ZLNw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-4.9.0.tgz", + "integrity": "sha512-eX1+mpL4z/UVNa5blIuqguWF3txIBOw7OCuVOnCQMStNhHXxbnTnDRZrh7+S4AH9kxT0DmJXMHR6JN44xzzprg==", "dependencies": { "antlr4": "4.9.3" }, @@ -1295,9 +1300,10 @@ } }, "node_modules/@sap/hana-client": { - "version": "2.20.20", - "resolved": "https://registry.npmjs.org/@sap/hana-client/-/hana-client-2.20.20.tgz", - "integrity": "sha512-LPZ0aozDr3NNoRVcqzs+VHwy5jRTRMdw5mSfxhGdpznMIVDT65uRxGudRqMK/YEikAZIR4Z+NRsKzSWECwoxTQ==", + "version": "2.20.22", + "resolved": "https://registry.npmjs.org/@sap/hana-client/-/hana-client-2.20.22.tgz", + "integrity": "sha512-m3vBrXPyzxAabAaLs1l9ymVEvmLUgdP27jyPvyhJzd+YbpnHrziEcFYAvG9uzlPctZkyXtomurB/IJhCFkkuUw==", + "devOptional": true, "hasInstallScript": true, "hasShrinkwrap": true, "dependencies": { @@ -1310,13 +1316,15 @@ "node_modules/@sap/hana-client/node_modules/debug": { "version": "3.1.0", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "devOptional": true, "dependencies": { "ms": "2.0.0" } }, "node_modules/@sap/hana-client/node_modules/ms": { "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "devOptional": true }, "node_modules/@sap/xssec": { "version": "3.6.1", @@ -1431,12 +1439,6 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -1446,12 +1448,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1474,21 +1470,20 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", - "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/type-utils": "7.7.1", - "@typescript-eslint/utils": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -1508,49 +1503,17 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", - "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4" }, "engines": { @@ -1570,13 +1533,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", - "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1" + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1587,13 +1551,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", - "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1614,10 +1579,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", - "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -1627,13 +1593,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", - "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1659,27 +1626,17 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1691,13 +1648,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1705,25 +1660,17 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", - "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1736,46 +1683,14 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", - "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/types": "7.11.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1929,6 +1844,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1958,10 +1874,11 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2101,9 +2018,9 @@ ] }, "node_modules/better-sqlite3": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.5.0.tgz", - "integrity": "sha512-01qVcM4gPNwE+PX7ARNiHINwzVuD6nx0gdldaAAcu+MrzyIAukQ31ZDKEpzRO/CNA9sHpxoTZ8rdjoyAin4dyg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-10.0.0.tgz", + "integrity": "sha512-rOz0JY8bt9oMgrFssP7GnvA5R3yln73y/NizzWqy3WlFth8Ux8+g4r/N9fjX97nn4X1YX6MTER2doNpTu5pqiA==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -2357,15 +2274,15 @@ } }, "node_modules/chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", + "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", "dev": true, "dependencies": { "check-error": "^1.0.2" }, "peerDependencies": { - "chai": ">= 2.1.2 < 5" + "chai": ">= 2.1.2 < 6" } }, "node_modules/chai-subset": { @@ -2732,6 +2649,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -3143,6 +3061,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3159,6 +3078,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3505,6 +3425,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4878,6 +4799,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -5265,6 +5187,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6593,10 +6516,10 @@ }, "postgres": { "name": "@cap-js/postgres", - "version": "1.7.0", + "version": "1.8.0", "license": "SEE LICENSE", "dependencies": { - "@cap-js/db-service": "^1.7.0", + "@cap-js/db-service": "^1.9.0", "pg": "^8" }, "engines": { @@ -6615,11 +6538,11 @@ }, "sqlite": { "name": "@cap-js/sqlite", - "version": "1.6.0", + "version": "1.7.1", "license": "SEE LICENSE", "dependencies": { - "@cap-js/db-service": "^1.7.0", - "better-sqlite3": "^9.3.0" + "@cap-js/db-service": "^1.9.0", + "better-sqlite3": "^10.0.0" }, "engines": { "node": ">=16", diff --git a/postgres/CHANGELOG.md b/postgres/CHANGELOG.md index c6311f894..3e2255d03 100644 --- a/postgres/CHANGELOG.md +++ b/postgres/CHANGELOG.md @@ -4,6 +4,21 @@ - The format is based on [Keep a Changelog](http://keepachangelog.com/). - This project adheres to [Semantic Versioning](http://semver.org/). +## [1.8.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.7.0...postgres-v1.8.0) (2024-05-08) + + +### Added + +* select decimals as strings if cds.env.features.string_decimals ([#616](https://github.com/cap-js/cds-dbs/issues/616)) ([39addbf](https://github.com/cap-js/cds-dbs/commit/39addbfe01da757d86a4d65e62eda86e59fc9b87)) + + +### Fixed + +* Align all quote functions with @sap/cds-compiler ([#619](https://github.com/cap-js/cds-dbs/issues/619)) ([42e9828](https://github.com/cap-js/cds-dbs/commit/42e9828baf11ec55281ea634ce56ce93e6741b91)) +* Change `sql` property to `query` for errors ([#611](https://github.com/cap-js/cds-dbs/issues/611)) ([585577a](https://github.com/cap-js/cds-dbs/commit/585577a9817e7749fb71958c26c4bfa20981c663)) +* Improved placeholders and limit clause ([#567](https://github.com/cap-js/cds-dbs/issues/567)) ([d5d5dbb](https://github.com/cap-js/cds-dbs/commit/d5d5dbb7219bcef6134440715cf756fdd439f076)) +* Use json datatype for `INSERT` ([#582](https://github.com/cap-js/cds-dbs/issues/582)) ([f1c9c89](https://github.com/cap-js/cds-dbs/commit/f1c9c89036a7f8e4709c67d713d06926630aa36d)) + ## [1.7.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.6.0...postgres-v1.7.0) (2024-04-12) diff --git a/postgres/lib/PostgresService.js b/postgres/lib/PostgresService.js index aae710f3b..2fd8cf49c 100644 --- a/postgres/lib/PostgresService.js +++ b/postgres/lib/PostgresService.js @@ -1,6 +1,6 @@ const { SQLService } = require('@cap-js/db-service') const { Client, Query } = require('pg') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const crypto = require('crypto') const { Writable, Readable } = require('stream') const sessionVariableMap = require('./session.json') @@ -144,18 +144,29 @@ GROUP BY k text: sql, name: sha, } + + const enhanceError = (err, sql) => Object.assign(err, { query: sql + '\n' + new Array(err.position).fill(' ').join('') + '^' }) + return { run: async values => { - // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78 - let newQuery = this._prepareStreams(query, values) - if (typeof newQuery.then === 'function') newQuery = await newQuery - const result = await this.dbc.query(newQuery) - return { changes: result.rowCount } + try { + // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78 + let newQuery = this._prepareStreams(query, values) + if (typeof newQuery.then === 'function') newQuery = await newQuery + const result = await this.dbc.query(newQuery) + return { changes: result.rowCount } + } catch (e) { + throw enhanceError(e, sql) + } }, get: async values => { - // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78 - const result = await this.dbc.query({ ...query, values: this._getValues(values) }) - return result.rows[0] + try { + // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78 + const result = await this.dbc.query({ ...query, values: this._getValues(values) }) + return result.rows[0] + } catch (e) { + throw enhanceError(e, sql) + } }, all: async values => { // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78 @@ -163,7 +174,7 @@ GROUP BY k const result = await this.dbc.query({ ...query, values: this._getValues(values) }) return result.rows } catch (e) { - throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' }) + throw enhanceError(e, sql) } }, stream: async (values, one) => { @@ -171,7 +182,7 @@ GROUP BY k const streamQuery = new QueryStream({ ...query, values: this._getValues(values) }, one) return await this.dbc.query(streamQuery) } catch (e) { - throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' }) + throw enhanceError(e, sql) } }, } @@ -206,8 +217,7 @@ GROUP BY k sql = sql.replace( new RegExp(`\\$${i + 1}`, 'g'), // Don't ask about the dollar signs - `(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${ - query.name + `(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${query.name }' AND ID=$${i + 1})`, ) return @@ -288,21 +298,17 @@ GROUP BY k if (query.SELECT?.columns?.find(col => col.as === '$mediaContentType')) { const columns = query.SELECT.columns const index = columns.findIndex(col => query.elements[col.ref?.[col.ref.length - 1]].type === 'cds.LargeBinary') - const binary = columns[index] + const binary = columns.splice(index, 1) // SELECT without binary column - columns.splice(index, 1) - const { sql, values } = this.cqn2sql(query, data) - let ps = this.prepare(sql) - let res = await ps.all(values) - if (res.length === 0) return - res = res.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))[0] + let res = await super.onSELECT({ query, data }) + if (!res) return res // SELECT only binary column - query.SELECT.columns = [binary] + query.SELECT.columns = binary const { sql: streamSql, values: valuesStream } = this.cqn2sql(query, data) - ps = this.prepare(streamSql) + const ps = this.prepare(streamSql) const stream = await ps.stream(valuesStream, true) // merge results - res[binary.as || binary.ref[binary.ref.length - 1]] = stream + res[this.class.CQN2SQL.prototype.column_name(binary[0])] = stream return res } return super.onSELECT({ query, data }) @@ -431,6 +437,14 @@ GROUP BY k return 'FOR SHARE' } + // Postgres requires own quote function, becuase the effective name is lower case + quote(s) { + if (typeof s !== 'string') return '"' + s + '"' + if (s.includes('"')) return '"' + s.replace(/"/g, '""').toLowerCase() + '"' + if (s in this.class.ReservedWords || !/^[A-Za-z_][A-Za-z_$0-9]*$/.test(s)) return '"' + s.toLowerCase() + '"' + return s + } + defaultValue(defaultValue = this.context.timestamp.toISOString()) { return this.string(`${defaultValue}`) } @@ -443,6 +457,7 @@ GROUP BY k ...super.TypeMap, // REVISIT: check whether we should use native UUID support UUID: () => `VARCHAR(36)`, + UInt8: () => `INT`, String: e => `VARCHAR(${e.length || 5000})`, Binary: () => `BYTEA`, Double: () => 'FLOAT8', @@ -452,6 +467,13 @@ GROUP BY k Time: () => 'TIME', DateTime: () => 'TIMESTAMP', Timestamp: () => 'TIMESTAMP', + + // HANA Types + 'cds.hana.CLOB': () => 'BYTEA', + 'cds.hana.BINARY': () => 'BYTEA', + 'cds.hana.TINYINT': () => 'SMALLINT', + 'cds.hana.ST_POINT': () => 'POINT', + 'cds.hana.ST_GEOMETRY': () => 'POLYGON', } // Used for INSERT statements @@ -472,6 +494,12 @@ GROUP BY k DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`, Binary: e => `DECODE(${e},'base64')`, LargeBinary: e => `DECODE(${e},'base64')`, + + // HANA Types + 'cds.hana.CLOB': e => `DECODE(${e},'base64')`, + 'cds.hana.BINARY': e => `DECODE(${e},'base64')`, + 'cds.hana.ST_POINT': e => `POINT(((${e})::json->>'x')::float, ((${e})::json->>'y')::float)`, + 'cds.hana.ST_GEOMETRY': e => `POLYGON(${e})`, } static OutputConverters = { @@ -489,8 +517,12 @@ GROUP BY k array: e => `jsonb(${e})`, // Reading int64 as string to not loose precision Int64: expr => `cast(${expr} as varchar)`, + // REVISIT: always cast to string in next major // Reading decimal as string to not loose precision - Decimal: expr => `cast(${expr} as varchar)`, + Decimal: cds.env.features.string_decimals ? expr => `cast(${expr} as varchar)` : undefined, + + // Convert point back to json format + 'cds.hana.ST_POINT': expr => `CASE WHEN (${expr}) IS NOT NULL THEN json_object('x':(${expr})[0],'y':(${expr})[1])::varchar END`, } } @@ -565,7 +597,7 @@ GROUP BY k // Create new schema using schema owner await this.tx(async tx => { await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`) - if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {}) + if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => { }) }) } finally { await this.disconnect() @@ -593,7 +625,7 @@ class QueryStream extends Query { }) this.connection.flush() } - : () => {}, + : () => { }, }) this.push = this.stream.push.bind(this.stream) @@ -708,7 +740,7 @@ class ParameterStream extends Writable { } // Used by the client to handle timeouts - callback() {} + callback() { } _write(chunk, enc, cb) { return this.flush(chunk, cb) diff --git a/postgres/lib/cql-functions.js b/postgres/lib/cql-functions.js index 157ef1e91..af763c364 100644 --- a/postgres/lib/cql-functions.js +++ b/postgres/lib/cql-functions.js @@ -6,7 +6,8 @@ const StandardFunctions = { if (x.val === '$now') sql += '::timestamp' return sql }, - countdistinct: x => `count(distinct ${x || '*'})`, + count: x => `count(${x?.val || x || '*'})`, + countdistinct: x => `count(distinct ${x.val || x || '*'})`, contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`, indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed startswith: (x, y) => `strpos(${x},${y}) = 1`, // sqlite instr is 1 indexed diff --git a/postgres/package.json b/postgres/package.json index ec9788814..618ece1be 100644 --- a/postgres/package.json +++ b/postgres/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/postgres", - "version": "1.7.0", + "version": "1.8.0", "description": "CDS database service for Postgres", "homepage": "https://github.com/cap-js/cds-dbs/tree/main/postgres#cds-database-service-for-postgres", "repository": { @@ -31,7 +31,7 @@ "start": "docker-compose -f pg-stack.yml up -d" }, "dependencies": { - "@cap-js/db-service": "^1.7.0", + "@cap-js/db-service": "^1.9.0", "pg": "^8" }, "peerDependencies": { diff --git a/postgres/test/ql.test.js b/postgres/test/ql.test.js index e95558cab..50e41a017 100644 --- a/postgres/test/ql.test.js +++ b/postgres/test/ql.test.js @@ -63,13 +63,13 @@ describe('QL to PostgreSQL', () => { .where({ abv: { '>': 1.0 } }) .orderBy({ abv: 'desc' }), ) - expect(beers[0].abv).to.equal("5.9") + expect(beers[0].abv).to.equal(cds.env.features.string_decimals ? "5.9" : 5.9) const reverseBeers = await cds.run( SELECT.from(Beers) .where({ abv: { '>': 1.0 } }) .orderBy({ abv: 'asc' }), ) - expect(reverseBeers[0].abv).to.equal("4.9") + expect(reverseBeers[0].abv).to.equal(cds.env.features.string_decimals ? "4.9" : 4.9) }) test('-> with groupBy', async () => { diff --git a/sqlite/CHANGELOG.md b/sqlite/CHANGELOG.md index 44b61dec3..fd8467f7e 100644 --- a/sqlite/CHANGELOG.md +++ b/sqlite/CHANGELOG.md @@ -4,6 +4,26 @@ - The format is based on [Keep a Changelog](http://keepachangelog.com/). - This project adheres to [Semantic Versioning](http://semver.org/). +## [1.7.1](https://github.com/cap-js/cds-dbs/compare/sqlite-v1.7.0...sqlite-v1.7.1) (2024-05-16) + + +### Fixed + +* **deps:** update dependency better-sqlite3 to v10 ([#636](https://github.com/cap-js/cds-dbs/issues/636)) ([0cc60e7](https://github.com/cap-js/cds-dbs/commit/0cc60e72ec18e1704a07e0a9bfee5388de682ec7)) + +## [1.7.0](https://github.com/cap-js/cds-dbs/compare/sqlite-v1.6.0...sqlite-v1.7.0) (2024-05-08) + + +### Added + +* select decimals as strings if cds.env.features.string_decimals ([#616](https://github.com/cap-js/cds-dbs/issues/616)) ([39addbf](https://github.com/cap-js/cds-dbs/commit/39addbfe01da757d86a4d65e62eda86e59fc9b87)) + + +### Fixed + +* Change `sql` property to `query` for errors ([#611](https://github.com/cap-js/cds-dbs/issues/611)) ([585577a](https://github.com/cap-js/cds-dbs/commit/585577a9817e7749fb71958c26c4bfa20981c663)) +* Improved placeholders and limit clause ([#567](https://github.com/cap-js/cds-dbs/issues/567)) ([d5d5dbb](https://github.com/cap-js/cds-dbs/commit/d5d5dbb7219bcef6134440715cf756fdd439f076)) + ## [1.6.0](https://github.com/cap-js/cds-dbs/compare/sqlite-v1.5.1...sqlite-v1.6.0) (2024-03-22) diff --git a/sqlite/cds-plugin.js b/sqlite/cds-plugin.js index 78f5da060..4eb2731bd 100644 --- a/sqlite/cds-plugin.js +++ b/sqlite/cds-plugin.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') if (!cds.env.fiori.lean_draft) { throw new Error('"@cap-js/sqlite" only works if cds.fiori.lean_draft is enabled. Please adapt your configuration.') diff --git a/sqlite/lib/SQLiteService.js b/sqlite/lib/SQLiteService.js index f11354562..e09420917 100644 --- a/sqlite/lib/SQLiteService.js +++ b/sqlite/lib/SQLiteService.js @@ -7,6 +7,7 @@ try { // When failing to load better-sqlite3 it fallsback to sql.js (wasm version of sqlite) sqlite = require('./sql.js.js') } + const $session = Symbol('dbc.session') const convStrm = require('stream/consumers') const { Readable } = require('stream') @@ -87,7 +88,7 @@ class SQLiteService extends SQLService { stream: (..._) => this._stream(stmt, ..._), } } catch (e) { - e.message += ' in:\n' + (e.sql = sql) + e.message += ' in:\n' + (e.query = sql) throw e } } @@ -174,7 +175,8 @@ class SQLiteService extends SQLService { } val(v) { - if (Buffer.isBuffer(v.val)) v.val = v.val.toString('base64') + if (typeof v.val === 'boolean') v.val = v.val ? 1 : 0 + else if (Buffer.isBuffer(v.val)) v.val = v.val.toString('base64') // intercept DateTime values and convert to Date objects to compare ISO Strings else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{1,9})?(Z|[+-]\d{2}(:?\d{2})?)$/.test(v.val)) { const date = new Date(v.val) @@ -196,12 +198,10 @@ class SQLiteService extends SQLService { // Used for INSERT statements static InputConverters = { ...super.InputConverters, - // The following allows passing in ISO strings with non-zulu // timezones and converts them into zulu dates and times Date: e => `strftime('%Y-%m-%d',${e})`, Time: e => `strftime('%H:%M:%S',${e})`, - // Both, DateTimes and Timestamps are canonicalized to ISO strings with // ms precision to allow safe comparisons, also to query {val}s in where clauses DateTime: e => `ISO(${e})`, @@ -210,31 +210,25 @@ class SQLiteService extends SQLService { static OutputConverters = { ...super.OutputConverters, - // Structs and arrays are stored as JSON strings; the ->'$' unwraps them. // Otherwise they would be added as strings to json_objects. Association: expr => `${expr}->'$'`, struct: expr => `${expr}->'$'`, array: expr => `${expr}->'$'`, - // SQLite has no booleans so we need to convert 0 and 1 boolean: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`, - // DateTimes are returned without ms added by InputConverters DateTime: e => `substr(${e},0,20)||'Z'`, - // Timestamps are returned with ms, as written by InputConverters. // And as cds.builtin.classes.Timestamp inherits from DateTime we need // to override the DateTime converter above Timestamp: undefined, - // int64 is stored as native int64 for best comparison // Reading int64 as string to not loose precision Int64: expr => `CAST(${expr} as TEXT)`, - + // REVISIT: always cast to string in next major // Reading decimal as string to not loose precision - Decimal: expr => `CAST(${expr} as TEXT)`, - + Decimal: cds.env.features.string_decimals ? expr => `CAST(${expr} as TEXT)` : undefined, // Binary is not allowed in json objects Binary: expr => `${expr} || ''`, } diff --git a/sqlite/package.json b/sqlite/package.json index 6f411b396..8ad6b28c5 100644 --- a/sqlite/package.json +++ b/sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/sqlite", - "version": "1.6.0", + "version": "1.7.1", "description": "CDS database service for SQLite", "homepage": "https://github.com/cap-js/cds-dbs/tree/main/sqlite#cds-database-service-for-sqlite", "repository": { @@ -30,8 +30,8 @@ "test": "jest --silent" }, "dependencies": { - "@cap-js/db-service": "^1.7.0", - "better-sqlite3": "^9.3.0" + "@cap-js/db-service": "^1.9.0", + "better-sqlite3": "^10.0.0" }, "optionalDependencies": { "sql.js": "^1.10.3" diff --git a/sqlite/test/general/stream.test.js b/sqlite/test/general/stream.test.js index 2006c83ab..3d776a3ef 100644 --- a/sqlite/test/general/stream.test.js +++ b/sqlite/test/general/stream.test.js @@ -261,6 +261,17 @@ describe('streaming', () => { await checkSize(stream2_) })) + test('WRITE stream property from READ stream', async () => cds.tx(async () => { + const { Images } = cds.entities('test') + const { data: stream } = await SELECT.one.from(Images).columns('data').where({ ID: 1 }) + + const changes = await UPDATE(Images).with({ data2: stream }).where({ ID: 3 }) + expect(changes).toEqual(1) + + const [{ data2: stream_ }] = await SELECT.from(Images).columns('data2').where({ ID: 3 }) + await checkSize(stream_) + })) + test('WRITE multiple blob properties', async () => cds.tx(async () => { const { Images } = cds.entities('test') const blob1 = fs.readFileSync(path.join(__dirname, 'samples/test.jpg')) diff --git a/sqlite/test/queries-without-models.test.js b/sqlite/test/queries-without-models.test.js index ec761527d..b27ed3265 100644 --- a/sqlite/test/queries-without-models.test.js +++ b/sqlite/test/queries-without-models.test.js @@ -1,5 +1,5 @@ const impl = require.resolve('../index') -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') const { expect } = cds.test // eslint-disable-next-line no-global-assign diff --git a/test/bookshop/package.json b/test/bookshop/package.json index 361b2ecbb..dd419c157 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -22,6 +22,9 @@ "cds": { "requires": { "db": "sql" + }, + "features": { + "odata_new_adapter": true } } } diff --git a/test/bookshop/srv/admin-service.js b/test/bookshop/srv/admin-service.js index c6c08bc8a..519c2ece8 100644 --- a/test/bookshop/srv/admin-service.js +++ b/test/bookshop/srv/admin-service.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds') module.exports = class AdminService extends cds.ApplicationService { init() { diff --git a/test/cds.js b/test/cds.js index 9a65a6133..ba0c89c18 100644 --- a/test/cds.js +++ b/test/cds.js @@ -1,4 +1,20 @@ -const cds = require('@sap/cds/lib') +// REVISIT: enable UInt8 type +const typeCheck = require('@sap/cds-compiler/lib/checks/checkForTypes.js') +typeCheck.type = function () { } + +// REVISIT: enable cds.hana types +const typeMapping = require('@sap/cds-compiler/lib/render/utils/common.js') +typeMapping.cdsToSqlTypes.postgres = { + ...typeMapping.cdsToSqlTypes.postgres, + // Fill in failing cds.hana types for postgres + 'cds.hana.CLOB': 'BYTEA', + 'cds.hana.BINARY': 'BYTEA', + 'cds.hana.TINYINT': 'SMALLINT', + 'cds.hana.ST_POINT': 'POINT', + 'cds.hana.ST_GEOMETRY': 'POLYGON', +} + +const cds = require('@sap/cds') module.exports = cds // Adding cds.hana types to cds.builtin.types @@ -70,7 +86,7 @@ cds.test = Object.setPrototypeOf(function () { const hash = createHash('sha1') const isolateName = (require.main.filename || 'test_tenant') + isolateCounter++ hash.update(isolateName) - isolate = { + ret.data.isolation = isolate = { // Create one database for each overall test execution database: process.env.TRAVIS_JOB_ID || process.env.GITHUB_RUN_ID || require('os').userInfo().username || 'test_db', // Create one tenant for each test suite @@ -106,6 +122,10 @@ cds.test = Object.setPrototypeOf(function () { // Clean database connection pool await cds.db?.disconnect?.() + if (isolate) { + await cds.db?.tenant?.(isolate, true) + } + // Clean cache delete cds.services._pending.db delete cds.services.db diff --git a/test/compliance/CREATE.test.js b/test/compliance/CREATE.test.js index f02896186..48690a687 100644 --- a/test/compliance/CREATE.test.js +++ b/test/compliance/CREATE.test.js @@ -1,5 +1,5 @@ const assert = require('assert') -const { Readable } = require('stream') +const { Readable } = require('stream') const { buffer } = require('stream/consumers') const cds = require('../cds.js') const fspath = require('path') @@ -59,7 +59,7 @@ describe('CREATE', () => { }) } }) - .catch(() => {}) + .catch(() => { }) await db.run(async tx => { deploy = Promise.resolve() @@ -79,7 +79,7 @@ describe('CREATE', () => { }, }), ) - await deploy.catch(() => {}) + await deploy.catch(() => { }) }) }) @@ -101,7 +101,7 @@ describe('CREATE', () => { }) } }) - .catch(() => {}) + .catch(() => { }) await db.disconnect() }) @@ -165,7 +165,7 @@ describe('CREATE', () => { } catch (e) { if (throws === false) throw e // Check for error test cases - assert.equal(e.message, throws, 'Ensure that the correct error message is being thrown.') + assert.match(e.message, throws, 'Ensure that the correct error message is being thrown.') return } @@ -181,7 +181,7 @@ describe('CREATE', () => { const sel = await tx.run({ SELECT: { from: { ref: [table] }, - columns + columns }, }) diff --git a/test/compliance/SELECT.test.js b/test/compliance/SELECT.test.js index 1bd02bc75..d4b707974 100644 --- a/test/compliance/SELECT.test.js +++ b/test/compliance/SELECT.test.js @@ -204,6 +204,19 @@ describe('SELECT', () => { assert.strictEqual(Object.keys(res[0].author).length, 200) }) + test('expand association with static values', async () => { + const cqn = { + SELECT: { + from: { ref: ['complex.associations.unmanaged.Authors'] }, + columns: [{ ref: ['ID'] }, { ref: ['static'], expand: ['*'] }] + }, + } + + const res = await cds.run(cqn) + // ensure that all values are returned in json format + assert.strictEqual(res[0].static.length, 1) + }) + test.skip('invalid cast (wrong)', async () => { await expect( cds.run(CQL` @@ -250,6 +263,23 @@ describe('SELECT', () => { assert.strictEqual(timestampMatches.length, 1, 'Ensure that the dateTime column matches the timestamp value') }) + test('combine expr and other compare', async () => { + const cqn = CQL`SELECT bool FROM basic.literals.globals` + cqn.SELECT.where = [ + { + xpr: [ + { + xpr: [{ ref: ['bool'] }, '!=', { val: true }] + } + ] + }, + 'and', + { ref: ['bool'] }, '=', { val: false } + ] + const res = await cds.run(cqn) + assert.strictEqual(res.length, 1, 'Ensure that all rows are coming back') + }) + test('exists path expression', async () => { const cqn = { SELECT: { diff --git a/test/compliance/resources/db/basic/literals.cds b/test/compliance/resources/db/basic/literals.cds index 6525ab78c..939d1689d 100644 --- a/test/compliance/resources/db/basic/literals.cds +++ b/test/compliance/resources/db/basic/literals.cds @@ -4,9 +4,15 @@ entity globals { bool : Boolean; } +entity uuid { + uuid : UUID; +} + entity number { - integer : Integer; - integer64 : Integer64; + integer8 : UInt8; + integer16 : Int16; + integer32 : Int32; + integer64 : Int64; double : cds.Double; // Decimal: (p,s) p = 1 - 38, s = 0 - p // p = number of total decimal digits diff --git a/test/compliance/resources/db/basic/literals/basic.literals.number.js b/test/compliance/resources/db/basic/literals/basic.literals.number.js index 9e5d1a614..998242e00 100644 --- a/test/compliance/resources/db/basic/literals/basic.literals.number.js +++ b/test/compliance/resources/db/basic/literals/basic.literals.number.js @@ -1,12 +1,53 @@ module.exports = [ { - integer: null, + integer8: null, }, { - integer: -2147483648, + integer8: 0, }, { - integer: 2147483647, + integer8: 255, + }, + /* REVISIT: UInt8 is not allowed to be over/under flow 0-255 + { + integer8: -1, + '!': /./, + }, + { + integer8: 256, + '!': /./, + }, + */ + { + integer16: null, + }, + { + integer16: 32767, + }, + { + integer16: -32768, + }, + /* REVISIT: UInt16 is not allowed to be over/under flow -32768 - 32767 + { + integer16: 32768, + '!': /./, + }, + { + integer16: -32769, + '!': /./, + }, + */ + { + integer32: null, + }, + { + integer32: -2147483648, + }, + { + integer32: 2147483647, + }, + { + integer64: null, }, { integer64: '9223372036854775806', @@ -16,15 +57,11 @@ module.exports = [ }, { decimal: '3.14153', - '=decimal': /^3.1415/ + '=decimal': /^3\.1415/ }, - /*{ - decimal: '12345678910', // TODO: should properly consider precision - '!': 'should throw an error' - },*/ { decimal: 3.14, - '=decimal': /^3.14/ + '=decimal': /^3\.14/ }, { double: 3.14159265358979 @@ -35,12 +72,12 @@ module.exports = [ }, { float: '-9007199254740991', - '=float': /^-9007199254740991/ + '=float': /-9007199254740991/ }, { - float: '9007199254740991', + float: '9007199254740991', '=float': /^9007199254740991/ - } + }, /* Ignoring transformations { decimal: 3.141592653589793, @@ -49,6 +86,6 @@ module.exports = [ { decimal: 31415, '=decimal': 5 - } - */, + }, + */ ] diff --git a/test/compliance/resources/db/basic/literals/basic.literals.string.js b/test/compliance/resources/db/basic/literals/basic.literals.string.js index 227bb933f..4de7f9808 100644 --- a/test/compliance/resources/db/basic/literals/basic.literals.string.js +++ b/test/compliance/resources/db/basic/literals/basic.literals.string.js @@ -31,6 +31,9 @@ module.exports = [ { char: 'A', }, + { + char: '대', // Ensure multi byte utf-8 characters also fit into a single character column + }, { large: () => [...new Array(1000)].map(alphabetize).join(''), }, diff --git a/test/compliance/resources/db/complex/associationsUnmanaged.cds b/test/compliance/resources/db/complex/associationsUnmanaged.cds index 4919981c1..dde05cdd0 100644 --- a/test/compliance/resources/db/complex/associationsUnmanaged.cds +++ b/test/compliance/resources/db/complex/associationsUnmanaged.cds @@ -11,4 +11,5 @@ entity Authors { key ID : Integer; name : String(111); books : Association to many Books on books.author = $self; + static : Association to many Books on static.author = $self and static.ID > 0; } diff --git a/test/compliance/resources/db/data/complex.associations.unmanaged.Authors.csv b/test/compliance/resources/db/data/complex.associations.unmanaged.Authors.csv new file mode 100644 index 000000000..f68ab4983 --- /dev/null +++ b/test/compliance/resources/db/data/complex.associations.unmanaged.Authors.csv @@ -0,0 +1,2 @@ +ID;name +1;Emily diff --git a/test/compliance/resources/db/data/complex.associations.unmanaged.Books.csv b/test/compliance/resources/db/data/complex.associations.unmanaged.Books.csv new file mode 100644 index 000000000..4caf39e2e --- /dev/null +++ b/test/compliance/resources/db/data/complex.associations.unmanaged.Books.csv @@ -0,0 +1,2 @@ +ID;title;author_ID +1;Wuthering Heights;1 diff --git a/test/compliance/resources/db/hana/index.cds b/test/compliance/resources/db/hana/index.cds index ece4a08ec..9789aa764 100644 --- a/test/compliance/resources/db/hana/index.cds +++ b/test/compliance/resources/db/hana/index.cds @@ -1,4 +1,4 @@ // namespace edge.hana; // Would overwrite default hana namespace -// using from './literals'; +using from './literals'; using from './funcs'; diff --git a/test/compliance/resources/db/hana/literals/edge.hana.literals.HANA_ST.js b/test/compliance/resources/db/hana/literals/edge.hana.literals.HANA_ST.js index 2e9b1835f..fd600f48f 100644 --- a/test/compliance/resources/db/hana/literals/edge.hana.literals.HANA_ST.js +++ b/test/compliance/resources/db/hana/literals/edge.hana.literals.HANA_ST.js @@ -2,15 +2,16 @@ module.exports = [ { point: null, - }, + },/* { point: 'POINT(1 1)', }, { point: '0101000000000000000000F03F000000000000F03F', - }, + },*/ { // GeoJSON specification: https://www.rfc-editor.org/rfc/rfc7946 point: '{"x":1,"y":1,"spatialReference":{"wkid":4326}}', + '=point': /\{\W*"x"\W*:\W*1\W*,\W*"y"\W*:\W*1(,.*)?\}/, }, ] diff --git a/test/compliance/resources/package.json b/test/compliance/resources/package.json index 02026ecb6..a5bce2e95 100644 --- a/test/compliance/resources/package.json +++ b/test/compliance/resources/package.json @@ -10,6 +10,9 @@ "url": ":memory:" } } + }, + "features": { + "string_decimals": true } } } diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index cdb82a471..fed31c252 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -1,7 +1,6 @@ const cds = require('../../cds.js') const bookshop = require('path').resolve(__dirname, '../../bookshop') cds.test.in(bookshop) -cds.env.features.odata_new_adapter = true describe('Bookshop - Functions', () => { const { expect, GET } = cds.test() @@ -11,6 +10,11 @@ describe('Bookshop - Functions', () => { const res = await GET(`/browse/Books?$filter=concat(concat(author,', '),title) eq 'Edgar Allen Poe, Eleonora'`) expect(res.status).to.be.eq(200) expect(res.data.value.length).to.be.eq(1) + + // Test concat with more then 2 arguments + const { Books } = cds.entities('CatalogService') + const cqnRes = await SELECT.from(Books).where`concat(author, ${', '}, title) = ${'Edgar Allen Poe, Eleonora'}` + expect(cqnRes.length).to.be.eq(1) }) test('contains', async () => { @@ -92,8 +96,8 @@ describe('Bookshop - Functions', () => { .where`matchespattern(author,${'^Ed'})` expect(res1.length).to.eq(res2.length).to.be.eq(2) - expect(res1).to.deep.eq(res2).to.deep.include({author: 'Edgar Allen Poe', title: 'Eleonora'}) - expect(res1).to.deep.eq(res2).to.deep.include({author: 'Edgar Allen Poe', title: 'The Raven'}) + expect(res1).to.deep.eq(res2).to.deep.include({ author: 'Edgar Allen Poe', title: 'Eleonora' }) + expect(res1).to.deep.eq(res2).to.deep.include({ author: 'Edgar Allen Poe', title: 'The Raven' }) }) test('tolower', async () => { @@ -141,8 +145,8 @@ describe('Bookshop - Functions', () => { // okra error: 400 - Property 'hassubset' does not exist in type 'CatalogService.Books' // new adapter error: 400 - Function 'hassubset' is not supported const { Books } = cds.entities('sap.capire.bookshop') - await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([123, ['1','2','3']])) - await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([124, ['2','5','6']])) + await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([123, ['1', '2', '3']])) + await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([124, ['2', '5', '6']])) const res = await GET(`/browse/Books?$filter=hassubset(footnotes, ['3','1'])`) expect(res.status).to.be.eq(200) expect(res.data.value.length).to.be.eq(1) @@ -151,8 +155,8 @@ describe('Bookshop - Functions', () => { // okra error: 400 - Property 'hassubset' does not exist in type 'CatalogService.Books' // new adapter error: 400 - Function 'hassubsequence' is not supported const { Books } = cds.entities('sap.capire.bookshop') - await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([123, ['1','2','3']])) - await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([124, ['2','5','6']])) + await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([123, ['1', '2', '3']])) + await cds.run(INSERT.into(Books).columns(['ID', 'footnotes']).rows([124, ['2', '5', '6']])) const res = await GET(`/browse/Books?$filter=hassubset(footnotes, ['1','3'])`) expect(res.status).to.be.eq(200) expect(res.data.value.length).to.be.eq(1) @@ -255,7 +259,7 @@ describe('Bookshop - Functions', () => { test('day function with null value', async () => { const { result } = await SELECT.one(`day(null) as result`) - .from('sap.capire.bookshop.Books') + .from('sap.capire.bookshop.Books') expect(result).to.be.null }) @@ -313,27 +317,27 @@ describe('Bookshop - Functions', () => { test('now', async () => { const db = await cds.connect.to('db') return db.run(async tx => { - Object.defineProperty(cds.context, 'timestamp', {value: new Date('1972-09-15T21:36:51.123Z')}) + Object.defineProperty(cds.context, 'timestamp', { value: new Date('1972-09-15T21:36:51.123Z') }) const cqn = { SELECT: { - from: { ref: ['localized.CatalogService.Books'], as: 'Books' }, - columns: [{ ref: ['Books', 'ID'] }], - where: [ - { - func: 'now', - args: [], - }, - '=', - { - val: '1972-09-15T21:36:51.123Z', - }, - ], - }, - } - const res = await tx.run(cqn) - expect(res.length).to.be.eq(5) + from: { ref: ['localized.CatalogService.Books'], as: 'Books' }, + columns: [{ ref: ['Books', 'ID'] }], + where: [ + { + func: 'now', + args: [], + }, + '=', + { + val: '1972-09-15T21:36:51.123Z', + }, + ], + }, + } + const res = await tx.run(cqn) + expect(res.length).to.be.eq(5) + }) }) - }) test('second', async () => { const res = await GET(`/browse/Books?$select=ID&$filter=second(1970-01-01T00:00:45.123Z) eq 45&$top=1`) diff --git a/test/scenarios/bookshop/read.test.js b/test/scenarios/bookshop/read.test.js index a57a4b4d3..9b60fb492 100644 --- a/test/scenarios/bookshop/read.test.js +++ b/test/scenarios/bookshop/read.test.js @@ -157,25 +157,6 @@ describe('Bookshop - Read', () => { expect(res.data.author.books.length).to.be.eq(2) }) - test('Insert Book', async () => { - const res = await POST( - '/admin/Books', - { - ID: 2, - title: 'Poems : Pocket Poets', - descr: - "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", - author: { ID: 101 }, - genre: { ID: 12 }, - stock: 5, - price: '12.05', - currency: { code: 'USD' }, - }, - admin, - ) - expect(res.status).to.be.eq(201) - }) - test('Sorting Books', async () => { const res = await POST( '/admin/Books', @@ -192,19 +173,23 @@ describe('Bookshop - Read', () => { }, admin, ) - expect(res.status).to.be.eq(201) - - const res2 = await GET('/browse/Books?$orderby=title', { headers: { 'accept-language': 'de' } }) - expect(res2.status).to.be.eq(200) - expect(res2.data.value[1].title).to.be.eq('dracula') - - const q = CQL`SELECT title FROM sap.capire.bookshop.Books ORDER BY title` - const res3 = await cds.run(q) - expect(res3[res3.length - 1].title).to.be.eq('dracula') - - q.SELECT.localized = true - const res4 = await cds.run(q) - expect(res4[1].title).to.be.eq('dracula') + try { + expect(res.status).to.be.eq(201) + + const res2 = await GET('/browse/Books?$orderby=title', { headers: { 'accept-language': 'de' } }) + expect(res2.status).to.be.eq(200) + expect(res2.data.value[1].title).to.be.eq('dracula') + + const q = CQL`SELECT title FROM sap.capire.bookshop.Books ORDER BY title` + const res3 = await cds.run(q) + expect(res3[res3.length - 1].title).to.be.eq('dracula') + + q.SELECT.localized = true + const res4 = await cds.run(q) + expect(res4[1].title).to.be.eq('dracula') + } finally { + await DELETE('/admin/Books(280)', admin) + } }) test('Filter Books(multiple functions)', async () => { @@ -215,23 +200,34 @@ describe('Bookshop - Read', () => { expect(res.data.value.length).to.be.eq(3) }) - test.skip('Insert Booky', async () => { - const res = await POST( - '/admin/Booky', - { - ID: 2000, - totle: 'Poems : Pocket Poets', - description: - "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", - author: { ID: 101 }, - genre: { ID: 12 }, - stock: 5, - price: '12.05', - currency: { code: 'USD' }, - }, + test('Filter Books(LargeBinary type)', async () => { + expect(await GET( + `/admin/Books?$filter=image ne null`, admin, - ) - expect(res.status).to.be.eq(201) + )).to.have.nested.property('data.value.length', 0) + + expect(await GET( + `/admin/Books?$filter=null ne image`, + admin, + )).to.have.nested.property('data.value.length', 0) + + + expect(await GET( + `/admin/Books?$filter=image eq null`, + admin, + )).to.have.nested.property('data.value.length', 5) + + // intentionally not tranformed `null = image` SQL which always returns `null` + expect(await GET( + `/admin/Books?$filter=null eq image`, + admin, + )).to.have.nested.property('data.value.length', 0) + }) + + test('Filter Books(complex filter in apply)', async () => { + const res = await GET(`/browse/Books?$apply=filter(((ID eq 251 or ID eq 252) and ((contains(tolower(descr),tolower('Edgar'))))))`) + expect(res.status).to.be.eq(200) + expect(res.data.value.length).to.be.eq(2) }) it('joins as subselect are executable', async () => { @@ -299,9 +295,4 @@ describe('Bookshop - Read', () => { return expect(cds.db.run(query)).to.be.rejectedWith(/joins must specify the selected columns/) }) - test('Delete Book', async () => { - const res = await DELETE('/admin/Books(271)', admin) - expect(res.status).to.be.eq(204) - }) - })