@cap-js/db-service
Advanced tools
Comparing version 1.0.1 to 1.1.0
@@ -7,2 +7,12 @@ # Change Log | ||
## Version 1.1.0 - 2023-08-01 | ||
### Fixed | ||
- `UPDATE` with path expressions do not end up in a dump anymore. Instead, a proper error message is emitted. | ||
- `UPDATE` is only noop if it does not include an element annotated with `@cds.on.update`. | ||
- `SELECT` with `'*'` that is not expanded creates now a clearer error when the column name is required. | ||
- `SELECT` with plain SQL statements will return correct result regardless of casing. | ||
- View resolving for streams. | ||
## Version 1.0.1 - 2023-07-03 | ||
@@ -16,5 +26,4 @@ | ||
## Version 1.0.0 - 2023-06-23 | ||
- Initial Release | ||
- Initial Release |
18
index.js
@@ -0,4 +1,18 @@ | ||
const DatabaseService = require('./lib/common/DatabaseService') | ||
const SQLService = require('./lib/SQLService') | ||
const CQN2SQL = require('./lib/cqn2sql').classDefinition | ||
/** | ||
* @template T | ||
* @typedef {import('./lib/common/factory').Factory<T>} Factory | ||
*/ | ||
/** | ||
* @typedef {import('./lib/SQLService').prototype.PreparedStatement} PreparedStatement | ||
*/ | ||
module.exports = { | ||
DatabaseService: require('./lib/common/DatabaseService'), | ||
SQLService: require('./lib/SQLService'), | ||
DatabaseService, | ||
SQLService, | ||
CQN2SQL, | ||
} |
@@ -11,2 +11,4 @@ const infer = require('../infer') | ||
/** @typedef {unknown} DatabaseDriver */ | ||
class DatabaseService extends cds.Service { | ||
@@ -16,2 +18,4 @@ /** | ||
* https://github.com/coopernurse/node-pool#createpool. | ||
* @abstract | ||
* @type {import('./factory').Factory<DatabaseDriver>} | ||
*/ | ||
@@ -23,2 +27,5 @@ get factory() { | ||
/** | ||
* @returns {boolean} whether this service is multi tenant enabled | ||
*/ | ||
get isMultitenant() { | ||
@@ -29,3 +36,12 @@ return 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy | ||
/** | ||
* Set one or more session context variables like so: | ||
* @typedef {Object} DefaultSessionVariables | ||
* @property {string} '$user.id' | ||
* @property {string} '$user.locale' | ||
* @property {string} '$valid.from' | ||
* @property {string} '$valid.to' | ||
*/ | ||
/** | ||
* Set one or more session context variables | ||
* @example | ||
* ```js | ||
@@ -38,8 +54,14 @@ * const tx = cds.db.tx() | ||
* ``` | ||
* @param {unknown|DefaultSessionVariables} variables | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
set(variables) { | ||
variables | ||
throw '2b overridden by subclass' | ||
} | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} q | ||
* @param {import('@sap/cds/apis/csn').CSN} m | ||
* @returns {import('../infer/cqn').Query} | ||
*/ | ||
infer(q, m = this.model) { | ||
@@ -49,2 +71,5 @@ return infer(q, m) | ||
/** | ||
* @returns {Promise<DatabaseService>} | ||
*/ | ||
async begin() { | ||
@@ -58,2 +83,3 @@ const ctx = this.context | ||
try { | ||
/** @type {DatabaseDriver} */ | ||
dbc = this.dbc = await pool.acquire() | ||
@@ -67,2 +93,5 @@ } catch (err) { | ||
connections.push(this) | ||
/** | ||
* @param {DatabaseDriver} dbc | ||
*/ | ||
this._release = async dbc => { | ||
@@ -119,2 +148,5 @@ await pool.release(dbc) | ||
// REVISIT: should happen automatically after a configurable time | ||
/** | ||
* @param {string} tenant | ||
*/ | ||
async disconnect(tenant) { | ||
@@ -128,2 +160,9 @@ const pool = this.pools[tenant] | ||
/** | ||
* Runs a Query on the database service | ||
* @param {import("@sap/cds/apis/cqn").Query} query | ||
* @param {unknown} data | ||
* @param {...unknown} etc | ||
* @returns {Promise<unknown>} | ||
*/ | ||
run(query, data, ...etc) { | ||
@@ -135,7 +174,19 @@ // Allow db.run('...',1,2,3,4) | ||
url4(/*tenant*/) { | ||
// eslint-disable-line no-unused-vars | ||
/** | ||
* Generated the database url for the given tenant | ||
* @param {string} tenant | ||
* @returns {string} | ||
*/ | ||
url4(tenant) { | ||
tenant | ||
let { url } = this.options?.credentials || this.options || {} | ||
return url | ||
} | ||
/** | ||
* Old name of url4 | ||
* @deprecated | ||
* @param {string} tenant | ||
* @returns {string} | ||
*/ | ||
getDbUrl(tenant) { | ||
@@ -142,0 +193,0 @@ return this.url4(tenant) |
@@ -5,4 +5,20 @@ const StandardFunctions = { | ||
// String and Collection Functions | ||
// length : (x) => `length(${x})`, | ||
/** | ||
* Generates SQL statement that produces the length of a given string | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
length: x => `length(${x})`, | ||
/** | ||
* Generates SQL statement that produces the average of a given expression | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
average: x => `avg(${x})`, | ||
/** | ||
* Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns | ||
* @param {string} ref | ||
* @param {string} arg | ||
* @returns {string} | ||
*/ | ||
search: function (ref, arg) { | ||
@@ -14,10 +30,61 @@ if (!('val' in arg)) throw `SQLite only supports single value arguments for $search` | ||
}, | ||
/** | ||
* Generates SQL statement that produces a string with all provided strings concatenated | ||
* @param {...string} args | ||
* @returns {string} | ||
*/ | ||
concat: (...args) => args.join('||'), | ||
/** | ||
* Generates SQL statement that produces a boolean value indicating whether the first string contains the second string | ||
* @param {...string} args | ||
* @returns {string} | ||
*/ | ||
contains: (...args) => `ifnull(instr(${args}),0)`, | ||
/** | ||
* Generates SQL statement that produces the number of elements in a given collection | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
count: x => `count(${x || '*'})`, | ||
/** | ||
* Generates SQL statement that produces the number of distinct values of a given expression | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
countdistinct: x => `count(distinct ${x || '*'})`, | ||
/** | ||
* Generates SQL statement that produces the index of the first occurrence of the second string in the first string | ||
* @param {string} x | ||
* @param {string} y | ||
* @returns {string} | ||
*/ | ||
indexof: (x, y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed | ||
/** | ||
* Generates SQL statement that produces a boolean value indicating whether the first string starts with the second string | ||
* @param {string} x | ||
* @param {string} y | ||
* @returns {string} | ||
*/ | ||
startswith: (x, y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed | ||
// takes the end of the string of the size of the target and compares it with the target | ||
/** | ||
* Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string | ||
* @param {string} x | ||
* @param {string} y | ||
* @returns {string} | ||
*/ | ||
endswith: (x, y) => `substr(${x}, length(${x}) + 1 - length(${y})) = ${y}`, | ||
/** | ||
* Generates SQL statement that produces the substring of a given string | ||
* @example | ||
* // returns 'bc' | ||
* {func:'substring',args:[{val:'abc'},{val:1}]} | ||
* @example | ||
* // returns 'b' | ||
* {func:'substring',args:[{val:'abc'},{val:1},{val:1}]} | ||
* @param {string} x | ||
* @param {string} y | ||
* @param {string} z | ||
* @returns {string} | ||
*/ | ||
substring: (x, y, z) => | ||
@@ -29,23 +96,103 @@ z | ||
// String Functions | ||
/** | ||
* Generates SQL statement that matches the given string against a regular expression | ||
* @param {string} x | ||
* @param {string} y | ||
* @returns {string} | ||
*/ | ||
matchesPattern: (x, y) => `${x} regexp ${y})`, | ||
/** | ||
* Generates SQL statement that produces the lower case value of a given string | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
tolower: x => `lower(${x})`, | ||
/** | ||
* Generates SQL statement that produces the upper case value of a given string | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
toupper: x => `upper(${x})`, | ||
// trim : (x) => `trim(${x})`, | ||
/** | ||
* Generates SQL statement that produces the trimmed value of a given string | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
trim: x => `trim(${x})`, | ||
// Arithmetic Functions | ||
/** | ||
* Generates SQL statement that produces the rounded up value of a given number | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
ceiling: x => `ceil(${x})`, | ||
// floor : (x) => `floor(${x})`, | ||
// round : (x) => `round(${x})`, | ||
/** | ||
* Generates SQL statement that produces the rounded down value of a given number | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
floor: x => `floor(${x})`, | ||
/** | ||
* Generates SQL statement that produces the rounded value of a given number | ||
* @param {string} x | ||
* @param {string} p precision | ||
* @returns {string} | ||
*/ | ||
round: (x, p) => `round(${x}${p ? `,${p}` : ''})`, | ||
// Date and Time Functions | ||
/** | ||
* Generates SQL statement that produces the year of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
year: x => `cast( strftime('%Y',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the month of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
month: x => `cast( strftime('%m',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the day of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
day: x => `cast( strftime('%d',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the hours of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
hour: x => `cast( strftime('%H',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the minutes of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
minute: x => `cast( strftime('%M',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the seconds of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
second: x => `cast( strftime('%S',${x}) as Integer )`, | ||
/** | ||
* Generates SQL statement that produces the fractional seconds of a given timestamp | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
fractionalseconds: x => `cast( strftime('%f0000',${x}) as Integer )`, | ||
/** | ||
* maximum date time value | ||
* @returns {string} | ||
*/ | ||
maxdatetime: () => '9999-12-31 23:59:59.999', | ||
/** | ||
* minimum date time value | ||
* @returns {string} | ||
*/ | ||
mindatetime: () => '0001-01-01 00:00:00.000', | ||
@@ -58,2 +205,7 @@ | ||
// which can be divided by 60 back to minutes | ||
/** | ||
* Generates SQL statement that produces the offset in minutes of a given date time offset string | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
totaloffsetminutes: x => `case | ||
@@ -72,2 +224,7 @@ when substr(${x}, length(${x})) = 'z' then 0 | ||
// while the odata specification states that the seconds has to be fractional which only julianday allows | ||
/** | ||
* Generates SQL statement that produces an OData compliant duration string like: P12DT23H59M59.999999999999S | ||
* @param {string} x | ||
* @returns {string} | ||
*/ | ||
totalseconds: x => `( | ||
@@ -98,3 +255,15 @@ ( | ||
// Time functions | ||
/** | ||
* Generates SQL statement that calculates the difference in 100nanoseconds between two timestamps | ||
* @param {string} x left timestamp | ||
* @param {string} y right timestamp | ||
* @returns {string} | ||
*/ | ||
nano100_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 864000000000`, | ||
/** | ||
* Generates SQL statement that calculates the difference in seconds between two timestamps | ||
* @param {string} x left timestamp | ||
* @param {string} y right timestamp | ||
* @returns {string} | ||
*/ | ||
seconds_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 86400`, | ||
@@ -105,2 +274,8 @@ // Calculates the difference in full days using julian day | ||
// Which on the day resolution are included as the individual days therefor ignoring them to match HANA | ||
/** | ||
* Generates SQL statement that calculates the difference in days between two timestamps | ||
* @param {string} x left timestamp | ||
* @param {string} y right timestamp | ||
* @returns {string} | ||
*/ | ||
days_between: (x, y) => `( | ||
@@ -132,2 +307,8 @@ cast ( julianday(${y}) as Integer ) - cast ( julianday(${x}) as Integer ) | ||
*/ | ||
/** | ||
* Generates SQL statement that calculates the difference in months between two timestamps | ||
* @param {string} x left timestamp | ||
* @param {string} y right timestamp | ||
* @returns {string} | ||
*/ | ||
months_between: (x, y) => ` | ||
@@ -150,2 +331,8 @@ ( | ||
)`, | ||
/** | ||
* Generates SQL statement that calculates the difference in years between two timestamps | ||
* @param {string} x left timestamp | ||
* @param {string} y right timestamp | ||
* @returns {string} | ||
*/ | ||
years_between(x, y) { | ||
@@ -152,0 +339,0 @@ return `floor(${this.months_between(x, y)} / 12)` |
@@ -17,7 +17,21 @@ const cds = require('@sap/cds/lib') | ||
class CQN2SQLRenderer { | ||
/** | ||
* Creates a new CQN2SQL instance for processing a query | ||
* @constructor | ||
* @param {import('@sap/cds/apis/services').ContextProperties} context the cds.context of the request | ||
*/ | ||
constructor(context) { | ||
/** | ||
* @type {import('@sap/cds/apis/services').ContextProperties} | ||
*/ | ||
this.context = cds.context || context | ||
// REVISIT: find a way to make CQN2SQLRenderer work in SQLService as well | ||
/** @type {CQN2SQLRenderer|unknown} */ | ||
this.class = new.target // for IntelliSense | ||
this.class._init() // is a noop for subsequent calls | ||
} | ||
/** | ||
* Initializes the class one first creation to link types to data converters | ||
*/ | ||
static _init() { | ||
@@ -41,5 +55,15 @@ const _add_mixins = (aspect, mixins) => { | ||
/** | ||
* Renders incoming query into SQL and generates binding values | ||
* @param {import('./infer/cqn').Query} q CQN query to be rendered | ||
* @param {unknown[]|undefined} vars Values to be used for params | ||
* @returns {CQN2SQLRenderer|unknown} | ||
*/ | ||
render(q, vars) { | ||
const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ... | ||
/** | ||
* @type {string} the rendered SQL string | ||
*/ | ||
this.sql = '' // to have it as first property for debugging | ||
/** @type {unknown[]} */ | ||
this.values = [] // prepare values, filled in by subroutines | ||
@@ -56,2 +80,7 @@ this[cmd]((this.cqn = q)) // actual sql rendering happens here | ||
/** | ||
* Links the incoming query with the current service model | ||
* @param {import('./infer/cqn').Query} q | ||
* @returns {import('./infer/cqn').Query} | ||
*/ | ||
infer(q) { | ||
@@ -63,2 +92,6 @@ return q.target ? q : cds_infer(q) | ||
/** | ||
* Renders a CREATE query into generic SQL | ||
* @param {import('./infer/cqn').CREATE} q | ||
*/ | ||
CREATE(q) { | ||
@@ -78,2 +111,7 @@ const { target } = q, | ||
/** | ||
* Renders a column clause for the given elements | ||
* @param {import('./infer/cqn').elements} elements | ||
* @returns {string} SQL | ||
*/ | ||
CREATE_elements(elements) { | ||
@@ -90,2 +128,7 @@ let sql = '' | ||
/** | ||
* Renders a column definition for the given element | ||
* @param {import('./infer/cqn').element} element | ||
* @returns {string} SQL | ||
*/ | ||
CREATE_element(element) { | ||
@@ -96,2 +139,7 @@ const type = this.type4(element) | ||
/** | ||
* Renders the SQL type definition for the given element | ||
* @param {import('./infer/cqn').element} element | ||
* @returns {string} | ||
*/ | ||
type4(element) { | ||
@@ -105,2 +153,5 @@ if (!element._type) element = cds.builtin.types[element.type] || element | ||
/** @callback converter */ | ||
/** @type {Object<string,import('@sap/cds/apis/csn').Definition>} */ | ||
static TypeMap = { | ||
@@ -131,2 +182,6 @@ // Utilizing cds.linked inheritance | ||
/** | ||
* Renders a DROP query into generic SQL | ||
* @param {import('./infer/cqn').DROP} q | ||
*/ | ||
DROP(q) { | ||
@@ -140,2 +195,6 @@ const { target } = q | ||
/** | ||
* Renders a SELECT statement into generic SQL | ||
* @param {import('./infer/cqn').SELECT} q | ||
*/ | ||
SELECT(q) { | ||
@@ -158,12 +217,30 @@ let { from, expand, where, groupBy, having, orderBy, limit, one, distinct, localized } = q.SELECT | ||
else if ((x = limit)) sql += ` LIMIT ${this.limit(x)}` | ||
if (expand) sql = this.SELECT_expand(q, sql) | ||
// Expand cannot work without an inferred query | ||
if (expand) { | ||
if (!q.elements) cds.error`Query was not inferred and includes expand. For which the metadata is missing.` | ||
sql = this.SELECT_expand(q, sql) | ||
} | ||
return (this.sql = sql) | ||
} | ||
/** | ||
* Renders a column clause into generic SQL | ||
* @param {import('./infer/cqn').SELECT} param0 | ||
* @returns {string} SQL | ||
*/ | ||
SELECT_columns({ SELECT }) { | ||
// REVISIT: We don't have to run x.as through this.column_name(), do we? | ||
if (!SELECT.columns) return '*' | ||
return SELECT.columns.map(x => this.column_expr(x) + (typeof x.as === 'string' ? ' as ' + this.quote(x.as) : '')) | ||
return SELECT.columns.map(x => { | ||
if (x === '*') return x | ||
return this.column_expr(x) + (typeof x.as === 'string' ? ' as ' + this.quote(x.as) : '') | ||
}) | ||
} | ||
/** | ||
* Renders a JSON select around the provided SQL statement | ||
* @param {import('./infer/cqn').SELECT} param0 | ||
* @param {string} sql | ||
* @returns {string} SQL | ||
*/ | ||
SELECT_expand({ SELECT, elements }, sql) { | ||
@@ -197,2 +274,7 @@ if (!SELECT.columns) return sql | ||
/** | ||
* Renders a SELECT column expression into generic SQL | ||
* @param {import('./infer/cqn').col} x | ||
* @returns {string} SQL | ||
*/ | ||
column_expr(x) { | ||
@@ -208,2 +290,7 @@ if (x.func && !x.as) x.as = x.func | ||
/** | ||
* Renders a FROM clause into generic SQL | ||
* @param {import('./infer/cqn').source} from | ||
* @returns {string} SQL | ||
*/ | ||
from(from) { | ||
@@ -224,2 +311,7 @@ const { ref, as } = from, | ||
/** | ||
* Renders a WHERE clause into generic SQL | ||
* @param {import('./infer/cqn').predicate} xpr | ||
* @returns {string} SQL | ||
*/ | ||
where(xpr) { | ||
@@ -229,2 +321,7 @@ return this.xpr({ xpr }) | ||
/** | ||
* Renders a HAVING clause into generic SQL | ||
* @param {import('./infer/cqn').predicate} xpr | ||
* @returns {string} SQL | ||
*/ | ||
having(xpr) { | ||
@@ -234,2 +331,7 @@ return this.xpr({ xpr }) | ||
/** | ||
* Renders a groupBy clause into generic SQL | ||
* @param {import('./infer/cqn').expr[]} clause | ||
* @returns {string[] | string} SQL | ||
*/ | ||
groupBy(clause) { | ||
@@ -239,2 +341,8 @@ return clause.map(c => this.expr(c)) | ||
/** | ||
* Renders an orderBy clause into generic SQL | ||
* @param {import('./infer/cqn').ordering_term[]} orderBy | ||
* @param {boolean | undefined} localized | ||
* @returns {string[] | string} SQL | ||
*/ | ||
orderBy(orderBy, localized) { | ||
@@ -251,2 +359,8 @@ return orderBy.map( | ||
/** | ||
* Renders an limit clause into generic SQL | ||
* @param {import('./infer/cqn').limit} param0 | ||
* @returns {string} SQL | ||
* @throws {Error} When no rows are defined | ||
*/ | ||
limit({ rows, offset }) { | ||
@@ -259,2 +373,7 @@ if (!rows) throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)') | ||
/** | ||
* Renders an INSERT query into generic SQL | ||
* @param {import('./infer/cqn').INSERT} q | ||
* @returns {string} SQL | ||
*/ | ||
INSERT(q) { | ||
@@ -273,2 +392,7 @@ const { INSERT } = q | ||
/** | ||
* Renders an INSERT query with entries property | ||
* @param {import('./infer/cqn').INSERT} q | ||
* @returns {string} SQL | ||
*/ | ||
INSERT_entries(q) { | ||
@@ -285,2 +409,4 @@ const { INSERT } = q | ||
: ObjectKeys(INSERT.entries[0]) | ||
/** @type {string[]} */ | ||
this.columns = columns.filter(elements ? c => !elements[c]?.['@cds.extension'] : () => true).map(c => this.quote(c)) | ||
@@ -318,2 +444,7 @@ | ||
/** | ||
* Renders an INSERT query with rows property | ||
* @param {import('./infer/cqn').INSERT} q | ||
* @returns {string} SQL | ||
*/ | ||
INSERT_rows(q) { | ||
@@ -347,2 +478,7 @@ const { INSERT } = q | ||
/** | ||
* Renders an INSERT query with values property | ||
* @param {import('./infer/cqn').INSERT} q | ||
* @returns {string} SQL | ||
*/ | ||
INSERT_values(q) { | ||
@@ -353,2 +489,7 @@ let { columns, values } = q.INSERT | ||
/** | ||
* Renders an INSERT query from SELECT query | ||
* @param {import('./infer/cqn').INSERT} q | ||
* @returns {string} SQL | ||
*/ | ||
INSERT_select(q) { | ||
@@ -369,2 +510,8 @@ const { INSERT } = q | ||
/** | ||
* Wraps the provided SQL expression for output processing | ||
* @param {import('./infer/cqn').element} element | ||
* @param {string} expr | ||
* @returns {string} SQL | ||
*/ | ||
output_converter4(element, expr) { | ||
@@ -375,4 +522,6 @@ const fn = element?.[this.class._convertOutput] | ||
/** @type {import('./converters').Converters} */ | ||
static InputConverters = {} // subclasses to override | ||
/** @type {import('./converters').Converters} */ | ||
static OutputConverters = {} // subclasses to override | ||
@@ -384,2 +533,7 @@ | ||
/** | ||
* Renders an UPSERT query into generic SQL | ||
* @param {import('./infer/cqn').UPDATE} q | ||
* @returns {string} SQL | ||
*/ | ||
UPSERT(q) { | ||
@@ -406,2 +560,7 @@ let { UPSERT } = q, | ||
/** | ||
* Renders an UPDATE query into generic SQL | ||
* @param {import('./infer/cqn').UPDATE} q | ||
* @returns {string} SQL | ||
*/ | ||
UPDATE(q) { | ||
@@ -445,2 +604,7 @@ const { | ||
/** | ||
* Renders a DELETE query into generic SQL | ||
* @param {import('./infer/cqn').DELETE} param0 | ||
* @returns {string} SQL | ||
*/ | ||
DELETE({ DELETE: { from, where } }) { | ||
@@ -454,2 +618,7 @@ let sql = `DELETE FROM ${this.from(from)}` | ||
/** | ||
* Renders a STREAM query into generic SQL | ||
* @param {import('./infer/cqn').STREAM} q | ||
* @returns {string} SQL | ||
*/ | ||
STREAM(q) { | ||
@@ -476,2 +645,8 @@ let { from, into, where, column, data } = q.STREAM | ||
/** | ||
* Renders an expression object into generic SQL | ||
* @param {import('./infer/cqn').expr} x | ||
* @returns {string} SQL | ||
* @throws {Error} When an unknown un supported expression is provided | ||
*/ | ||
expr(x) { | ||
@@ -490,2 +665,7 @@ const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql | ||
/** | ||
* Renders an list of expression objects into generic SQL | ||
* @param {import('./infer/cqn').xpr} param0 | ||
* @returns {string} SQL | ||
*/ | ||
xpr({ xpr }) { | ||
@@ -502,2 +682,9 @@ return xpr | ||
/** | ||
* Renders an operation into generic SQL | ||
* @param {string} x The current operator string | ||
* @param {Number} i Current index of the operator inside the xpr | ||
* @param {import('./infer/cqn').predicate[]} xpr The parent xpr in which the operator is used | ||
* @returns {string} The correct operator string | ||
*/ | ||
operator(x, i, xpr) { | ||
@@ -509,2 +696,8 @@ if (x === '=' && xpr[i + 1]?.val === null) return 'is' | ||
/** | ||
* Renders an argument place holder into the SQL for prepared statements | ||
* @param {import('./infer/cqn').ref} param0 | ||
* @returns {string} SQL | ||
* @throws {Error} When an unsupported ref definition is provided | ||
*/ | ||
param({ ref }) { | ||
@@ -515,2 +708,7 @@ if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}` | ||
/** | ||
* Renders a ref into generic SQL | ||
* @param {import('./infer/cqn').ref} param0 | ||
* @returns {string} SQL | ||
*/ | ||
ref({ ref }) { | ||
@@ -520,2 +718,7 @@ return ref.map(r => this.quote(r)).join('.') | ||
/** | ||
* Renders a value into the correct SQL syntax of a placeholder for a prepared statement | ||
* @param {import('./infer/cqn').val} param0 | ||
* @returns {string} SQL | ||
*/ | ||
val({ val }) { | ||
@@ -543,2 +746,7 @@ switch (typeof val) { | ||
static Functions = require('./cql-functions') | ||
/** | ||
* Renders a function call into mapped SQL definitions from the Functions definition | ||
* @param {import('./infer/cqn').func} param0 | ||
* @returns {string} SQL | ||
*/ | ||
func({ func, args }) { | ||
@@ -549,2 +757,7 @@ args = (args || []).map(e => (e === '*' ? e : { __proto__: e, toString: (x = e) => this.expr(x) })) | ||
/** | ||
* Renders a list into generic SQL | ||
* @param {import('./infer/cqn').list} param0 | ||
* @returns {string} SQL | ||
*/ | ||
list({ list }) { | ||
@@ -554,2 +767,7 @@ return `(${list.map(e => this.expr(e))})` | ||
/** | ||
* Renders a Regular Expression into its string representation | ||
* @param {RegExp} o | ||
* @returns {string} SQL | ||
*/ | ||
regex(o) { | ||
@@ -559,6 +777,16 @@ if (is_regexp(o)) return o.source | ||
/** | ||
* Renders the object as a JSON string in generic SQL | ||
* @param {object} o | ||
* @returns {string} SQL | ||
*/ | ||
json(o) { | ||
return JSON.stringify(o) | ||
return this.string(JSON.stringify(o)) | ||
} | ||
/** | ||
* Renders a javascript string into a generic SQL string | ||
* @param {string} s | ||
* @returns {string} SQL | ||
*/ | ||
string(s) { | ||
@@ -568,6 +796,18 @@ return `'${s.replace(/'/g, "''")}'` | ||
/** | ||
* Calculates the effect column name | ||
* @param {import('./infer/cqn').col} col | ||
* @returns {string} explicit/implicit column alias | ||
*/ | ||
column_name(col) { | ||
if (col === '*') | ||
cds.error`Query was not inferred and includes '*' in the columns. For which there is no column name available.` | ||
return (typeof col.as === 'string' && col.as) || ('val' in col && col.val + '') || col.ref[col.ref.length - 1] | ||
} | ||
/** | ||
* Calculates the Database name of the given name | ||
* @param {string|import('./infer/cqn').ref} name | ||
* @returns {string} Database name | ||
*/ | ||
name(name) { | ||
@@ -577,10 +817,23 @@ return (name.id || name).replace(/\./g, '_') | ||
/** @type {unknown} */ | ||
static ReservedWords = {} | ||
/** | ||
* Ensures that the given identifier is properly quoted when required by the database | ||
* @param {string} s | ||
* @returns {string} SQL | ||
*/ | ||
quote(s) { | ||
if (typeof s !== 'string') return '"' + s + '"' | ||
if (s.includes('"')) return '"' + s.replace(/"/g, '""') + '"' | ||
if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' @./\\]/.test(s)) return '"' + s + '"' | ||
if (s.toUpperCase() in this.class.ReservedWords || /^\d|[$' ?@./\\]/.test(s)) return '"' + s + '"' | ||
return s | ||
} | ||
/** | ||
* Convers the columns array into an array of SQL expressions that extract the correct value from inserted JSON data | ||
* @param {object[]} columns | ||
* @param {import('./infer/cqn').elements} elements | ||
* @param {Boolean} isUpdate | ||
* @returns {string[]} Array of SQL expressions for processing input JSON data | ||
*/ | ||
managed(columns, elements, isUpdate = false) { | ||
@@ -631,2 +884,7 @@ const annotation = isUpdate ? '@cds.on.update' : '@cds.on.insert' | ||
/** | ||
* Returns the default value | ||
* @param {string} defaultValue | ||
* @returns {string} | ||
*/ | ||
defaultValue(defaultValue = this.context.timestamp.toISOString()) { | ||
@@ -648,2 +906,9 @@ return typeof defaultValue === 'string' ? this.string(defaultValue) : defaultValue | ||
const _empty = a => !a || a.length === 0 | ||
module.exports = Object.assign((q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m), { class: CQN2SQLRenderer }) | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} q | ||
* @param {import('@sap/cds/apis/csn').CSN} m | ||
*/ | ||
module.exports = (q, m) => new CQN2SQLRenderer().render(cqn4sql(q, m), m) | ||
module.exports.class = CQN2SQLRenderer | ||
module.exports.classDefinition = CQN2SQLRenderer // class is a reserved typescript word |
@@ -7,2 +7,13 @@ const cds = require('@sap/cds') | ||
/** | ||
* @callback nextCallback | ||
* @param {Error|undefined} error | ||
* @returns {Promise<unknown>} | ||
*/ | ||
/** | ||
* @param {import('@sap/cds/apis/services').Request} req | ||
* @param {nextCallback} next | ||
* @returns {Promise<number>} | ||
*/ | ||
async function onDeep(req, next) { | ||
@@ -140,2 +151,6 @@ const { query } = req | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} query | ||
* @param {import('@sap/cds/apis/csn').Definition} target | ||
*/ | ||
const getExpandForDeep = (query, target) => { | ||
@@ -146,2 +161,3 @@ const from = query.DELETE?.from || query.UPDATE?.entity | ||
/** @type {import("@sap/cds/apis/ql").SELECT<unknown>} */ | ||
const cqn = SELECT.from(from) | ||
@@ -156,2 +172,8 @@ if (where) cqn.SELECT.where = where | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} query | ||
* @param {unknown[]} dbData | ||
* @param {import('@sap/cds/apis/csn').Definition} target | ||
* @returns | ||
*/ | ||
const getDeepQueries = (query, dbData, target) => { | ||
@@ -181,2 +203,7 @@ let queryData | ||
/** | ||
* @param {unknown[]} diff | ||
* @param {import('@sap/cds/apis/csn').Definition} target | ||
* @returns {import('@sap/cds/apis/cqn').Query[]} | ||
*/ | ||
const _getDeepQueries = (diff, target) => { | ||
@@ -183,0 +210,0 @@ const queries = [] |
@@ -44,2 +44,12 @@ const cds = require('@sap/cds') | ||
/** | ||
* @callback nextCallback | ||
* @param {Error|undefined} error | ||
* @returns {Promise<unknown>} | ||
*/ | ||
/** | ||
* @param {import('@sap/cds/apis/services').Request} req | ||
* @param {nextCallback} next | ||
*/ | ||
module.exports = async function fill_in_keys(req, next) { | ||
@@ -46,0 +56,0 @@ // REVISIT dummy handler until we have input processing |
@@ -7,3 +7,2 @@ 'use strict' | ||
const { pseudos } = require('./pseudos') | ||
// REVISIT: we should always return cds.linked elements | ||
const cdsTypes = cds.linked({ | ||
@@ -22,7 +21,6 @@ definitions: { | ||
for (const each in cdsTypes) cdsTypes[`cds.${each}`] = cdsTypes[each] | ||
/** | ||
* @param {CQN|CQL} originalQuery | ||
* @param {CSN} [model] | ||
* @returns {InferredCQN} = q with .target and .elements | ||
* @param {import('@sap/cds/apis/cqn').Query|string} originalQuery | ||
* @param {import('@sap/cds/apis/csn').CSN} [model] | ||
* @returns {import('./cqn').Query} = q with .target and .elements | ||
*/ | ||
@@ -45,2 +43,6 @@ function infer(originalQuery, model = cds.context?.model || cds.model) { | ||
inferred.STREAM | ||
// cache for already processed calculated elements | ||
const alreadySeenCalcElements = new Set() | ||
const sources = inferTarget(_.from || _.into || _.entity, {}) | ||
@@ -140,7 +142,7 @@ const joinTree = new JoinTree(sources) | ||
* | ||
* @param {Object} arg - The argument object that will be augmented with additional properties. | ||
* @param {object} arg - The argument object that will be augmented with additional properties. | ||
* It must contain a 'ref' property, which is an array representing the steps to be processed. | ||
* Optionally, it can also contain an 'xpr' property, which is also processed recursively. | ||
* | ||
* @param {Object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be | ||
* @param {object} $baseLink - Optional parameter. It represents the environment in which the first 'ref' step should be | ||
* resolved. It's needed for infix filter / expand columns. It must contain a 'definition' | ||
@@ -184,3 +186,3 @@ * property, which is an object representing the base environment. | ||
} | ||
arg.$refLinks.push({ definition: e, target: e._target || e }) | ||
arg.$refLinks.push({ definition: e, target: definition }) | ||
// filter paths are flattened | ||
@@ -196,3 +198,3 @@ // REVISIT: too much augmentation -> better remove flatName.. | ||
const recent = arg.$refLinks[i - 1] | ||
const { elements } = recent.target | ||
const { elements } = recent.definition._target || recent.definition | ||
const e = elements[id] | ||
@@ -225,2 +227,7 @@ if (!e) throw new Error(`"${id}" not found in the elements of "${arg.$refLinks[i - 1].definition.name}"`) | ||
}) | ||
const { definition, target } = arg.$refLinks[arg.$refLinks.length - 1] | ||
if (definition.value) { | ||
// nested calculated element | ||
attachRefLinksToArg(definition.value, { definition: definition.parent, target }, true) | ||
} | ||
} | ||
@@ -237,3 +244,3 @@ | ||
* | ||
* @returns {Object} The `$combinedElements` dictionary, which maps element names to an array of objects | ||
* @returns {object} The `$combinedElements` dictionary, which maps element names to an array of objects | ||
* containing the index and table alias where the element can be found. | ||
@@ -275,5 +282,5 @@ */ | ||
* | ||
* @param {Object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names | ||
* @param {object} $combinedElements The `$combinedElements` dictionary of the query, which maps element names | ||
* to an array of objects containing the index and table alias where the element can be found. | ||
* @returns {Object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions. | ||
* @returns {object} The inferred `elements` dictionary of the query, which maps element names to their corresponding definitions. | ||
*/ | ||
@@ -456,3 +463,3 @@ function inferQueryElements($combinedElements) { | ||
function inferQueryElement(column, insertIntoQueryElements = true, $baseLink = null, context) { | ||
const { inExists, inExpr, inNestedProjection } = context || {} | ||
const { inExists, inExpr, inNestedProjection, inCalcElement } = context || {} | ||
if (column.param) return // parameter references are only resolved into values on execution e.g. :val, :1 or ? | ||
@@ -499,3 +506,3 @@ if (column.args) column.args.forEach(arg => inferQueryElement(arg, false, $baseLink, context)) // e.g. function in expression | ||
const element = elements[id] | ||
if (!inExists && !inNestedProjection && element.target) { | ||
if (!inExists && !inNestedProjection && !inCalcElement && element.target) { | ||
// only fk access in infix filter | ||
@@ -514,3 +521,4 @@ const nextStep = column.ref[1]?.id || column.ref[1] | ||
} | ||
column.$refLinks.push({ definition: elements[id], target }) | ||
const resolvableIn = definition.target ? definition._target : target | ||
column.$refLinks.push({ definition: elements[id], target: resolvableIn }) | ||
} else { | ||
@@ -550,4 +558,5 @@ stepNotFoundInPredecessor(id, definition.name) | ||
const target = definition._target || column.$refLinks[i - 1].target | ||
if (element) { | ||
const $refLink = { definition: elements[id], target: column.$refLinks[i - 1].target } | ||
const $refLink = { definition: elements[id], target } | ||
column.$refLinks.push($refLink) | ||
@@ -558,3 +567,3 @@ } else if (firstStepIsSelf) { | ||
// `$user.some.unknown.element` -> no error | ||
column.$refLinks.push({ definition: {}, target: column.$refLinks[i - 1].target }) | ||
column.$refLinks.push({ definition: {}, target }) | ||
} else if (id === '$dummy') { | ||
@@ -644,3 +653,4 @@ // `some.known.element.$dummy` -> no error; used by cds.ql to simulate joins | ||
throw new Error(`Duplicate definition of element “${elementName}”`) | ||
queryElements[elementName] = getCopyWithAnnos(column, leafArt) | ||
const element = getCopyWithAnnos(column, leafArt) | ||
queryElements[elementName] = element | ||
} | ||
@@ -662,8 +672,16 @@ } | ||
} | ||
const virtual = (column.$refLinks[column.$refLinks.length - 1].definition.virtual || !isPersisted) && !inExpr | ||
const leafArt = column.$refLinks[column.$refLinks.length - 1].definition | ||
const virtual = (leafArt.virtual || !isPersisted) && !inExpr | ||
// check if we need to merge the column `ref` into the join tree of the query | ||
if (!inExists && !virtual && isColumnJoinRelevant(column, firstStepIsSelf)) { | ||
if (!inExists && !virtual && !inCalcElement && isColumnJoinRelevant(column, firstStepIsSelf)) { | ||
if (originalQuery.UPDATE) | ||
throw cds.error( | ||
'Path expressions for UPDATE statements are not supported. Use “where exists” with infix filters instead.', | ||
) | ||
Object.defineProperty(column, 'isJoinRelevant', { value: true }) | ||
joinTree.mergeColumn(column) | ||
joinTree.mergeColumn(column, $baseLink) | ||
} | ||
if (leafArt.value && !leafArt.value.stored) { | ||
resolveCalculatedElement(leafArt, column) | ||
} | ||
@@ -673,5 +691,5 @@ /** | ||
* | ||
* @param {Object} col - The column object with properties: `inline` and `$refLinks`. | ||
* @param {object} col - The column object with properties: `inline` and `$refLinks`. | ||
* @param {string} [namePrefix=col.as || col.flatName] - Prefix for naming new columns. Defaults to `col.as` or `col.flatName`. | ||
* @returns {Object} - An object with resolved and processed inline column definitions. | ||
* @returns {object} - An object with resolved and processed inline column definitions. | ||
* | ||
@@ -730,4 +748,4 @@ * Procedure: | ||
* | ||
* @param {Object} col - The column object with properties: `expand` and `$refLinks`. | ||
* @returns {Object} - A `cds.struct` object with expanded column definitions. | ||
* @param {object} col - The column object with properties: `expand` and `$refLinks`. | ||
* @returns {object} - A `cds.struct` object with expanded column definitions. | ||
* | ||
@@ -798,3 +816,55 @@ * Procedure: | ||
} | ||
function resolveCalculatedElement(calcElement) { | ||
if (alreadySeenCalcElements.has(calcElement)) return | ||
else alreadySeenCalcElements.add(calcElement) | ||
const { ref, xpr, func } = calcElement.value | ||
if (ref || xpr) { | ||
attachRefLinksToArg(calcElement.value, { definition: calcElement.parent, target: calcElement.parent }, true) | ||
// column is now fully linked, now we need to find out if we need to merge it into the join tree | ||
// for that, we calculate all paths from a calc element and merge them into the join tree | ||
mergePathsIntoJoinTree(calcElement.value) | ||
} | ||
if (func) calcElement.value.args?.forEach(arg => inferQueryElement(arg, false)) // {func}.args are optional | ||
function mergePathsIntoJoinTree(e, basePath = null) { | ||
basePath = basePath || { $refLinks: [], ref: [] } | ||
if (e.ref) { | ||
e.$refLinks.forEach((link, i) => { | ||
const { definition } = link | ||
if (!definition.value) { | ||
basePath.$refLinks.push(link) | ||
basePath.ref.push(e.ref[i]) | ||
} | ||
}) | ||
const leafOfCalculatedElementRef = e.$refLinks[e.$refLinks.length - 1].definition | ||
if (leafOfCalculatedElementRef.value) mergePathsIntoJoinTree(leafOfCalculatedElementRef.value, basePath) | ||
mergePathIfNecessary(basePath, e) | ||
} else if (e.xpr) { | ||
e.xpr.forEach(step => { | ||
if (step.ref) { | ||
const subPath = { $refLinks: [...basePath.$refLinks], ref: [...basePath.ref] } | ||
step.$refLinks.forEach((link, i) => { | ||
const { definition } = link | ||
if (definition.value) { | ||
mergePathsIntoJoinTree(definition.value) | ||
} else { | ||
subPath.$refLinks.push(link) | ||
subPath.ref.push(step.ref[i]) | ||
} | ||
}) | ||
mergePathIfNecessary(subPath, step) | ||
} | ||
}) | ||
} | ||
function mergePathIfNecessary(p, step) { | ||
const calcElementIsJoinRelevant = isColumnJoinRelevant(p) | ||
if (calcElementIsJoinRelevant) { | ||
if (!calcElement.value.isColumnJoinRelevant) Object.defineProperty(step, 'isJoinRelevant', { value: true }) | ||
joinTree.mergeColumn(p) | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
@@ -850,6 +920,12 @@ * Checks whether or not the `ref` of the given column is join relevant. | ||
function inferElementsFromWildCard() { | ||
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false | ||
if (Object.keys(queryElements).length === 0 && aliases.length === 1) { | ||
// only one query source and no overwritten columns | ||
Object.entries(sources[aliases[0]].elements).forEach(([name, element]) => { | ||
if (element.type !== 'cds.LargeBinary') queryElements[name] = element | ||
if (!exclude(name) && element.type !== 'cds.LargeBinary') queryElements[name] = element | ||
if (element.value) { | ||
// we might have join relevant calculated elements | ||
resolveCalculatedElement(element) | ||
} | ||
}) | ||
@@ -859,3 +935,2 @@ return | ||
const exclude = _.excluding ? x => _.excluding.includes(x) : () => false | ||
const ambiguousElements = {} | ||
@@ -862,0 +937,0 @@ Object.entries($combinedElements).forEach(([name, tableAliases]) => { |
'use strict' | ||
// REVISIT: define following unknown types | ||
/** | ||
* @typedef {unknown} $refLink | ||
*/ | ||
/** | ||
* @typedef {unknown} parent | ||
*/ | ||
/** | ||
* @typedef {unknown} where | ||
*/ | ||
/** | ||
* @typedef {unknown} children | ||
*/ | ||
/** | ||
* @typedef {unknown} queryArtifact | ||
*/ | ||
/** | ||
* @typedef {string} alias | ||
*/ | ||
/** | ||
* @typedef {Map<alias,Root>} _roots | ||
*/ | ||
/** | ||
* @typedef {Object.<string, unknown>} sources | ||
*/ | ||
/** | ||
* A class representing a Node in the join tree. | ||
* | ||
* @property {$refLink} - A reference link to this node. | ||
* @property {parent} - The parent Node of this node. | ||
* @property {where} - An optional condition to be applied to this node. | ||
* @property {children} - A Map of children nodes belonging to this node. | ||
*/ | ||
class Node { | ||
/** | ||
* @param {$refLink} $refLink | ||
* @param {parent} parent | ||
* @param {where} where | ||
*/ | ||
constructor($refLink, parent, where = null) { | ||
/** @type {$refLink} - A reference link to this node. */ | ||
this.$refLink = $refLink | ||
/** @type {parent} - The parent Node of this node. */ | ||
this.parent = parent | ||
/** @type {where} - An optional condition to be applied to this node. */ | ||
this.where = where | ||
/** @type {children} - A Map of children nodes belonging to this node. */ | ||
this.children = new Map() | ||
@@ -22,14 +60,16 @@ } | ||
* A class representing the root of the join tree. | ||
* | ||
* @property {queryArtifact} - The artifact used to make the query. | ||
* @property {alias} - The alias of the artifact. | ||
* @property {parent} - The parent Node of this root, null for the root Node. | ||
* @property {children} - A Map of children nodes belonging to this root. | ||
*/ | ||
class Root { | ||
/** | ||
* @param {[alias, queryArtifact]} querySource | ||
*/ | ||
constructor(querySource) { | ||
const [alias, queryArtifact] = querySource | ||
/** @type {queryArtifact} - The artifact used to make the query. */ | ||
this.queryArtifact = queryArtifact | ||
/** @type {alias} - The alias of the artifact. */ | ||
this.alias = alias | ||
/** @type {parent} - The parent Node of this root, null for the root Node. */ | ||
this.parent = null | ||
/** @type {children} - A Map of children nodes belonging to this root. */ | ||
this.children = new Map() | ||
@@ -41,10 +81,12 @@ } | ||
* A class representing a Join Tree. | ||
* | ||
* @property {_roots} - A Map of root nodes. | ||
* @property {isInitial} - A boolean indicating if the join tree is in its initial state. | ||
* @property {_queryAliases} - A Map of query aliases, which is used during the association to join translation. | ||
*/ | ||
class JoinTree { | ||
/** | ||
* | ||
* @param {sources} sources | ||
*/ | ||
constructor(sources) { | ||
/** @type {_roots} - A Map of root nodes. */ | ||
this._roots = new Map() | ||
/** @type {boolean} - A boolean indicating if the join tree is in its initial state. */ | ||
this.isInitial = true | ||
@@ -58,2 +100,3 @@ /** | ||
* table alias entry, is the capitalized version of the alias. | ||
* @type {Map<string, string>} | ||
*/ | ||
@@ -88,2 +131,3 @@ this._queryAliases = new Map() | ||
* @param {string} alias - The original alias name. | ||
* @param {unknown[]} outerQueries - An array of outer queries. | ||
* @returns {string} - The next unambiguous table alias. | ||
@@ -114,3 +158,3 @@ */ | ||
* | ||
* @param {Object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref. | ||
* @param {object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref. | ||
* @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree. | ||
@@ -153,4 +197,5 @@ */ | ||
if (child.$refLink.definition.isAssociation) { | ||
if (child.where) { | ||
// always join relevant | ||
if (child.where || 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 | ||
@@ -157,0 +202,0 @@ } else { |
@@ -15,2 +15,6 @@ const iterator = Symbol.iterator | ||
module.exports = class InsertResult { | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').INSERT} query | ||
* @param {unknown[]} results | ||
*/ | ||
constructor(query, results) { | ||
@@ -21,3 +25,3 @@ this.query = query | ||
/* | ||
/** | ||
* Lazy access to auto-generated keys. | ||
@@ -75,4 +79,5 @@ */ | ||
/* | ||
/** | ||
* the number of inserted (root) entries or the number of affectedRows in case of INSERT into SELECT | ||
* @return {number} | ||
*/ | ||
@@ -85,4 +90,5 @@ get affectedRows() { | ||
/* | ||
/** | ||
* for checks such as res > 2 | ||
* @return {number} | ||
*/ | ||
@@ -93,5 +99,16 @@ valueOf() { | ||
/** | ||
* The last id of the auto incremented key column | ||
* @param {unknown[]} result | ||
* @returns {number} | ||
*/ | ||
insertedRowId4(result) { | ||
return result.lastID | ||
} | ||
/** | ||
* Number of affected rows | ||
* @param {unknown[]} result | ||
* @returns {number} | ||
*/ | ||
affectedRows4(result) { | ||
@@ -98,0 +115,0 @@ return result.changes |
@@ -7,2 +7,15 @@ const cds = require('@sap/cds/lib'), | ||
/** | ||
* @callback next | ||
* @param {Error} param0 | ||
* @returns {Promise<unknown>} | ||
*/ | ||
/** | ||
* @callback Handler | ||
* @param {import('@sap/cds/apis/services').Request} param0 | ||
* @param {next} param1 | ||
* @returns {Promise<unknown>} | ||
*/ | ||
class SQLService extends DatabaseService { | ||
@@ -25,2 +38,3 @@ init() { | ||
/** @type {Handler} */ | ||
async transformStreamFromCQN({ query }, next) { | ||
@@ -34,2 +48,3 @@ if (!query._streaming) return next() | ||
/** @type {Handler} */ | ||
async transformStreamIntoCQN({ query, data, target }, next) { | ||
@@ -62,7 +77,7 @@ let col, type, etag | ||
/** Handler for SELECT */ | ||
/** | ||
* Handler for SELECT | ||
* @type {Handler} | ||
*/ | ||
async onSELECT({ query, data }) { | ||
// REVISIT: disable this for queries like (SELECT 1) | ||
// Will return multiple rows with objects inside | ||
query.SELECT.expand = 'root' | ||
const { sql, values, cqn } = this.cqn2sql(query, data) | ||
@@ -77,2 +92,6 @@ let ps = await this.prepare(sql) | ||
/** | ||
* Handler for INSERT | ||
* @type {Handler} | ||
*/ | ||
async onINSERT({ query, data }) { | ||
@@ -86,2 +105,6 @@ const { sql, entries, cqn } = this.cqn2sql(query, data) | ||
/** | ||
* Handler for UPSERT | ||
* @type {Handler} | ||
*/ | ||
async onUPSERT({ query, data }) { | ||
@@ -95,9 +118,21 @@ const { sql, entries } = this.cqn2sql(query, data) | ||
/** Handler for UPDATE */ | ||
/** | ||
* Handler for UPDATE | ||
* @type {Handler} | ||
*/ | ||
async onUPDATE(req) { | ||
if (!req.query.UPDATE.data && !req.query.UPDATE.with) return 0 | ||
// noop if not a touch for @cds.on.update | ||
if ( | ||
!req.query.UPDATE.data && | ||
!req.query.UPDATE.with && | ||
!Object.values(req.target?.elements || {}).some(e => e['@cds.on.update']) | ||
) | ||
return 0 | ||
return this.onSIMPLE(req) | ||
} | ||
/** Handler for Stream */ | ||
/** | ||
* Handler for Stream | ||
* @type {Handler} | ||
*/ | ||
async onSTREAM(req) { | ||
@@ -121,3 +156,6 @@ const { sql, values, entries } = this.cqn2sql(req.query) | ||
/** Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN */ | ||
/** | ||
* Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN | ||
* @type {Handler} | ||
*/ | ||
async onSIMPLE({ query, data }) { | ||
@@ -129,3 +167,6 @@ const { sql, values } = this.cqn2sql(query, data) | ||
/** Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN */ | ||
/** | ||
* Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN | ||
* @type {Handler} | ||
*/ | ||
async onEVENT({ event }) { | ||
@@ -136,3 +177,6 @@ DEBUG?.(event) // in the other cases above DEBUG happens in cqn2sql | ||
/** Handler for SQL statements which don't have any CQN */ | ||
/** | ||
* Handler for SQL statements which don't have any CQN | ||
* @type {Handler} | ||
*/ | ||
async onPlainSQL({ query, data }, next) { | ||
@@ -148,8 +192,16 @@ if (typeof query === 'string') { | ||
/** Override in subclasses to detect more statements to be called with ps.all() */ | ||
/** | ||
* Override in subclasses to detect more statements to be called with ps.all() | ||
* @param {string} sql | ||
*/ | ||
hasResults(sql) { | ||
return /^(SELECT|WITH|CALL|PRAGMA table_info)/.test(sql) | ||
return /^(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql) | ||
} | ||
/** Derives and executes a query to fill in `$count` for given query */ | ||
/** | ||
* Derives and executes a query to fill in `$count` for given query | ||
* @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN | ||
* @param {unknown[]} ret - Results of the original query | ||
* @returns {Promise<number>} | ||
*/ | ||
async count(query, ret) { | ||
@@ -182,11 +234,25 @@ if (ret) { | ||
static CQN2SQL = require('./cqn2sql').class | ||
constructor() { | ||
super(...arguments) | ||
/** @param {unknown[]} args */ | ||
constructor(...args) { | ||
super(...args) | ||
/** @type {unknown} */ | ||
this.class = new.target // for IntelliSense | ||
} | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} q | ||
* @param {unknown} values | ||
* @returns {typeof SQLService.CQN2SQL} | ||
*/ | ||
cqn2sql(q, values) { | ||
const cqn = this.cqn4sql(q) | ||
// REVISIT: disable this for queries like (SELECT 1) | ||
// Will return multiple rows with objects inside | ||
// Only enable expand when the query is inferred | ||
if (cqn.SELECT && cqn.elements) cqn.SELECT.expand = cqn.SELECT.expand ?? 'root' | ||
const cmd = cqn.cmd || Object.keys(cqn)[0] | ||
if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) { | ||
if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || cqn.STREAM?.into) { | ||
let resolvedCqn = resolveView(cqn, this.model, this) | ||
@@ -201,2 +267,7 @@ if (resolvedCqn && resolvedCqn[cmd]._transitions?.[0].target) { | ||
} | ||
/** | ||
* @param {import('@sap/cds/apis/cqn').Query} q | ||
* @returns {import('./infer/cqn').Query} | ||
*/ | ||
cqn4sql(q) { | ||
@@ -220,5 +291,7 @@ // REVISIT: move this check to cqn4sql? | ||
* Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK | ||
* @param {string} sql | ||
* @returns {Promise<unknown>} The result of the query | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
async exec(sql) { | ||
sql | ||
throw '2b overridden by subclass' | ||
@@ -228,24 +301,34 @@ } | ||
/** Interface of prepared statement objects as returned by {@link SQLService#prepare} */ | ||
/** | ||
* Interface of prepared statement objects as returned by {@link SQLService#prepare} | ||
* @class | ||
* @interface | ||
*/ | ||
class PreparedStatement { | ||
// eslint-disable-line no-unused-vars | ||
/** | ||
* Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP | ||
* @param {[]|{}} binding_params | ||
* @param {unknown|unknown[]} binding_params | ||
*/ | ||
async run(/*binding_params*/) {} // eslint-disable-line no-unused-vars | ||
async run(binding_params) { | ||
binding_params | ||
return 0 | ||
} | ||
/** | ||
* Executes a prepared SELECT query and returns a single/first row only | ||
* @param {[]|{}} binding_params | ||
* @param {unknown|unknown[]} binding_params | ||
* @returns {Promise<unknown>} | ||
*/ | ||
async get(/*binding_params*/) { | ||
async get(binding_params) { | ||
binding_params | ||
return {} | ||
} // eslint-disable-line no-unused-vars | ||
} | ||
/** | ||
* Executes a prepared SELECT query and returns an array of all rows | ||
* @param {[]|{}} binding_params | ||
* @param {unknown|unknown[]} binding_params | ||
* @returns {Promise<unknown[]>} | ||
*/ | ||
async all(/*binding_params*/) { | ||
async all(binding_params) { | ||
binding_params | ||
return [{}] | ||
} // eslint-disable-line no-unused-vars | ||
} | ||
} | ||
@@ -303,2 +386,3 @@ SQLService.prototype.PreparedStatement = PreparedStatement | ||
module.exports = Object.assign(SQLService, { _target_name4 }) | ||
Object.assign(SQLService, { _target_name4 }) | ||
module.exports = SQLService |
{ | ||
"name": "@cap-js/db-service", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "CDS base database service", | ||
"homepage": "https://cap.cloud.sap/", | ||
"homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/cap-js/cds-dbs" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/cap-js/cds-dbs/issues" | ||
}, | ||
"keywords": [ | ||
@@ -12,2 +19,3 @@ "CAP", | ||
"main": "index.js", | ||
"types": "./dist/index.d.ts", | ||
"files": [ | ||
@@ -23,10 +31,14 @@ "lib", | ||
"prettier": "npx prettier --write .", | ||
"test": "npx jest --silent", | ||
"test": "npm run build && npx jest --silent", | ||
"build": "tsc && find lib/ -type f -name '*.d.ts' -exec cp '{}' 'dist/{}' ';' && cp ts.eslintrc.cjs dist/.eslintrc.cjs && npx eslint ./dist --ext .d.ts", | ||
"lint": "npx eslint . && npx prettier --check . " | ||
}, | ||
"dependencies": {}, | ||
"peerDependencies": { | ||
"@sap/cds": ">=7" | ||
"@sap/cds": ">=7.1.1" | ||
}, | ||
"license": "SEE LICENSE" | ||
"license": "SEE LICENSE", | ||
"devDependencies": { | ||
"@typescript-eslint/eslint-plugin": "^6.2.0", | ||
"typescript": "^5.1.6" | ||
} | ||
} |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
229125
19
5190
0
1
2