mythix-orm-sql-base 1.0.1

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.
@@ -0,0 +1,1117 @@
1
+ 'use strict';
2
+
3
+ const Nife = require('nife');
4
+ const {
5
+ Types,
6
+ QueryEngine,
7
+ Utils,
8
+ Literals,
9
+ Model: ModelBase,
10
+ QueryGeneratorBase,
11
+ } = require('mythix-orm');
12
+
13
+ const DefaultHelpers = Types.DefaultHelpers;
14
+ const LiteralBase = Literals.LiteralBase;
15
+
16
+ class SQLQueryGeneratorBase extends QueryGeneratorBase {
17
+ prepareArrayValuesForSQL(...args) {
18
+ return this.connection.prepareArrayValuesForSQL(...args);
19
+ }
20
+
21
+ parseFieldProjection = (str, getRawField) => {
22
+ let modelName;
23
+ let fieldName;
24
+ let projectionField;
25
+
26
+ str.replace(/(AS\s+)?"(\w+):([\w.]+)"/i, (m, as, _modelName, _fieldName) => {
27
+ modelName = _modelName;
28
+ fieldName = _fieldName;
29
+ });
30
+
31
+ if (!modelName || !fieldName) {
32
+ // Reverse search model and field name
33
+ // based on table and column name
34
+ str.replace(/"([^"]+)"."([^"]+)"/i, (m, _tableName, _columnName) => {
35
+ this.connection.findModelField(({ field, stop }) => {
36
+ if (field.columnName !== _columnName)
37
+ return;
38
+
39
+ let tableName = field.Model.getTableName(this.connection);
40
+ if (tableName !== _tableName)
41
+ return;
42
+
43
+ if (getRawField) {
44
+ projectionField = field;
45
+
46
+ stop();
47
+
48
+ return;
49
+ }
50
+
51
+ modelName = field.Model.getModelName();
52
+ fieldName = field.fieldName;
53
+
54
+ stop();
55
+ });
56
+ });
57
+
58
+ if (getRawField && projectionField)
59
+ return projectionField;
60
+ }
61
+
62
+ if (getRawField && modelName && fieldName) {
63
+ let field = this.connection.getField(fieldName, modelName);
64
+ if (field)
65
+ return field;
66
+ } else if (!modelName || !fieldName) {
67
+ return str;
68
+ }
69
+
70
+ return `${modelName}:${fieldName}`;
71
+ };
72
+
73
+ isFieldProjection(str) {
74
+ return (/^"[^"]+"."[^"]+"|"\w+:[\w.]+"/i).test(str);
75
+ }
76
+
77
+ parseFieldProjectionToFieldMap(selectStatement) {
78
+ let firstPart = selectStatement.replace(/[\r\n]/g, ' ').split(/\s+FROM\s+/i)[0].replace(/^SELECT\s+/i, '').trim();
79
+ let fieldParts = firstPart.split(',');
80
+ let projectionFieldMap = new Map();
81
+
82
+ for (let i = 0, il = fieldParts.length; i < il; i++) {
83
+ let fieldPart = fieldParts[i].trim();
84
+ let field = this.parseFieldProjection(fieldPart, true);
85
+
86
+ if (field !== fieldPart)
87
+ projectionFieldMap.set(`${field.Model.getModelName()}:${field.fieldName}`, this.getEscapedProjectionName(field.Model, field));
88
+ else
89
+ projectionFieldMap.set(field, field);
90
+
91
+ // If this isn't a field, then add it
92
+ if (!this.isFieldProjection(fieldPart))
93
+ projectionFieldMap.set(fieldPart, fieldPart);
94
+ }
95
+
96
+ let projectedFieldNames = Array.from(projectionFieldMap.keys());
97
+ let sortedFieldNames = this.sortedProjectedFields(projectedFieldNames);
98
+ let sortedProjectionFieldMap = new Map();
99
+
100
+ for (let i = 0, il = sortedFieldNames.length; i < il; i++) {
101
+ let sortedFieldName = sortedFieldNames[i];
102
+ let value = projectionFieldMap.get(sortedFieldName);
103
+
104
+ sortedProjectionFieldMap.set(sortedFieldName, value);
105
+ }
106
+
107
+ return sortedProjectionFieldMap;
108
+ }
109
+
110
+ generateSelectQueryFieldProjection(queryEngine, options, asMap) {
111
+ let projectedFields = this.getProjectedFields(queryEngine, options, asMap);
112
+
113
+ if (asMap === true)
114
+ return projectedFields;
115
+ else
116
+ return Array.from(projectedFields.values()).join(',');
117
+ }
118
+
119
+ // eslint-disable-next-line no-unused-vars
120
+ generateSelectQueryOperatorFromQueryEngineOperator(operator, value, valueIsReference, options) {
121
+ if (operator instanceof LiteralBase)
122
+ return operator.toString(this.connection);
123
+
124
+ switch (operator) {
125
+ case 'EQ':
126
+ if (!valueIsReference) {
127
+ if (value === null || value === true || value === false)
128
+ return 'IS';
129
+ else if (Array.isArray(value))
130
+ return 'IN';
131
+ }
132
+
133
+ return '=';
134
+ case 'NEQ':
135
+ if (!valueIsReference) {
136
+ if (value === null || value === true || value === false)
137
+ return 'IS NOT';
138
+ else if (Array.isArray(value))
139
+ return 'NOT IN';
140
+ }
141
+
142
+ return '!=';
143
+ case 'GT':
144
+ return '>';
145
+ case 'GTE':
146
+ return '>=';
147
+ case 'LT':
148
+ return '<';
149
+ case 'LTE':
150
+ return '<=';
151
+ default:
152
+ throw new Error(`${this.constructor.name}::generateSelectQueryOperatorFromQueryEngineOperator: Unknown operator "${operator}".`);
153
+ }
154
+ }
155
+
156
+ generateSelectQueryCondition(queryPart, _value, options) {
157
+ let value = _value;
158
+ let field = queryPart.Field;
159
+ let isNot = queryPart.not;
160
+ let operator = (isNot) ? queryPart.inverseOperator : queryPart.operator;
161
+
162
+ // If the value is an array, then handle the
163
+ // special "IN" case for an array
164
+ if (Array.isArray(value)) {
165
+ // Flatten array, filter down to
166
+ // only unique items, and remove
167
+ // anything that we can't match on
168
+ // (such as "undefined", and objects)
169
+ value = this.prepareArrayValuesForSQL(value);
170
+
171
+ // Filter out NULL, TRUE, and FALSE,
172
+ // as these will need to be compared
173
+ // with "IS" or "IS NOT" operators
174
+ let specialValues = value.filter((item) => (item === null || item === false || item === true));
175
+
176
+ // See what remains (if anything)
177
+ let arrayValues = value.filter((item) => (item !== null && item !== false && item !== true));
178
+
179
+ // If we have special values, then build a
180
+ // condition enclosed in parenthesis
181
+ if (specialValues.length > 0) {
182
+ let subParts = specialValues.map((specialValue) => {
183
+ return this.generateSelectQueryCondition(queryPart, specialValue, options);
184
+ });
185
+
186
+ if (arrayValues.length > 0)
187
+ subParts.push(this.generateSelectQueryCondition(queryPart, arrayValues, options));
188
+
189
+ return `(${subParts.join(' OR ')})`;
190
+ }
191
+
192
+ // If no values left in array, then
193
+ // skip condition altogether
194
+ if (Nife.isEmpty(arrayValues))
195
+ return '';
196
+
197
+ // Otherwise, fall-through
198
+ value = arrayValues;
199
+ }
200
+
201
+ let escapedTableName = this.escapeID(this.getTableNameFromQueryPart(queryPart));
202
+ let escapedColumnName = this.escapeID(field.columnName);
203
+ let sqlOperator = this.generateSelectQueryOperatorFromQueryEngineOperator(operator, value, false, options);
204
+
205
+ if (QueryEngine.isQuery(value)) {
206
+ if (!this.queryHasConditions(value._getRawQuery()))
207
+ return '';
208
+
209
+ if (sqlOperator === '=')
210
+ sqlOperator = 'IN';
211
+ else if (sqlOperator === '!=')
212
+ sqlOperator = 'NOT IN';
213
+
214
+ return `${escapedTableName}.${escapedColumnName} ${sqlOperator} (${this.generateSelectStatement(value, this.stackAssign(options, { isSubQuery: true }))})`;
215
+ }
216
+
217
+ return `${escapedTableName}.${escapedColumnName} ${sqlOperator} ${this.escape(field, value)}`;
218
+ }
219
+
220
+ // eslint-disable-next-line no-unused-vars
221
+ generateFromTableOrTableJoin(Model, joinType, options) {
222
+ if (!Model)
223
+ throw new Error(`${this.constructor.name}::generateFromTableOrTableJoin: No valid model provided.`);
224
+
225
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
226
+ return (joinType) ? `${joinType} ${escapedTableName}` : `FROM ${escapedTableName}`;
227
+ }
228
+
229
+ generateSelectJoinOnTableQueryCondition(leftQueryPart, rightQueryPart, leftField, rightField, operator, options) {
230
+ let leftSideEscapedTableName = this.escapeID(this.getTableNameFromQueryPart(leftQueryPart));
231
+ let leftSideEscapedColumnName = this.escapeID(leftField.columnName);
232
+ let rightSideEscapedTableName = this.escapeID(this.getTableNameFromQueryPart(rightQueryPart));
233
+ let rightSideEscapedColumnName = this.escapeID(rightField.columnName);
234
+ let sqlOperator = this.generateSelectQueryOperatorFromQueryEngineOperator(operator, undefined, true, options);
235
+
236
+ return `${leftSideEscapedTableName}.${leftSideEscapedColumnName} ${sqlOperator} ${rightSideEscapedTableName}.${rightSideEscapedColumnName}`;
237
+ }
238
+
239
+ generateJoinOnTableQueryConditions(joinInfos, options) {
240
+ if (Nife.isEmpty(joinInfos))
241
+ return '';
242
+
243
+ let rootInfo = joinInfos[0];
244
+ let sqlParts = [
245
+ this.generateFromTableOrTableJoin(rootInfo.joinModel, rootInfo.joinType, options),
246
+ 'ON',
247
+ ];
248
+
249
+ for (let i = 0, il = joinInfos.length; i < il; i++) {
250
+ let joinInfo = joinInfos[i];
251
+
252
+ if (i > 0)
253
+ sqlParts.push((joinInfo.leftQueryContext.and) ? 'AND' : 'OR');
254
+
255
+ sqlParts.push(this.generateSelectJoinOnTableQueryCondition(joinInfo.rightQueryContext, joinInfo.leftQueryContext, joinInfo.rightSideField, joinInfo.leftSideField, joinInfo.operator, options));
256
+ }
257
+
258
+ return sqlParts.join(' ');
259
+ }
260
+
261
+ // TODO: Needs to take a join type object
262
+ // eslint-disable-next-line no-unused-vars
263
+ generateSQLJoinTypeFromQueryEngineJoinType(joinType, outer, options) {
264
+ if (!joinType || joinType === 'inner')
265
+ return 'INNER JOIN';
266
+ else if (joinType === 'left')
267
+ return (outer) ? 'LEFT OUTER JOIN' : 'LEFT JOIN';
268
+ else if (joinType === 'right')
269
+ return (outer) ? 'RIGHT OUTER JOIN' : 'RIGHT JOIN';
270
+ else if (joinType === 'full')
271
+ return (outer) ? 'FULL OUTER JOIN' : 'FULL JOIN';
272
+ else if (joinType === 'cross')
273
+ return 'CROSS JOIN';
274
+
275
+ return joinType;
276
+ }
277
+
278
+ sortJoinRelationOrder(joins) {
279
+ let modelNames = Array.from(joins.keys());
280
+
281
+ return Utils.sortModelNamesByDependencyOrder(this.connection, modelNames, (Model, modelName) => {
282
+ let joinInfos = joins.get(modelName);
283
+ let dependencies = [];
284
+
285
+ for (let i = 0, il = joinInfos.length; i < il; i++) {
286
+ let joinInfo = joinInfos[i];
287
+
288
+ if (joinInfo.rightSideModelName !== modelName)
289
+ dependencies.push(joinInfo.rightSideModelName);
290
+
291
+ if (joinInfo.leftSideModelName !== modelName)
292
+ dependencies.push(joinInfo.leftSideModelName);
293
+ }
294
+
295
+ return dependencies;
296
+ });
297
+ }
298
+
299
+ generateSelectQueryJoinTables(queryEngine, options) {
300
+ const addToJoins = (joinInfo) => {
301
+ let items = joins.get(joinInfo.joinModelName);
302
+ if (!items) {
303
+ items = [];
304
+ joins.set(joinInfo.joinModelName, items);
305
+ }
306
+
307
+ items.push(joinInfo);
308
+ };
309
+
310
+ let query = queryEngine._getRawQuery();
311
+ let joins = new Map();
312
+
313
+ for (let i = 0, il = query.length; i < il; i++) {
314
+ let queryPart = query[i];
315
+ if (!(Object.prototype.hasOwnProperty.call(queryPart, 'condition') && queryPart.condition === true))
316
+ continue;
317
+
318
+ let operatorValue = queryPart.value;
319
+ if (!QueryEngine.isQuery(operatorValue))
320
+ continue;
321
+
322
+ // If the query has a condition, then it is a sub-query
323
+ // not a join
324
+ if (operatorValue._getRawQueryContext().condition)
325
+ continue;
326
+
327
+ let joinType = this.generateSQLJoinTypeFromQueryEngineJoinType(queryPart.joinType, queryPart.joinOuter, options);
328
+ let joinInfo = this.getJoinTableInfoFromQueryContexts(queryPart, operatorValue._getRawQueryContext(), joinType, options);
329
+
330
+ addToJoins(joinInfo);
331
+ }
332
+
333
+ // We sort the order of joins because
334
+ // some databases care very much
335
+ let modelNames = this.sortJoinRelationOrder(joins);
336
+ let sqlParts = [];
337
+
338
+ for (let i = 0, il = modelNames.length; i < il; i++) {
339
+ let modelName = modelNames[i];
340
+ let joinInfos = joins.get(modelName);
341
+
342
+ let sqlStr = this.generateJoinOnTableQueryConditions(joinInfos, options);
343
+ sqlParts.push(sqlStr);
344
+ }
345
+
346
+ return sqlParts.join(' ');
347
+ }
348
+
349
+ generateSelectWhereConditions(queryEngine, options) {
350
+ const logicalCondition = (sqlParts, queryPart) => {
351
+ if (sqlParts.length === 0)
352
+ return '';
353
+
354
+ if (queryPart.and)
355
+ return 'AND ';
356
+ else if (queryPart.or)
357
+ return 'OR ';
358
+
359
+ return '';
360
+ };
361
+
362
+ let query = queryEngine._getRawQuery();
363
+ let sqlParts = [];
364
+ let hasValue = false;
365
+
366
+ for (let i = 0, il = query.length; i < il; i++) {
367
+ let queryPart = query[i];
368
+ let queryOperator = queryPart.operator;
369
+ let queryValue = queryPart.value;
370
+ let result = undefined;
371
+
372
+ if (Object.prototype.hasOwnProperty.call(queryPart, 'condition')) {
373
+ if (queryPart.condition !== true)
374
+ continue;
375
+
376
+ result = this.generateSelectQueryCondition(queryPart, queryValue, options);
377
+ } else if (Object.prototype.hasOwnProperty.call(queryPart, 'logical')) {
378
+ if (queryOperator === 'NOT')
379
+ continue;
380
+
381
+ // If we have a value for the logical operator
382
+ // then that means we have a sub-grouping
383
+ if (Object.prototype.hasOwnProperty.call(queryPart, 'value') && QueryEngine.isQuery(queryValue)) {
384
+ result = this.generateSelectWhereConditions(queryValue, options);
385
+ if (result)
386
+ result = `(${result})`;
387
+ }
388
+ }
389
+
390
+ if (result) {
391
+ let finalResult = `${logicalCondition(sqlParts, queryPart)}${result}`;
392
+
393
+ if (sqlParts[0] !== result && sqlParts.indexOf(finalResult) < 0) {
394
+ sqlParts.push(finalResult);
395
+ hasValue = true;
396
+ }
397
+ }
398
+ }
399
+
400
+ if (!hasValue)
401
+ return '';
402
+
403
+ // Trim any trailing "NOT", "OR", or "AND"
404
+ // from the parts
405
+ let lastIndex = sqlParts.length;
406
+ // eslint-disable-next-line no-unreachable-loop
407
+ for (let i = sqlParts.length - 1; i >= 0; i--) {
408
+ let part = sqlParts[i];
409
+ if (part === 'NOT' || part === 'OR' || part === 'AND') {
410
+ lastIndex = i;
411
+ continue;
412
+ }
413
+
414
+ break;
415
+ }
416
+
417
+ if (lastIndex < sqlParts.length)
418
+ sqlParts = sqlParts.slice(0, lastIndex);
419
+
420
+ return sqlParts.join(' ');
421
+ }
422
+
423
+ // eslint-disable-next-line no-unused-vars
424
+ generateOrderClause(_orders, _options) {
425
+ if (_orders instanceof LiteralBase)
426
+ return _orders.toString(this.connection);
427
+
428
+ let orders = Nife.toArray(_orders).filter(Boolean);
429
+ if (Nife.isEmpty(orders))
430
+ return '';
431
+
432
+ let options = _options || {};
433
+ let orderByParts = [];
434
+ for (let i = 0, il = orders.length; i < il; i++) {
435
+ let orderField = orders[i];
436
+
437
+ if (orderField instanceof LiteralBase) {
438
+ orderByParts.push(orderField.toString(this.connection));
439
+ continue;
440
+ }
441
+
442
+ // Only allow fields that are in our projection
443
+ if (options.projectionFields) {
444
+ let modelName = orderField.Model.getModelName();
445
+ let fieldName = orderField.Field.fieldName;
446
+ let fqFieldName = `${modelName}:${fieldName}`;
447
+
448
+ if (!options.projectionFields.has(fqFieldName))
449
+ continue;
450
+ }
451
+
452
+ let escapedTableName = this.escapeID(orderField.Model.getTableName(this.connection));
453
+ let escapedColumnName = this.escapeID(orderField.Field.columnName);
454
+ let orderStr;
455
+
456
+ if (options.reverseOrder !== true)
457
+ orderStr = (orderField.direction === '-') ? 'DESC' : 'ASC';
458
+ else
459
+ orderStr = (orderField.direction === '-') ? 'ASC' : 'DESC';
460
+
461
+ orderByParts.push(`${escapedTableName}.${escapedColumnName} ${orderStr}`);
462
+ }
463
+
464
+ if (Nife.isEmpty(orderByParts))
465
+ return '';
466
+
467
+ return `ORDER BY ${orderByParts.join(',')}`;
468
+ }
469
+
470
+ // eslint-disable-next-line no-unused-vars
471
+ generateLimitClause(limit, options) {
472
+ if (limit instanceof LiteralBase)
473
+ return limit.toString(this.connection);
474
+
475
+ return `LIMIT ${limit}`;
476
+ }
477
+
478
+ // eslint-disable-next-line no-unused-vars
479
+ generateOffsetClause(offset, options) {
480
+ if (offset instanceof LiteralBase)
481
+ return offset.toString(this.connection);
482
+
483
+ return `OFFSET ${offset}`;
484
+ }
485
+
486
+ generateSelectOrderLimitOffset(queryEngine, options) {
487
+ let {
488
+ order,
489
+ limit,
490
+ offset,
491
+ } = this.getOrderLimitOffset(queryEngine, options);
492
+ let sqlParts = [];
493
+
494
+ if (Nife.isNotEmpty(order)) {
495
+ let result = this.generateOrderClause(order, options);
496
+ if (result)
497
+ sqlParts.push(result);
498
+ }
499
+
500
+ if (!Object.is(limit, Infinity) && Nife.isNotEmpty(limit)) {
501
+ let result = this.generateLimitClause(limit, options);
502
+ if (result)
503
+ sqlParts.push(result);
504
+ }
505
+
506
+ if (Nife.isNotEmpty(offset)) {
507
+ let result = this.generateOffsetClause(offset, options);
508
+ if (result)
509
+ sqlParts.push(result);
510
+ }
511
+
512
+ return sqlParts.join(' ');
513
+ }
514
+
515
+ generateWhereAndOrderLimitOffset(queryEngine, _options) {
516
+ let options = _options || {};
517
+ let sqlParts = [];
518
+
519
+ let where = this.generateSelectWhereConditions(queryEngine, options);
520
+ if (where)
521
+ sqlParts.push(`WHERE ${where}`);
522
+
523
+ let orderLimitOffset = this.generateSelectOrderLimitOffset(queryEngine, options);
524
+ if (orderLimitOffset)
525
+ sqlParts.push(orderLimitOffset);
526
+
527
+ return sqlParts.join(' ');
528
+ }
529
+
530
+ generateSelectStatement(_queryEngine, _options) {
531
+ let queryEngine = _queryEngine;
532
+ if (!QueryEngine.isQuery(queryEngine))
533
+ throw new Error(`${this.constructor.name}::generateSelectStatement: A query is required as the first argument.`);
534
+
535
+ let options = Object.create(_options || {});
536
+ if (options.includeRelations === true)
537
+ queryEngine = queryEngine.clone().PROJECT('*');
538
+
539
+ let rootModel = queryEngine._getRawQueryContext().rootModel;
540
+ if (!rootModel)
541
+ throw new Error(`${this.constructor.name}::generateSelectStatement: No root model found.`);
542
+
543
+ let sqlParts = [ 'SELECT' ];
544
+ let projectionFields;
545
+
546
+ options.selectStatement = true;
547
+
548
+ projectionFields = this.generateSelectQueryFieldProjection(queryEngine, options, true);
549
+ sqlParts.push(Array.from(projectionFields.values()).join(','));
550
+
551
+ sqlParts.push(this.generateFromTableOrTableJoin(rootModel, undefined, options));
552
+ sqlParts.push(this.generateSelectQueryJoinTables(queryEngine, options));
553
+ sqlParts.push(this.generateWhereAndOrderLimitOffset(queryEngine, this.stackAssign(options, { projectionFields })));
554
+
555
+ let sql = sqlParts.filter(Boolean).join(' ');
556
+
557
+ if (options.returnFieldProjection === true)
558
+ return { sql, projectionFields };
559
+ else
560
+ return sql;
561
+ }
562
+
563
+ getFieldDefaultValue(field, fieldName, _options) {
564
+ let options = _options || {};
565
+ let defaultValue = field.defaultValue;
566
+ if (defaultValue === undefined)
567
+ return;
568
+
569
+ if (options.isUpdateOperation && !DefaultHelpers.checkDefaultValueFlags(field.defaultValue, [ 'onUpdate' ]))
570
+ return;
571
+
572
+ if (options.isInsertOperation && !DefaultHelpers.checkDefaultValueFlags(field.defaultValue, [ 'onInsert' ]))
573
+ return;
574
+
575
+ let useDefaultKeyword = (Object.prototype.hasOwnProperty.call(options, 'useDefaultKeyword')) ? options.useDefaultKeyword : true;
576
+ let escapeValue = (Object.prototype.hasOwnProperty.call(options, 'escape')) ? options.escape : true;
577
+
578
+ if (typeof defaultValue === 'function') {
579
+ if (options.remoteOnly !== true) {
580
+ defaultValue = defaultValue({ field, fieldName, connection: this.connection, _static: true });
581
+ } else if (DefaultHelpers.checkDefaultValueFlags(field.defaultValue, [ 'literal', 'remote' ])) {
582
+ defaultValue = defaultValue({ field, fieldName, connection: this.connection, _static: true });
583
+ escapeValue = false;
584
+ } else {
585
+ return;
586
+ }
587
+ }
588
+
589
+ if (defaultValue instanceof LiteralBase) {
590
+ if (defaultValue.options.escape === false)
591
+ escapeValue = false;
592
+
593
+ useDefaultKeyword = (defaultValue.options.noDefaultStatementOnCreateTable !== true);
594
+
595
+ if (options.isUpdateOperation || options.isInsertOperation)
596
+ useDefaultKeyword = false;
597
+
598
+ if (options.rawLiterals !== true)
599
+ defaultValue = defaultValue.toString(this.connection);
600
+ else
601
+ return defaultValue;
602
+ }
603
+
604
+ if (escapeValue)
605
+ defaultValue = this.escape(field, defaultValue);
606
+
607
+ if (useDefaultKeyword)
608
+ return `DEFAULT ${defaultValue}`;
609
+ else
610
+ return `${defaultValue}`;
611
+ }
612
+
613
+ // eslint-disable-next-line no-unused-vars
614
+ generateIndexName(Model, field, index, options) {
615
+ let tableName = Model.getTableName(this.connection);
616
+
617
+ if (index === true)
618
+ return this.escapeID(`idx_${tableName}_${field.columnName}`.replace(/\W+/g, '_'));
619
+
620
+ let fieldNames = [];
621
+ for (let i = 0, il = index.length; i < il; i++) {
622
+ let indexFieldName = index[i];
623
+ let indexField = Model.getField(indexFieldName);
624
+ if (!indexField)
625
+ throw new Error(`${this.constructor.name}::generateIndexName: Unable to find field named "${indexFieldName}".`);
626
+
627
+ fieldNames.push(indexField.columnName);
628
+ }
629
+
630
+ return this.escapeID(`idx_${tableName}_${fieldNames.join('_')}`);
631
+ }
632
+
633
+ generateColumnIndex(Model, field, index, _options) {
634
+ let options = _options || {};
635
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
636
+ let indexName = this.generateIndexName(Model, field, index, options);
637
+ let flags = [];
638
+
639
+ if (options.concurrently)
640
+ flags.push('CONCURRENTLY');
641
+
642
+ if (options.ifNotExists)
643
+ flags.push('IF NOT EXISTS');
644
+
645
+ flags = flags.join(' ');
646
+
647
+ if (index === true)
648
+ return `CREATE INDEX ${flags} ${indexName} ON ${escapedTableName} (${this.escapeID(field.columnName)});`;
649
+
650
+ let fieldNames = [];
651
+ for (let i = 0, il = index.length; i < il; i++) {
652
+ let indexFieldName = index[i];
653
+ let indexField = Model.getField(indexFieldName);
654
+ if (!indexField)
655
+ throw new Error(`${this.constructor.name}::generateColumnIndex: Unable to find field named "${indexFieldName}".`);
656
+
657
+ fieldNames.push(this.escapeID(indexField.columnName));
658
+ }
659
+
660
+ return `CREATE INDEX ${indexName} ON ${escapedTableName} (${fieldNames.join(',')});`;
661
+ }
662
+
663
+ generateDropTableStatement(Model, _options) {
664
+ let options = _options || {};
665
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
666
+ let flags = [];
667
+
668
+ if (options.ifExists)
669
+ flags.push('IF EXISTS');
670
+
671
+ flags = flags.join(' ');
672
+
673
+ return `DROP TABLE ${flags} ${escapedTableName}${(options.cascade === true) ? ' CASCADE' : ''}`;
674
+ }
675
+
676
+ generateForeignKeyConstraint(field, type) {
677
+ let options = type.getOptions();
678
+ let targetModel = type.getTargetModel(this.connection);
679
+ let targetField = type.getTargetField(this.connection);
680
+
681
+ let sqlParts = [
682
+ 'FOREIGN KEY(',
683
+ this.escapeID(field.columnName),
684
+ ') REFERENCES ',
685
+ this.escapeID(targetModel.getTableName(this.connection)),
686
+ '(',
687
+ this.escapeID(targetField.columnName),
688
+ ')',
689
+ ];
690
+
691
+ if (options.deferred === true) {
692
+ sqlParts.push(' ');
693
+ sqlParts.push('DEFERRABLE INITIALLY DEFERRED');
694
+ }
695
+
696
+ if (options.onDelete) {
697
+ sqlParts.push(' ');
698
+ sqlParts.push(`ON DELETE ${options.onDelete.toUpperCase()}`);
699
+ }
700
+
701
+ if (options.onUpdate) {
702
+ sqlParts.push(' ');
703
+ sqlParts.push(`ON UPDATE ${options.onUpdate.toUpperCase()}`);
704
+ }
705
+
706
+ return sqlParts.join('');
707
+ }
708
+
709
+ // eslint-disable-next-line no-unused-vars
710
+ generateCreateTableStatementInnerTail(Model, options) {
711
+ let fieldParts = [];
712
+
713
+ Model.iterateFields(({ field }) => {
714
+ if (field.type.isVirtual())
715
+ return;
716
+
717
+ if (field.type.isForeignKey()) {
718
+ let result = this.generateForeignKeyConstraint(field, field.type);
719
+ if (result)
720
+ fieldParts.push(result);
721
+
722
+ return;
723
+ }
724
+ });
725
+
726
+ return fieldParts;
727
+ }
728
+
729
+ // eslint-disable-next-line no-unused-vars
730
+ generateCreateTableStatementOuterTail(Model, options) {
731
+ let fieldParts = [];
732
+
733
+ Model.iterateFields(({ field }) => {
734
+ if (field.type.isVirtual())
735
+ return;
736
+
737
+ if (field.type.isForeignKey())
738
+ return;
739
+
740
+ if (!field.index)
741
+ return;
742
+
743
+ let Model = field.Model;
744
+ let indexes = Nife.toArray(field.index).filter(Boolean);
745
+ for (let i = 0, il = indexes.length; i < il; i++) {
746
+ let index = indexes[i];
747
+ let result = this.generateColumnIndex(Model, field, index, options);
748
+ if (result)
749
+ fieldParts.push(result);
750
+ }
751
+ });
752
+
753
+ return fieldParts;
754
+ }
755
+
756
+ generateCreateTableStatement(Model, _options) {
757
+ let options = _options || {};
758
+ let fieldParts = [];
759
+
760
+ Model.iterateFields(({ field, fieldName }) => {
761
+ if (field.type.isVirtual())
762
+ return;
763
+
764
+ let columnName = field.columnName || fieldName;
765
+ let constraintParts = [];
766
+
767
+ let defaultValue = this.getFieldDefaultValue(field, fieldName, { remoteOnly: true });
768
+
769
+ if (field.primaryKey) {
770
+ if (field.primaryKey instanceof LiteralBase)
771
+ constraintParts.push(field.primaryKey.toString(this.connection));
772
+ else
773
+ constraintParts.push('PRIMARY KEY');
774
+
775
+ if (defaultValue !== 'AUTOINCREMENT')
776
+ constraintParts.push('NOT NULL');
777
+ } else {
778
+ if (field.unique) {
779
+ if (field.unique instanceof LiteralBase)
780
+ constraintParts.push(field.unique.toString(this.connection));
781
+ else
782
+ constraintParts.push('UNIQUE');
783
+ }
784
+
785
+ if (field.allowNull === false)
786
+ constraintParts.push('NOT NULL');
787
+ }
788
+
789
+ if (defaultValue != null && defaultValue !== '')
790
+ constraintParts.push(defaultValue);
791
+
792
+ constraintParts = constraintParts.join(' ');
793
+ if (Nife.isNotEmpty(constraintParts))
794
+ constraintParts = ` ${constraintParts}`;
795
+
796
+ fieldParts.push(` ${this.escapeID(columnName)} ${field.type.toConnectionType(this.connection, { createTable: true, defaultValue })}${constraintParts}`);
797
+ });
798
+
799
+ let ifNotExists = '';
800
+ if (options.ifNotExists === true)
801
+ ifNotExists = 'IF NOT EXISTS ';
802
+
803
+ let trailingParts = Nife.toArray(this.generateCreateTableStatementInnerTail(Model, options)).filter(Boolean);
804
+ if (Nife.isNotEmpty(trailingParts))
805
+ fieldParts = fieldParts.concat(trailingParts.map((part) => ` ${part.trim()}`));
806
+
807
+ let finalStatement = `CREATE TABLE ${ifNotExists}${this.escapeID(Model.getTableName(this.connection))} (${fieldParts.join(',\n')}\n);`;
808
+ return finalStatement;
809
+ }
810
+
811
+ generateInsertFieldValuesFromModel(model, _options) {
812
+ if (!model)
813
+ return '';
814
+
815
+ let options = _options || {};
816
+ let sqlParts = [];
817
+ let modelChanges = {};
818
+ let dirtyFields = model._getDirtyFields({ insert: true });
819
+
820
+ if (dirtyFields && Object.keys(dirtyFields).length === 0)
821
+ return '';
822
+
823
+ model.iterateFields(
824
+ ({ field, fieldName }) => {
825
+ if (field.type.isVirtual())
826
+ return;
827
+
828
+ if (!Object.prototype.hasOwnProperty.call(dirtyFields, fieldName)) {
829
+ sqlParts.push('');
830
+ return;
831
+ }
832
+
833
+ let dirtyField = dirtyFields[fieldName];
834
+ let fieldValue = dirtyField.current;
835
+
836
+ if (fieldValue === undefined)
837
+ fieldValue = null;
838
+
839
+ modelChanges[fieldName] = fieldValue;
840
+
841
+ if (fieldValue instanceof LiteralBase)
842
+ fieldValue = fieldValue.toString(this.connection);
843
+ else
844
+ fieldValue = this.escape(field, fieldValue);
845
+
846
+ sqlParts.push(fieldValue);
847
+ },
848
+ /* We need dirty fields for all models so the columns align */
849
+ options.dirtyFields || model.getDirtyFields(options),
850
+ );
851
+
852
+ return { modelChanges, rowValues: sqlParts.join(',') };
853
+ }
854
+
855
+ generateInsertValuesFromModels(Model, _models, _options) {
856
+ let options = _options || {};
857
+ let preparedModels = this.connection.prepareAllModelsForOperation(Model, _models, options);
858
+ let { models, dirtyFields } = preparedModels;
859
+ let allModelChanges = [];
860
+
861
+ if (Nife.isEmpty(models))
862
+ return '';
863
+
864
+ let sqlParts = [];
865
+ let subOptions = this.stackAssign(options, { dirtyFields });
866
+
867
+ for (let i = 0, il = models.length; i < il; i++) {
868
+ let model = models[i];
869
+ if (!(model instanceof ModelBase))
870
+ model = new Model(model);
871
+
872
+ let { rowValues, modelChanges } = this.generateInsertFieldValuesFromModel(model, subOptions);
873
+ allModelChanges.push(modelChanges);
874
+
875
+ sqlParts.push(`(${rowValues})`);
876
+ }
877
+
878
+ return {
879
+ modelChanges: allModelChanges,
880
+ values: (options.newlines === false) ? sqlParts.join(',') : sqlParts.join(',\n'),
881
+ };
882
+ }
883
+
884
+ // eslint-disable-next-line no-unused-vars
885
+ generateInsertStatementTail(Model, model, options, context) {
886
+ }
887
+
888
+ generateInsertStatement(Model, _models, _options) {
889
+ let options = _options || {};
890
+ let preparedModels = this.connection.prepareAllModelsForOperation(Model, _models, options);
891
+ let { models, dirtyFields } = preparedModels;
892
+ if (Nife.isEmpty(models) || Nife.isEmpty(dirtyFields))
893
+ return '';
894
+
895
+ let subOptions = this.stackAssign(options, {
896
+ asColumn: true,
897
+ columnNameOnly: true,
898
+ fields: dirtyFields,
899
+ dirtyFields,
900
+ });
901
+
902
+ let { values, modelChanges } = this.generateInsertValuesFromModels(Model, preparedModels, subOptions);
903
+ if (!values)
904
+ return '';
905
+
906
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
907
+ let escapedFieldNames = Array.from(Object.values(this.getEscapedModelFields(Model, subOptions)));
908
+
909
+ let insertStatementTail = this.generateInsertStatementTail(
910
+ Model,
911
+ models,
912
+ subOptions,
913
+ {
914
+ escapedTableName,
915
+ modelChanges,
916
+ dirtyFields,
917
+ },
918
+ );
919
+
920
+ if (insertStatementTail)
921
+ return `INSERT INTO ${escapedTableName} (${escapedFieldNames}) VALUES ${values} ${insertStatementTail}`;
922
+
923
+ return `INSERT INTO ${escapedTableName} (${escapedFieldNames}) VALUES ${values}`;
924
+ }
925
+
926
+ // eslint-disable-next-line no-unused-vars
927
+ generateUpdateStatementTail(Model, model, queryEngine, options, context) {
928
+ }
929
+
930
+ generateUpdateStatement(Model, _model, _queryEngine, _options) {
931
+ if (!_model)
932
+ return '';
933
+
934
+ let queryEngine = _queryEngine;
935
+ let options = this.stackAssign(_options, { isUpdateOperation: true });
936
+
937
+ if (!QueryEngine.isQuery(queryEngine)) {
938
+ queryEngine = null;
939
+ options = _queryEngine || {};
940
+ }
941
+
942
+ let model = _model;
943
+ if (!(model instanceof ModelBase)) {
944
+ let newModel = new Model();
945
+ newModel.clearDirty();
946
+ newModel.setAttributes(model);
947
+ model = newModel;
948
+ }
949
+
950
+ let modelChanges = model._getDirtyFields({ update: true });
951
+ let dirtyFieldNames = Object.keys(modelChanges);
952
+ let dirtyFields = model.getFields(dirtyFieldNames);
953
+ if (Nife.isEmpty(dirtyFields))
954
+ return '';
955
+
956
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
957
+ let sqlParts = [ 'UPDATE ', escapedTableName, ' SET ' ];
958
+ let setParts = [];
959
+ let tabs = '';
960
+
961
+ if (options.newlines !== false) {
962
+ sqlParts.push('\n');
963
+ tabs = ' ';
964
+ }
965
+
966
+ for (let i = 0, il = dirtyFields.length; i < il; i++) {
967
+ let dirtyField = dirtyFields[i];
968
+ let fieldValue = modelChanges[dirtyField.fieldName].current;
969
+ let escapedColumnName = this.escapeID(dirtyField.columnName);
970
+ let escapedValue = (fieldValue instanceof LiteralBase) ? fieldValue.toString(this.connection) : this.escape(dirtyField, fieldValue);
971
+ if (!escapedValue)
972
+ continue;
973
+
974
+ setParts.push(`${tabs}${escapedColumnName} = ${escapedValue}`);
975
+ }
976
+
977
+ if (setParts.length === 0)
978
+ return '';
979
+
980
+ sqlParts.push((options.newlines === false) ? setParts.join(',') : setParts.join(',\n'));
981
+
982
+ let where;
983
+ if (queryEngine) {
984
+ where = this.generateWhereAndOrderLimitOffset(queryEngine, options);
985
+ if (where) {
986
+ if (options.newlines !== false)
987
+ sqlParts.push('\n');
988
+ else
989
+ sqlParts.push(' ');
990
+
991
+ sqlParts.push(where);
992
+ }
993
+ }
994
+
995
+ let updateStatementTail = this.generateUpdateStatementTail(
996
+ Model,
997
+ model,
998
+ options,
999
+ {
1000
+ queryEngine,
1001
+ escapedTableName,
1002
+ modelChanges: [ modelChanges ],
1003
+ dirtyFields,
1004
+ where,
1005
+ },
1006
+ );
1007
+
1008
+ if (updateStatementTail)
1009
+ sqlParts.push(` ${updateStatementTail}`);
1010
+
1011
+ return sqlParts.join('');
1012
+ }
1013
+
1014
+ generateDeleteStatement(Model, _queryEngine, _options) {
1015
+ let queryEngine = _queryEngine;
1016
+ let options = _options;
1017
+
1018
+ if (queryEngine) {
1019
+ if (!QueryEngine.isQuery(queryEngine)) {
1020
+ let models = Nife.toArray(queryEngine);
1021
+ queryEngine = Utils.buildQueryFromModelsAttributes(Model, models);
1022
+ if (!queryEngine)
1023
+ throw new Error(`${this.constructor.name}::generateDeleteStatement: Data provided for "${Model.getModelName()}" model is insufficient to complete operation.`);
1024
+ }
1025
+ }
1026
+
1027
+ let escapedTableName = this.escapeID(Model.getTableName(this.connection));
1028
+ if (queryEngine) {
1029
+ let pkField = Model.getPrimaryKeyField();
1030
+ let where = this.generateWhereAndOrderLimitOffset(queryEngine, options);
1031
+
1032
+ if (where && pkField) {
1033
+ if (pkField)
1034
+ queryEngine = queryEngine.clone().PROJECT(`${Model.getModelName()}:${pkField.fieldName}`);
1035
+
1036
+ let innerSelect = this.generateSelectStatement(queryEngine, this.stackAssign(options, { noProjectionAliases: true }));
1037
+ let escapedColumnName = this.getEscapedColumnName(Model, pkField, options);
1038
+
1039
+ return `DELETE FROM ${escapedTableName} WHERE ${escapedColumnName} IN (${innerSelect})`;
1040
+ } else {
1041
+ return `DELETE FROM ${escapedTableName}${(where) ? ` ${where}` : ''}`;
1042
+ }
1043
+ } else {
1044
+ return `DELETE FROM ${escapedTableName}`;
1045
+ }
1046
+ }
1047
+
1048
+ _collectRemoteReturningFields(Model) {
1049
+ let remoteFieldNames = [];
1050
+
1051
+ Model.iterateFields(({ field }) => {
1052
+ if (field.type.isVirtual())
1053
+ return;
1054
+
1055
+ if (typeof field.defaultValue !== 'function')
1056
+ return;
1057
+
1058
+ if (!DefaultHelpers.checkDefaultValueFlags(field.defaultValue, [ 'remote' ]))
1059
+ return;
1060
+
1061
+ remoteFieldNames.push(this.getEscapedColumnName(field.Model, field));
1062
+ });
1063
+
1064
+ return remoteFieldNames;
1065
+ }
1066
+
1067
+ _collectReturningFields(Model, model, options, context) {
1068
+ let {
1069
+ modelChanges,
1070
+ dirtyFields,
1071
+ } = context;
1072
+
1073
+ let returnFieldsMap = {};
1074
+
1075
+ for (let i = 0, il = dirtyFields.length; i < il; i++) {
1076
+ let dirtyField = dirtyFields[i];
1077
+
1078
+ for (let j = 0, jl = modelChanges.length; j < jl; j++) {
1079
+ let thisModelChanges = modelChanges[j];
1080
+ let dirtyStatus = thisModelChanges[dirtyField.fieldName];
1081
+ if (!dirtyStatus)
1082
+ continue;
1083
+
1084
+ let fieldValue = dirtyStatus.current;
1085
+ if (!(fieldValue instanceof Literals.LiteralBase))
1086
+ continue;
1087
+
1088
+ if (!fieldValue.options.remote)
1089
+ continue;
1090
+
1091
+ let escapedColumnName = this.getEscapedColumnName(dirtyField.Model, dirtyField);
1092
+ returnFieldsMap[escapedColumnName] = true;
1093
+
1094
+ break;
1095
+ }
1096
+ }
1097
+
1098
+ let pkFieldName = Model.getPrimaryKeyFieldName();
1099
+ if (pkFieldName)
1100
+ returnFieldsMap[pkFieldName] = true;
1101
+
1102
+ // Always return fields marked as "remote"
1103
+ let remoteFieldNames = this._collectRemoteReturningFields(Model);
1104
+ for (let i = 0, il = remoteFieldNames.length; i < il; i++) {
1105
+ let fieldName = remoteFieldNames[i];
1106
+ returnFieldsMap[fieldName] = true;
1107
+ }
1108
+
1109
+ let returnFields = Object.keys(returnFieldsMap);
1110
+ if (!returnFields.length)
1111
+ return;
1112
+
1113
+ return `RETURNING ${returnFields.join(',')}`;
1114
+ }
1115
+ }
1116
+
1117
+ module.exports = SQLQueryGeneratorBase;