@cap-js/sqlite 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,230 +0,0 @@
1
- const cds = require('@sap/cds')
2
- const { compareJson } = require('@sap/cds/libx/_runtime/cds-services/services/utils/compareJson')
3
- const { target_name4 } = require('./utils')
4
-
5
- const handledDeep = Symbol('handledDeep')
6
-
7
- async function onDeep (req, next) {
8
- const { query } = req
9
- // REVISIT: req.target does not match the query.INSERT target for path insert
10
- // cds.inferred(query)
11
- // const target = query.sources[Object.keys(query.sources)[0]]
12
- if (!this.model?.definitions[target_name4(req.query)]) {
13
- return next()
14
- }
15
- const {target} = this.infer(query)
16
- if (!hasDeep(query, target)) return next()
17
- const beforeData = query.INSERT ? [] : await this.run(getExpandForDeep(query, target, true))
18
- const queries = getDeepQueries(query, beforeData, target)
19
- const res = await Promise.all(queries.map (query => {
20
- if (query.INSERT) return this.onINSERT({query})
21
- if (query.UPDATE) return this.onUPDATE({query})
22
- if (query.DELETE) return this.onSIMPLE({query})
23
- }))
24
- return res[0] ?? 0 // TODO what todo with multiple result responses?
25
- }
26
-
27
-
28
- const hasDeep = (query, target) => {
29
- if (handledDeep in query) return
30
- if (query.DELETE) {
31
- for (let c in target?.compositions) return true
32
- return false
33
- }
34
- const data = query.INSERT?.entries || (query.UPDATE?.data && [query.UPDATE.data]) || (query.UPDATE?.with && [query.UPDATE.with])
35
- if (data) for (const c in target.compositions) {
36
- for (const row of data) if (row[c] !== undefined) return true
37
- }
38
- }
39
-
40
- // unofficial config!
41
- const DEEP_DELETE_MAX_RECURSION_DEPTH =
42
- (cds.env.features.recursion_depth && Number(cds.env.features.recursion_depth)) || 4 // we use 4 here as our test data has a max depth of 3
43
-
44
- // IMPORTANT: Skip only if @cds.persistence.skip is `true` → e.g. this skips skipping targets marked with @cds.persistence.skip: 'if-unused'
45
- const _hasPersistenceSkip = target => target?.["@cds.persistence.skip"] === true
46
-
47
- const getColumnsFromDataOrKeys = (data, target) => {
48
- if (Array.isArray(data)) {
49
- // loop and get all columns from current level
50
- const columns = new Set()
51
- data.forEach(row =>
52
- Object.keys(row || target.keys)
53
- .filter(propName => !target.elements[propName]?.isAssociation)
54
- .forEach(entry => {
55
- columns.add(entry)
56
- })
57
- )
58
- return Array.from(columns).map(c => ({ ref: [c] }))
59
- } else {
60
- // get all columns from current level
61
- return Object.keys(data || target.keys)
62
- .filter(propName => !target.elements[propName]?.isAssociation)
63
- .map(c => ({ ref: [c] }))
64
- }
65
- }
66
-
67
- const _calculateExpandColumns = (target, data, expandColumns = [], elementMap = new Map()) => {
68
- const compositions = target.compositions || {}
69
-
70
- if(expandColumns.length === 0) {
71
- // REVISIT: ensure that all keys are included in the expand columns
72
- expandColumns.push(...getColumnsFromDataOrKeys(data, target))
73
- }
74
-
75
- for (const compName in compositions) {
76
- let compositionData
77
- if (data === null || (Array.isArray(data) && !data.length)) {
78
- compositionData = null
79
- } else {
80
- compositionData = data[compName]
81
- }
82
-
83
- // ignore not provided compositions as nothing happens with them (expect deep delete)
84
- if (compositionData === undefined) {
85
- // fill columns in case
86
- continue
87
- }
88
-
89
- const composition = compositions[compName]
90
-
91
- const fqn = composition.parent.name + ':' + composition.name
92
- const seen = elementMap.get(fqn)
93
- if (seen && seen >= DEEP_DELETE_MAX_RECURSION_DEPTH) {
94
- // recursion -> abort
95
- return
96
- }
97
-
98
- let expandColumn = expandColumns.find(expandColumn => expandColumn.ref[0] === composition.name)
99
- if (!expandColumn) {
100
- expandColumn = {
101
- ref: [composition.name],
102
- expand: getColumnsFromDataOrKeys(compositionData, composition._target)
103
- }
104
-
105
- expandColumns.push(expandColumn)
106
- }
107
-
108
- // expand deep
109
- // Make a copy and do not share the same map among brother compositions
110
- // as we're only interested in deep recursions, not wide recursions.
111
- const newElementMap = new Map(elementMap)
112
- newElementMap.set(fqn, (seen && seen + 1) || 1)
113
-
114
- if (composition.is2many) {
115
- // expandColumn.expand = getColumnsFromDataOrKeys(compositionData, composition._target)
116
- if (compositionData === null || compositionData.length === 0) {
117
- // deep delete, get all subitems until recursion depth
118
- _calculateExpandColumns(composition._target, null, expandColumn.expand, newElementMap)
119
- continue
120
- }
121
-
122
- for (const row of compositionData) {
123
- _calculateExpandColumns(composition._target, row, expandColumn.expand, newElementMap)
124
- }
125
- } else {
126
- // to one
127
- _calculateExpandColumns(composition._target, compositionData, expandColumn.expand, newElementMap)
128
- }
129
- }
130
- }
131
-
132
- const getExpandForDeep = (query, target) => {
133
- const from = query.DELETE?.from || query.UPDATE?.entity
134
- const data = query.UPDATE?.data || null
135
- const where = query.DELETE?.where || query.UPDATE?.where
136
-
137
- const cqn = SELECT.from(from)
138
- if (where) cqn.SELECT.where = where
139
-
140
- const columns = []
141
- _calculateExpandColumns(target, data, columns)
142
- cqn.columns(columns)
143
- return cqn
144
- }
145
-
146
- const getDeepQueries = (query, dbData, target) => {
147
- let queryData
148
- if (query.INSERT) {
149
- queryData = query.INSERT.entries
150
- }
151
- if (query.DELETE) {
152
- queryData = []
153
- }
154
- if (query.UPDATE) {
155
- queryData = [query.UPDATE.data]
156
- }
157
-
158
- let diff = compareJson(queryData, dbData, target)
159
- if (!Array.isArray(diff)) {
160
- diff = [diff]
161
- }
162
-
163
- return _getDeepQueries(diff, target)
164
- }
165
-
166
- const _getDeepQueries = (diff, target) => {
167
- const queries = []
168
-
169
- for (const diffEntry of diff) {
170
- if (diffEntry === undefined) continue
171
- const subQueries = []
172
-
173
- for (const prop in diffEntry) {
174
- // handle deep operations
175
-
176
- const propData = diffEntry[prop]
177
-
178
- if (target.elements[prop] && _hasPersistenceSkip(target.elements[prop]._target)) {
179
- delete diffEntry[prop]
180
- } else if (target.compositions?.[prop]) {
181
- const arrayed = Array.isArray(propData) ? propData : [propData]
182
- arrayed.forEach(subEntry => {
183
- subQueries.push(..._getDeepQueries([subEntry], target.elements[prop]._target))
184
- })
185
- delete diffEntry[prop]
186
- }
187
- }
188
-
189
- // handle current entity level
190
- const op = diffEntry._op
191
- delete diffEntry._op
192
-
193
- if (diffEntry._old != null) {
194
- delete diffEntry._old
195
- }
196
-
197
- // first calculate subqueries and rm their properties, then build root query
198
- if (op === 'create') {
199
- queries.push(INSERT.into(target).entries(diffEntry))
200
- } else if (op === 'delete') {
201
- queries.push(DELETE.from(target).where(diffEntry))
202
- } else if (op === 'update') {
203
- // TODO do we need the where here?
204
- const keys = target.keys
205
- const cqn = UPDATE(target).with(diffEntry)
206
- for (const key in keys) {
207
- if (keys[key].virtual) continue
208
- if (!keys[key].isAssociation) {
209
- cqn.where(key + '=', diffEntry[key])
210
- }
211
- delete diffEntry[key]
212
- }
213
- cqn.with(diffEntry)
214
- queries.push(cqn)
215
- }
216
-
217
- queries.push(...subQueries)
218
- }
219
-
220
- queries.forEach(q => {
221
- Object.defineProperty(q, handledDeep, { value: true })
222
- })
223
- return queries
224
- }
225
-
226
- module.exports = {
227
- onDeep,
228
- getDeepQueries,
229
- getExpandForDeep
230
- }
@@ -1,147 +0,0 @@
1
- const StandardFunctions = {
2
-
3
- // OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
4
-
5
- // String and Collection Functions
6
- // length : (x) => `length(${x})`,
7
- search: function(ref, arg) {
8
- if (!('val' in arg)) throw `SQLite only supports single value arguments for $search`
9
- const refs = ref.list || [ref], {toString} = ref
10
- return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)),this.tolower(arg))).join(' or ') + ')'
11
- },
12
- concat : (...args) => args.join('||'),
13
- contains : (...args) => `ifnull(instr(${args}),0)`,
14
- count : (x) => `count(${x||'*'})`,
15
- countdistinct : (x) => `count(distinct ${x||'*'})`,
16
- indexof : (x,y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed
17
- startswith : (x,y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed
18
- endswith : (x,y) => `instr(${x},${y}) = length(${x}) - length(${y}) +1`,
19
- substring : (x,y,z) => ( z
20
- ? `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z} )`
21
- : `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end )`
22
- ),
23
-
24
- // String Functions
25
- matchesPattern : (x,y) => `${x} regexp ${y})`,
26
- tolower : (x) => `lower(${x})`,
27
- toupper : (x) => `upper(${x})`,
28
- // trim : (x) => `trim(${x})`,
29
-
30
- // Arithmetic Functions
31
- ceiling : (x) => `ceil(${x})`,
32
- // floor : (x) => `floor(${x})`,
33
- // round : (x) => `round(${x})`,
34
-
35
- // Date and Time Functions
36
- year : (x) => `cast( strftime('%Y',${x}) as Integer )`,
37
- month : (x) => `cast( strftime('%m',${x}) as Integer )`,
38
- day : (x) => `cast( strftime('%d',${x}) as Integer )`,
39
- hour : (x) => `cast( strftime('%H',${x}) as Integer )`,
40
- minute : (x) => `cast( strftime('%M',${x}) as Integer )`,
41
- second : (x) => `cast( strftime('%S',${x}) as Integer )`,
42
-
43
- fractionalseconds : (x) => `cast( strftime('%f',${x}) as Integer )`,
44
-
45
- maxdatetime : ()=> '9999-12-31 23:59:59.999',
46
- mindatetime : ()=> '0001-01-01 00:00:00.000',
47
-
48
- // odata spec defines the date time offset type as a normal ISO time stamp
49
- // Where the timezone can either be 'Z' (for UTC) or [+|-]xx:xx for the time offset
50
- // sqlite understands this so by splitting the timezone from the actual date
51
- // prefixing it with 1970 it allows sqlite to give back the number of seconds
52
- // which can be divided by 60 back to minutes
53
- totaloffsetminutes : (x) => `case
54
- when substr(${x}, length(${x})) = 'z' then 0
55
- else strftime('%s', '1970-01-01T00:00:00' || substr(${x}, length(${x}) - 5)) / 60
56
- end`,
57
-
58
- // odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
59
- // P -> duration indicator
60
- // D -> days, T -> Time seperator, H -> hours, M -> minutes, S -> fractional seconds
61
- // By splitting the DT and calculating the seconds of the time separate from the day
62
- // it possible to determine the full amount of seconds by adding them together as fractionals and multiplying
63
- // the number of seconds in a day
64
- // As sqlite is most accurate with juliandays it is better to do then then using actual second function
65
- // while the odata specification states that the seconds has to be fractional which only julianday allows
66
- totalseconds : (x) => `(
67
- (
68
- (
69
- cast(substr(${x},2,instr(${x},'DT') - 2) as Integer)
70
- ) + (
71
- julianday(
72
- '-4713-11-25T' ||
73
- replace(
74
- replace(
75
- replace(
76
- substr(${x},instr(${x},'DT') + 2),
77
- 'H',':'
78
- ),'M',':'
79
- ),'S','Z'
80
- )
81
- ) - 0.5
82
- )
83
- ) * 86400
84
- )`,
85
- }
86
-
87
-
88
- const HANAFunctions = {
89
- // https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
90
-
91
- // Time functions
92
- nano100_between : (x,y) => `(julianday(${y}) - julianday(${x})) * 864000000000`,
93
- seconds_between : (x,y) => `(julianday(${y}) - julianday(${x})) * 86400`,
94
- // Calculates the difference in full days using julian day
95
- // Using the exact time of the day to determine whether 24 hours have passed or not to add the final day
96
- // When just comparing the julianday values with each other there are leap seconds included
97
- // Which on the day resolution are included as the individual days therefor ignoring them to match HANA
98
- days_between : (x,y) => `(
99
- cast ( julianday(${y}) as Integer ) - cast ( julianday(${x}) as Integer )
100
- ) + (
101
- case
102
- when ( julianday(${y}) < julianday(${x}) ) then
103
- (cast( strftime('%H%M%S%f', ${y}) as Integer ) < cast( strftime('%H%M%S%f', ${x}) as Integer ))
104
- else
105
- (cast( strftime('%H%M%S%f', ${y}) as Integer ) > cast( strftime('%H%M%S%f', ${x}) as Integer )) * -1
106
- end
107
- )`,
108
-
109
- // (y1 - y0) * 12 + (m1 - m0) + (t1 < t0) * -1
110
- /* '%d%H%M%S%f' returns as a number like which results in an equal check to:
111
- (
112
- d1 < d0 ||
113
- (d1 = d0 && h1 < h0) ||
114
- (d1 = d0 && h1 = h0 && m1 < m0) ||
115
- (d1 = d0 && h1 = h0 && m1 = m0 && s1 < s0) ||
116
- (d1 = d0 && h1 = h0 && m1 = m0 && s1 = s0 && ms1 < ms0)
117
- )
118
- Which will remove the current month if the time of the month is below the time of the month of the start date
119
- It should not matter that the number of days in the month is different as for a month to have passed
120
- the time of the month would have to be higher then the time of the month of the start date
121
-
122
- Also check whether the result will be positive or negative to make sure to not subtract an extra month
123
- */
124
- months_between : (x,y) => `
125
- (
126
- (
127
- ( cast( strftime('%Y', ${y}) as Integer ) - cast( strftime('%Y', ${x}) as Integer ) ) * 12
128
- ) + (
129
- cast( strftime('%m', ${y}) as Integer ) - cast( strftime('%m', ${x}) as Integer )
130
- ) + (
131
- (
132
- case
133
- when ( cast( strftime('%Y%m', ${y}) as Integer ) < cast( strftime('%Y%m', ${x}) as Integer ) ) then
134
- (cast( strftime('%d%H%M%S%f', ${y}) as Integer ) > cast( strftime('%d%H%M%S%f', ${x}) as Integer ))
135
- else
136
- (cast( strftime('%d%H%M%S%f', ${y}) as Integer ) < cast( strftime('%d%H%M%S%f', ${x}) as Integer )) * -1
137
- end
138
- )
139
- )
140
- )`,
141
- years_between(x,y) { return `floor(${this.months_between(x,y)} / 12)` },
142
- }
143
-
144
- for (let each in HANAFunctions)
145
- HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
146
-
147
- module.exports = { ...StandardFunctions, ...HANAFunctions }
@@ -1,16 +0,0 @@
1
- /**
2
- * For operators of <eqOps>, this is replaced by comparing all leaf elements with null, combined with and.
3
- * If there are at least two leaf elements and if there are tokens before or after the recognized pattern, we enclose the resulting condition in parens (...)
4
- */
5
- const eqOps = [['is'], ['='], /* ['=='] */ ]
6
- /**
7
- * For operators of <notEqOps>, do the same but use or instead of and.
8
- * This ensures that not struc == <value> is the same as struc != <value>.
9
- */
10
- const notEqOps = [['is', 'not'], ['<>'], ['!=']]
11
- /**
12
- * not supported in comparison w/ struct because of unclear semantics
13
- */
14
- const notSupportedOps = [['>'], ['<'], ['>='], ['<=']]
15
-
16
- module.exports = { eqOps, notEqOps, notSupportedOps }
@@ -1,22 +0,0 @@
1
- const cds = require('@sap/cds/lib')
2
-
3
- const target_name4 = q => {
4
- const target = (
5
- q.SELECT?.from ||
6
- q.INSERT?.into ||
7
- q.UPSERT?.into ||
8
- q.UPDATE?.entity ||
9
- q.DELETE?.from ||
10
- q.CREATE?.entity ||
11
- q.DROP?.entity ||
12
- undefined
13
- )
14
- if (target?.SET?.op === "union") throw new cds.error('”UNION” based queries are not supported');
15
- if (!target?.ref) return target
16
- const [first] = target.ref
17
- return first.id || first
18
- }
19
-
20
- module.exports = {
21
- target_name4
22
- }
@@ -1,63 +0,0 @@
1
- const cds = require('@sap/cds')
2
- const propagateForeignKeys = require('@sap/cds/libx/_runtime/common/utils/propagateForeignKeys')
3
- const { enrichDataWithKeysFromWhere } = require('@sap/cds/libx/_runtime/common/utils/keys')
4
-
5
- const generateUUIDandPropagateKeys = (target, data, event) => {
6
- if (!data) return
7
- const keys = target.keys
8
- for (const key in keys) {
9
- if (keys[key].type === 'cds.UUID' && !data[key] && event === 'CREATE') {
10
- data[key] = cds.utils.uuid()
11
- }
12
- }
13
- const elements = target.elements
14
- for (const element in elements) {
15
- // if assoc keys are structured, do not ignore them, as they need to be flattened in propagateForeignKeys
16
- if (elements[element].key && !(elements[element]._isAssociationStrict && elements[element].is2one && element in data)) {
17
- continue
18
- }
19
-
20
- if (elements[element].is2one || elements[element].is2many) {
21
- let subData = data[element]
22
- if (subData) {
23
- if (!Array.isArray(subData)) {
24
- subData = [subData]
25
- }
26
- for (const sub of subData) {
27
- generateUUIDandPropagateKeys(elements[element]._target, sub, event)
28
- }
29
- }
30
-
31
- propagateForeignKeys(element, data, elements[element]._foreignKeys, elements[element].isComposition, {
32
- deleteAssocs: true
33
- })
34
- }
35
- }
36
- }
37
-
38
- const input = async function (req, next) {
39
- // REVISIT dummy handler until we have input processing
40
- if (!req.target || !this.model || req.target._unresolved) return next()
41
-
42
- if (req.event === "UPDATE") {
43
- // REVISIT for deep update we need to inject the keys first
44
- enrichDataWithKeysFromWhere(req.data, req, this)
45
- }
46
-
47
- // REVISIT no input processing for INPUT with rows/values
48
- if (req.event !== 'DELETE' && !(req.query.INSERT?.rows || req.query.INSERT?.values)) {
49
- if (Array.isArray(req.data)) {
50
- for (const d of req.data) {
51
- generateUUIDandPropagateKeys(req.target, d, req.event)
52
- }
53
- } else {
54
- generateUUIDandPropagateKeys(req.target, req.data, req.event)
55
- }
56
- }
57
-
58
- return next()
59
- }
60
-
61
- module.exports = {
62
- input
63
- }
@@ -1,149 +0,0 @@
1
- {
2
- "ABORT":1,
3
- "ACTION":1,
4
- "ADD":1,
5
- "AFTER":1,
6
- "ALL":1,
7
- "ALTER":1,
8
- "ALWAYS":1,
9
- "ANALYZE":1,
10
- "AND":1,
11
- "AS":1,
12
- "ASC":1,
13
- "ATTACH":1,
14
- "AUTOINCREMENT":1,
15
- "BEFORE":1,
16
- "BEGIN":1,
17
- "BETWEEN":1,
18
- "BY":1,
19
- "CASCADE":1,
20
- "CASE":1,
21
- "CAST":1,
22
- "CHECK":1,
23
- "COLLATE":1,
24
- "COLUMN":1,
25
- "COMMIT":1,
26
- "CONFLICT":1,
27
- "CONSTRAINT":1,
28
- "CREATE":1,
29
- "CROSS":1,
30
- "CURRENT":1,
31
- "CURRENT_DATE":1,
32
- "CURRENT_TIME":1,
33
- "CURRENT_TIMESTAMP":1,
34
- "DATABASE":1,
35
- "DEFAULT":1,
36
- "DEFERRABLE":1,
37
- "DEFERRED":1,
38
- "DELETE":1,
39
- "DESC":1,
40
- "DETACH":1,
41
- "DISTINCT":1,
42
- "DO":1,
43
- "DROP":1,
44
- "EACH":1,
45
- "ELSE":1,
46
- "END":1,
47
- "ESCAPE":1,
48
- "EXCEPT":1,
49
- "EXCLUDE":1,
50
- "EXCLUSIVE":1,
51
- "EXISTS":1,
52
- "EXPLAIN":1,
53
- "FAIL":1,
54
- "FILTER":1,
55
- "FIRST":1,
56
- "FOLLOWING":1,
57
- "FOR":1,
58
- "FOREIGN":1,
59
- "FROM":1,
60
- "FULL":1,
61
- "GENERATED":1,
62
- "GLOB":1,
63
- "GROUP":1,
64
- "GROUPS":1,
65
- "HAVING":1,
66
- "IF":1,
67
- "IGNORE":1,
68
- "IMMEDIATE":1,
69
- "IN":1,
70
- "INDEX":1,
71
- "INDEXED":1,
72
- "INITIALLY":1,
73
- "INNER":1,
74
- "INSERT":1,
75
- "INSTEAD":1,
76
- "INTERSECT":1,
77
- "INTO":1,
78
- "IS":1,
79
- "ISNULL":1,
80
- "JOIN":1,
81
- "KEY":1,
82
- "LAST":1,
83
- "LEFT":1,
84
- "LIKE":1,
85
- "LIMIT":1,
86
- "MATCH":1,
87
- "MATERIALIZED":1,
88
- "NATURAL":1,
89
- "NO":1,
90
- "NOT":1,
91
- "NOTHING":1,
92
- "NOTNULL":1,
93
- "NULL":1,
94
- "NULLS":1,
95
- "OF":1,
96
- "OFFSET":1,
97
- "ON":1,
98
- "OR":1,
99
- "ORDER":1,
100
- "OTHERS":1,
101
- "OUTER":1,
102
- "OVER":1,
103
- "PARTITION":1,
104
- "PLAN":1,
105
- "PRAGMA":1,
106
- "PRECEDING":1,
107
- "PRIMARY":1,
108
- "QUERY":1,
109
- "RAISE":1,
110
- "RANGE":1,
111
- "RECURSIVE":1,
112
- "REFERENCES":1,
113
- "REGEXP":1,
114
- "REINDEX":1,
115
- "RELEASE":1,
116
- "RENAME":1,
117
- "REPLACE":1,
118
- "RESTRICT":1,
119
- "RETURNING":1,
120
- "RIGHT":1,
121
- "ROLLBACK":1,
122
- "ROW":1,
123
- "ROWS":1,
124
- "SAVEPOINT":1,
125
- "SELECT":1,
126
- "SET":1,
127
- "TABLE":1,
128
- "TEMP":1,
129
- "TEMPORARY":1,
130
- "THEN":1,
131
- "TIES":1,
132
- "TO":1,
133
- "TRANSACTION":1,
134
- "TRIGGER":1,
135
- "UNBOUNDED":1,
136
- "UNION":1,
137
- "UNIQUE":1,
138
- "UPDATE":1,
139
- "USING":1,
140
- "VACUUM":1,
141
- "VALUES":1,
142
- "VIEW":1,
143
- "VIRTUAL":1,
144
- "WHEN":1,
145
- "WHERE":1,
146
- "WINDOW":1,
147
- "WITH":1,
148
- "WITHOUT":1
149
- }