@cap-js/db-service 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -2
- package/index.js +16 -2
- package/lib/InsertResults.js +22 -4
- package/lib/SQLService.js +169 -73
- package/lib/common/DatabaseService.js +93 -88
- package/lib/common/factory.d.ts +5 -0
- package/lib/common/generic-pool.js +34 -0
- package/lib/common/session-context.js +32 -0
- package/lib/converters.d.ts +24 -0
- package/lib/cql-functions.js +205 -5
- package/lib/cqn2sql.js +463 -154
- package/lib/cqn4sql.js +176 -71
- package/lib/deep-queries.js +31 -3
- package/lib/fill-in-keys.js +15 -4
- package/lib/infer/cqn.d.ts +45 -0
- package/lib/infer/index.js +128 -31
- package/lib/infer/join-tree.js +64 -19
- package/package.json +17 -8
|
@@ -1,143 +1,148 @@
|
|
|
1
|
+
const SessionContext = require('./session-context')
|
|
2
|
+
const ConnectionPool = require('./generic-pool')
|
|
1
3
|
const infer = require('../infer')
|
|
2
|
-
const cds = require('@sap/cds')
|
|
4
|
+
const cds = require('@sap/cds/lib')
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
const pool = createPool({ __proto__: factory, create: factory.create.bind(undefined, tenant) }, factory.options)
|
|
6
|
-
pool._trackedConnections = []
|
|
7
|
-
return pool
|
|
8
|
-
}
|
|
9
|
-
const { createPool } = require('@sap/cds-foss').pool
|
|
6
|
+
/** @typedef {unknown} DatabaseDriver */
|
|
10
7
|
|
|
11
8
|
class DatabaseService extends cds.Service {
|
|
9
|
+
/**
|
|
10
|
+
* Dictionary of connection pools per tenant
|
|
11
|
+
*/
|
|
12
|
+
pools = { _factory: this.factory }
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Return a pool factory + options property as expected by
|
|
14
16
|
* https://github.com/coopernurse/node-pool#createpool.
|
|
17
|
+
* @abstract
|
|
18
|
+
* @type {import('./factory').Factory<DatabaseDriver>}
|
|
15
19
|
*/
|
|
16
20
|
get factory() {
|
|
17
21
|
throw '2b overriden in subclass'
|
|
18
22
|
}
|
|
19
|
-
pools = { _factory: this.factory }
|
|
20
|
-
|
|
21
|
-
get isMultitenant() {
|
|
22
|
-
return 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
|
|
23
|
-
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* Set one or more session context variables like so:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* ```
|
|
26
|
+
*
|
|
27
|
+
* const tx = cds.db.tx()
|
|
28
|
+
* tx.set({ foo: 'bar' })
|
|
29
|
+
*
|
|
30
|
+
* This is used in this.begin() for standard properties
|
|
31
|
+
* like `$user.id` or `$user.locale`.
|
|
34
32
|
*/
|
|
35
33
|
// eslint-disable-next-line no-unused-vars
|
|
36
34
|
set(variables) {
|
|
37
35
|
throw '2b overridden by subclass'
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Acquires a pooled connection and starts a session, including setting
|
|
40
|
+
* session context like `$user.id` or `$user.locale`, and starting a
|
|
41
|
+
* transaction with `BEGIN`
|
|
42
|
+
* @returns this
|
|
43
|
+
*/
|
|
44
44
|
async begin() {
|
|
45
|
+
// We expect tx.begin() being called for an txed db service
|
|
45
46
|
const ctx = this.context
|
|
46
|
-
if (!ctx) return this.tx().begin()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
connections.push(this)
|
|
60
|
-
this._release = async dbc => {
|
|
61
|
-
await pool.release(dbc)
|
|
62
|
-
connections.splice(connections.indexOf(this), 1)
|
|
63
|
-
}
|
|
47
|
+
if (!ctx) return this.tx().begin() // REVISIT: Is this correct? When does this happen?
|
|
48
|
+
|
|
49
|
+
// REVISIT: tenant should be undefined if !this.isMultitenant
|
|
50
|
+
let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
|
|
51
|
+
let tenant = isMultitenant && ctx.tenant
|
|
52
|
+
|
|
53
|
+
// Setting this.pool as used in this.acquire() and this.release()
|
|
54
|
+
this.pool = this.pools[tenant] ??= new ConnectionPool(this.pools._factory, tenant)
|
|
55
|
+
|
|
56
|
+
// Acquire a pooled connection
|
|
57
|
+
this.dbc = await this.acquire()
|
|
58
|
+
|
|
59
|
+
// Begin a session...
|
|
64
60
|
try {
|
|
65
|
-
|
|
66
|
-
await this.set({
|
|
67
|
-
get '$user.id'() {
|
|
68
|
-
return _set(this, '$user.id', ctx.user?.id || 'anonymous')
|
|
69
|
-
},
|
|
70
|
-
get '$user.locale'() {
|
|
71
|
-
return _set(this, '$user.locale', ctx.locale || cds.env.i18n.default_language)
|
|
72
|
-
},
|
|
73
|
-
get '$valid.from'() {
|
|
74
|
-
return _set(this, '$valid.from', ctx._?.['VALID-FROM'] ?? ctx._?.['VALID-AT'] ?? '1970-01-01T00:00:00.000Z')
|
|
75
|
-
},
|
|
76
|
-
get '$valid.to'() {
|
|
77
|
-
return _set(
|
|
78
|
-
this,
|
|
79
|
-
'$valid.to',
|
|
80
|
-
ctx._?.['VALID-TO'] ?? _validTo4(ctx._?.['VALID-AT']) ?? '9999-11-11T22:22:22.000Z',
|
|
81
|
-
)
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
// Run BEGIN
|
|
61
|
+
await this.set(new SessionContext(ctx))
|
|
85
62
|
await this.send('BEGIN')
|
|
86
63
|
} catch (e) {
|
|
87
|
-
this.
|
|
64
|
+
this.release()
|
|
88
65
|
throw e
|
|
89
66
|
}
|
|
90
67
|
return this
|
|
91
68
|
}
|
|
92
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Commits a transaction and releases the connection to the pool.
|
|
72
|
+
*/
|
|
93
73
|
async commit() {
|
|
94
|
-
|
|
95
|
-
if (!dbc) return
|
|
74
|
+
if (!this.dbc) return
|
|
96
75
|
await this.send('COMMIT')
|
|
97
|
-
this.
|
|
76
|
+
this.release() // only release on successful commit as otherwise released on rollback
|
|
98
77
|
}
|
|
99
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Rolls back a transaction and releases the connection to the pool.
|
|
81
|
+
*/
|
|
100
82
|
async rollback() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
83
|
+
if (!this.dbc) return
|
|
84
|
+
else
|
|
85
|
+
try {
|
|
86
|
+
await this.send('ROLLBACK')
|
|
87
|
+
} finally {
|
|
88
|
+
this.release()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Acquires a connection from this.pool, stored into this.dbc
|
|
94
|
+
* This is for subclasses to intercept, if required.
|
|
95
|
+
*/
|
|
96
|
+
async acquire() {
|
|
97
|
+
return await this.pool.acquire()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Releases own connection, i.e. tix.dbc, from this.pool
|
|
102
|
+
* This is for subclasses to intercept, if required.
|
|
103
|
+
*/
|
|
104
|
+
async release() {
|
|
105
|
+
return this.pool.release(this.dbc)
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
// REVISIT: should happen automatically after a configurable time
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} tenant
|
|
111
|
+
*/
|
|
111
112
|
async disconnect(tenant) {
|
|
112
113
|
const pool = this.pools[tenant]
|
|
113
|
-
if (pool)
|
|
114
|
-
else return
|
|
114
|
+
if (!pool) return
|
|
115
115
|
await pool.drain()
|
|
116
116
|
await pool.clear()
|
|
117
|
+
delete this.pools[tenant]
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Infers the given query with this DatabaseService instance's model.
|
|
122
|
+
* In general `this.model` is the same then `cds.model`
|
|
123
|
+
* @param {CQN} query - the query to infer
|
|
124
|
+
* @returns {CQN} the inferred query
|
|
125
|
+
*/
|
|
126
|
+
infer(query) {
|
|
127
|
+
return infer(query, this.model)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* DatabaseServices also support passing native query strings to underlying databases.
|
|
132
|
+
*/
|
|
119
133
|
run(query, data, ...etc) {
|
|
120
134
|
// Allow db.run('...',1,2,3,4)
|
|
121
135
|
if (data !== undefined && typeof query === 'string' && typeof data !== 'object') data = [data, ...etc]
|
|
122
136
|
return super.run(query, data)
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @returns {string} A url-like string used to print log output,
|
|
141
|
+
* e.g., in cds.deploy()
|
|
142
|
+
*/
|
|
125
143
|
url4(/*tenant*/) {
|
|
126
|
-
|
|
127
|
-
let { url } = this.options?.credentials || this.options || {}
|
|
128
|
-
return url
|
|
144
|
+
return this.options.credentials?.url || this.options.url
|
|
129
145
|
}
|
|
130
|
-
getDbUrl(tenant) {
|
|
131
|
-
return this.url4(tenant)
|
|
132
|
-
} // REVISIT: Remove after cds v6.7
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const _set = (context, variable, value) => {
|
|
136
|
-
Object.defineProperty(context, variable, { value, configurable: true })
|
|
137
|
-
return value
|
|
138
|
-
}
|
|
139
|
-
const _validTo4 = validAt => {
|
|
140
|
-
return validAt?.replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || '')
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
DatabaseService.prototype.isDatabaseService = true
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { createPool } = require('@sap/cds-foss').pool
|
|
2
|
+
|
|
3
|
+
class ConnectionPool {
|
|
4
|
+
constructor(factory, tenant) {
|
|
5
|
+
let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
|
|
6
|
+
return _track_connections4(createPool(bound_factory, factory.options))
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// REVISIT: Is that really neccessary ?!
|
|
11
|
+
function _track_connections4(pool) {
|
|
12
|
+
const { acquire, release } = pool
|
|
13
|
+
return Object.assign(pool, {
|
|
14
|
+
async acquire() {
|
|
15
|
+
const connections = (this._trackedConnections ??= new Set())
|
|
16
|
+
try {
|
|
17
|
+
let dbc = await acquire.call(this)
|
|
18
|
+
connections.add((dbc._beginStack = new Error('begin called from:')))
|
|
19
|
+
return dbc
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// TODO: add acquire timeout error check
|
|
22
|
+
err.stack += `\nActive connections:${connections.size}\n${[...connections].map(e => e.stack).join('\n')}`
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
release(dbc) {
|
|
28
|
+
this._trackedConnections?.delete(dbc._beginStack)
|
|
29
|
+
return release.call(this, dbc)
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = ConnectionPool
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const cds = require('@sap/cds/lib')
|
|
2
|
+
|
|
3
|
+
class SessionContext {
|
|
4
|
+
constructor(ctx) {
|
|
5
|
+
Object.defineProperty(this, 'ctx', { value: ctx })
|
|
6
|
+
}
|
|
7
|
+
get '$user.id'() {
|
|
8
|
+
return (super['$user.id'] = this.ctx.user?.id || 'anonymous')
|
|
9
|
+
}
|
|
10
|
+
get '$user.locale'() {
|
|
11
|
+
return (super['$user.locale'] = this.ctx.locale || cds.env.i18n.default_language)
|
|
12
|
+
}
|
|
13
|
+
// REVISIT: should be decided in spec meeting for definitive name
|
|
14
|
+
get $now() {
|
|
15
|
+
return (super.$now = (this.ctx.timestamp || new Date()).toISOString())
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class TemporalSessionContext extends SessionContext {
|
|
20
|
+
get '$valid.from'() {
|
|
21
|
+
return (super['$valid.from'] = this.ctx._?.['VALID-FROM'] ?? this.ctx._?.['VALID-AT'] ?? new Date().toISOString())
|
|
22
|
+
}
|
|
23
|
+
get '$valid.to'() {
|
|
24
|
+
return (super['$valid.to'] =
|
|
25
|
+
this.ctx._?.['VALID-TO'] ??
|
|
26
|
+
this.ctx._?.['VALID-AT']?.replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || '') ??
|
|
27
|
+
new Date().toISOString().replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || ''))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// REVISIT: only set temporal context if required!
|
|
32
|
+
module.exports = TemporalSessionContext
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare function ConverterFunction(expression: string): string
|
|
2
|
+
export type Converter = typeof ConverterFunction
|
|
3
|
+
|
|
4
|
+
export type Converters = {
|
|
5
|
+
UUID: Converter
|
|
6
|
+
String: Converter
|
|
7
|
+
LargeString: Converter
|
|
8
|
+
Binary: Converter
|
|
9
|
+
LargeBinary: Converter
|
|
10
|
+
Boolean: Converter
|
|
11
|
+
Integer: Converter
|
|
12
|
+
UInt8: Converter
|
|
13
|
+
Int16: Converter
|
|
14
|
+
Int32: Converter
|
|
15
|
+
Int64: Converter
|
|
16
|
+
Float: Converter
|
|
17
|
+
Double: Converter
|
|
18
|
+
Decimal: Converter
|
|
19
|
+
DecimalFloat: Converter
|
|
20
|
+
Date: Converter
|
|
21
|
+
Time: Converter
|
|
22
|
+
DateTime: Converter
|
|
23
|
+
Timestamp: Converter
|
|
24
|
+
}
|
package/lib/cql-functions.js
CHANGED
|
@@ -2,49 +2,202 @@ const StandardFunctions = {
|
|
|
2
2
|
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
3
3
|
|
|
4
4
|
// String and Collection Functions
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Generates SQL statement that produces the length of a given string
|
|
7
|
+
* @param {string} x
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
length: x => `length(${x})`,
|
|
11
|
+
/**
|
|
12
|
+
* Generates SQL statement that produces the average of a given expression
|
|
13
|
+
* @param {string} x
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
6
16
|
average: x => `avg(${x})`,
|
|
17
|
+
/**
|
|
18
|
+
* Generates SQL statement that produces a boolean value indicating whether the search term is contained in the given columns
|
|
19
|
+
* @param {string} ref
|
|
20
|
+
* @param {string} arg
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
7
23
|
search: function (ref, arg) {
|
|
8
24
|
if (!('val' in arg)) throw `SQLite only supports single value arguments for $search`
|
|
9
25
|
const refs = ref.list || [ref],
|
|
10
26
|
{ toString } = ref
|
|
11
27
|
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
|
|
12
28
|
},
|
|
13
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Generates SQL statement that produces a string with all provided strings concatenated
|
|
31
|
+
* @param {...string} args
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
concat: (...args) => args.map(a => a.xpr ? `(${a})` : a).join(' || '),
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string contains the second string
|
|
38
|
+
* @param {...string} args
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
14
41
|
contains: (...args) => `ifnull(instr(${args}),0)`,
|
|
42
|
+
/**
|
|
43
|
+
* Generates SQL statement that produces the number of elements in a given collection
|
|
44
|
+
* @param {string} x
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
15
47
|
count: x => `count(${x || '*'})`,
|
|
48
|
+
/**
|
|
49
|
+
* Generates SQL statement that produces the number of distinct values of a given expression
|
|
50
|
+
* @param {string} x
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
16
53
|
countdistinct: x => `count(distinct ${x || '*'})`,
|
|
54
|
+
/**
|
|
55
|
+
* Generates SQL statement that produces the index of the first occurrence of the second string in the first string
|
|
56
|
+
* @param {string} x
|
|
57
|
+
* @param {string} y
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
17
60
|
indexof: (x, y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
|
61
|
+
/**
|
|
62
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string starts with the second string
|
|
63
|
+
* @param {string} x
|
|
64
|
+
* @param {string} y
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
18
67
|
startswith: (x, y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
|
19
68
|
// takes the end of the string of the size of the target and compares it with the target
|
|
69
|
+
/**
|
|
70
|
+
* Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string
|
|
71
|
+
* @param {string} x
|
|
72
|
+
* @param {string} y
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
20
75
|
endswith: (x, y) => `substr(${x}, length(${x}) + 1 - length(${y})) = ${y}`,
|
|
76
|
+
/**
|
|
77
|
+
* Generates SQL statement that produces the substring of a given string
|
|
78
|
+
* @example
|
|
79
|
+
* // returns 'bc'
|
|
80
|
+
* {func:'substring',args:[{val:'abc'},{val:1}]}
|
|
81
|
+
* @example
|
|
82
|
+
* // returns 'b'
|
|
83
|
+
* {func:'substring',args:[{val:'abc'},{val:1},{val:1}]}
|
|
84
|
+
* @param {string} x
|
|
85
|
+
* @param {string} y
|
|
86
|
+
* @param {string} z
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
21
89
|
substring: (x, y, z) =>
|
|
22
90
|
z
|
|
23
91
|
? `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z} )`
|
|
24
92
|
: `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end )`,
|
|
25
93
|
|
|
26
94
|
// String Functions
|
|
95
|
+
/**
|
|
96
|
+
* Generates SQL statement that matches the given string against a regular expression
|
|
97
|
+
* @param {string} x
|
|
98
|
+
* @param {string} y
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
27
101
|
matchesPattern: (x, y) => `${x} regexp ${y})`,
|
|
102
|
+
/**
|
|
103
|
+
* Generates SQL statement that produces the lower case value of a given string
|
|
104
|
+
* @param {string} x
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
28
107
|
tolower: x => `lower(${x})`,
|
|
108
|
+
/**
|
|
109
|
+
* Generates SQL statement that produces the upper case value of a given string
|
|
110
|
+
* @param {string} x
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
29
113
|
toupper: x => `upper(${x})`,
|
|
30
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Generates SQL statement that produces the trimmed value of a given string
|
|
116
|
+
* @param {string} x
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
trim: x => `trim(${x})`,
|
|
31
120
|
|
|
32
121
|
// Arithmetic Functions
|
|
122
|
+
/**
|
|
123
|
+
* Generates SQL statement that produces the rounded up value of a given number
|
|
124
|
+
* @param {string} x
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
33
127
|
ceiling: x => `ceil(${x})`,
|
|
34
|
-
|
|
35
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Generates SQL statement that produces the rounded down value of a given number
|
|
130
|
+
* @param {string} x
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
floor: x => `floor(${x})`,
|
|
134
|
+
/**
|
|
135
|
+
* Generates SQL statement that produces the rounded value of a given number
|
|
136
|
+
* @param {string} x
|
|
137
|
+
* @param {string} p precision
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
round: (x, p) => `round(${x}${p ? `,${p}` : ''})`,
|
|
36
141
|
|
|
37
142
|
// Date and Time Functions
|
|
143
|
+
|
|
144
|
+
current_date: p => p ? `current_date(${p})`: 'current_date',
|
|
145
|
+
current_time: p => p ? `current_time(${p})`: 'current_time',
|
|
146
|
+
current_timestamp: p => p ? `current_timestamp(${p})`: 'current_timestamp',
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generates SQL statement that produces the year of a given timestamp
|
|
150
|
+
* @param {string} x
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
38
153
|
year: x => `cast( strftime('%Y',${x}) as Integer )`,
|
|
154
|
+
/**
|
|
155
|
+
* Generates SQL statement that produces the month of a given timestamp
|
|
156
|
+
* @param {string} x
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
39
159
|
month: x => `cast( strftime('%m',${x}) as Integer )`,
|
|
160
|
+
/**
|
|
161
|
+
* Generates SQL statement that produces the day of a given timestamp
|
|
162
|
+
* @param {string} x
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
40
165
|
day: x => `cast( strftime('%d',${x}) as Integer )`,
|
|
166
|
+
/**
|
|
167
|
+
* Generates SQL statement that produces the hours of a given timestamp
|
|
168
|
+
* @param {string} x
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
41
171
|
hour: x => `cast( strftime('%H',${x}) as Integer )`,
|
|
172
|
+
/**
|
|
173
|
+
* Generates SQL statement that produces the minutes of a given timestamp
|
|
174
|
+
* @param {string} x
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
42
177
|
minute: x => `cast( strftime('%M',${x}) as Integer )`,
|
|
178
|
+
/**
|
|
179
|
+
* Generates SQL statement that produces the seconds of a given timestamp
|
|
180
|
+
* @param {string} x
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
43
183
|
second: x => `cast( strftime('%S',${x}) as Integer )`,
|
|
44
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Generates SQL statement that produces the fractional seconds of a given timestamp
|
|
187
|
+
* @param {string} x
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
45
190
|
fractionalseconds: x => `cast( strftime('%f0000',${x}) as Integer )`,
|
|
46
191
|
|
|
192
|
+
/**
|
|
193
|
+
* maximum date time value
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
47
196
|
maxdatetime: () => '9999-12-31 23:59:59.999',
|
|
197
|
+
/**
|
|
198
|
+
* minimum date time value
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
48
201
|
mindatetime: () => '0001-01-01 00:00:00.000',
|
|
49
202
|
|
|
50
203
|
// odata spec defines the date time offset type as a normal ISO time stamp
|
|
@@ -52,6 +205,11 @@ const StandardFunctions = {
|
|
|
52
205
|
// sqlite understands this so by splitting the timezone from the actual date
|
|
53
206
|
// prefixing it with 1970 it allows sqlite to give back the number of seconds
|
|
54
207
|
// which can be divided by 60 back to minutes
|
|
208
|
+
/**
|
|
209
|
+
* Generates SQL statement that produces the offset in minutes of a given date time offset string
|
|
210
|
+
* @param {string} x
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
55
213
|
totaloffsetminutes: x => `case
|
|
56
214
|
when substr(${x}, length(${x})) = 'z' then 0
|
|
57
215
|
else strftime('%s', '1970-01-01T00:00:00' || substr(${x}, length(${x}) - 5)) / 60
|
|
@@ -65,6 +223,11 @@ const StandardFunctions = {
|
|
|
65
223
|
// the number of seconds in a day
|
|
66
224
|
// As sqlite is most accurate with juliandays it is better to do then then using actual second function
|
|
67
225
|
// while the odata specification states that the seconds has to be fractional which only julianday allows
|
|
226
|
+
/**
|
|
227
|
+
* Generates SQL statement that produces an OData compliant duration string like: P12DT23H59M59.999999999999S
|
|
228
|
+
* @param {string} x
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
68
231
|
totalseconds: x => `(
|
|
69
232
|
(
|
|
70
233
|
(
|
|
@@ -84,18 +247,43 @@ const StandardFunctions = {
|
|
|
84
247
|
)
|
|
85
248
|
) * 86400
|
|
86
249
|
)`,
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Generates SQL statement that calls the session_context function with the given parameter
|
|
253
|
+
* @param {string} x session variable name or SQL expression
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
session_context: x => `session_context('${x.val}')`,
|
|
87
257
|
}
|
|
88
258
|
|
|
89
259
|
const HANAFunctions = {
|
|
90
260
|
// https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
|
|
91
261
|
|
|
92
262
|
// Time functions
|
|
263
|
+
/**
|
|
264
|
+
* Generates SQL statement that calculates the difference in 100nanoseconds between two timestamps
|
|
265
|
+
* @param {string} x left timestamp
|
|
266
|
+
* @param {string} y right timestamp
|
|
267
|
+
* @returns {string}
|
|
268
|
+
*/
|
|
93
269
|
nano100_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 864000000000`,
|
|
270
|
+
/**
|
|
271
|
+
* Generates SQL statement that calculates the difference in seconds between two timestamps
|
|
272
|
+
* @param {string} x left timestamp
|
|
273
|
+
* @param {string} y right timestamp
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
94
276
|
seconds_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 86400`,
|
|
95
277
|
// Calculates the difference in full days using julian day
|
|
96
278
|
// Using the exact time of the day to determine whether 24 hours have passed or not to add the final day
|
|
97
279
|
// When just comparing the julianday values with each other there are leap seconds included
|
|
98
280
|
// Which on the day resolution are included as the individual days therefor ignoring them to match HANA
|
|
281
|
+
/**
|
|
282
|
+
* Generates SQL statement that calculates the difference in days between two timestamps
|
|
283
|
+
* @param {string} x left timestamp
|
|
284
|
+
* @param {string} y right timestamp
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
99
287
|
days_between: (x, y) => `(
|
|
100
288
|
cast ( julianday(${y}) as Integer ) - cast ( julianday(${x}) as Integer )
|
|
101
289
|
) + (
|
|
@@ -122,6 +310,12 @@ const HANAFunctions = {
|
|
|
122
310
|
|
|
123
311
|
Also check whether the result will be positive or negative to make sure to not subtract an extra month
|
|
124
312
|
*/
|
|
313
|
+
/**
|
|
314
|
+
* Generates SQL statement that calculates the difference in months between two timestamps
|
|
315
|
+
* @param {string} x left timestamp
|
|
316
|
+
* @param {string} y right timestamp
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
125
319
|
months_between: (x, y) => `
|
|
126
320
|
(
|
|
127
321
|
(
|
|
@@ -139,6 +333,12 @@ const HANAFunctions = {
|
|
|
139
333
|
)
|
|
140
334
|
)
|
|
141
335
|
)`,
|
|
336
|
+
/**
|
|
337
|
+
* Generates SQL statement that calculates the difference in years between two timestamps
|
|
338
|
+
* @param {string} x left timestamp
|
|
339
|
+
* @param {string} y right timestamp
|
|
340
|
+
* @returns {string}
|
|
341
|
+
*/
|
|
142
342
|
years_between(x, y) {
|
|
143
343
|
return `floor(${this.months_between(x, y)} / 12)`
|
|
144
344
|
},
|