oak-db 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.
@@ -0,0 +1,920 @@
1
+ import assert from 'assert';
2
+ import { assign, cloneDeep, intersection, keys, set } from 'lodash';
3
+ import { DateTime } from 'luxon';
4
+ import { Attribute, DeduceCreateOperationData, DeduceSorterAttr, DeduceSorterItem, EntityDict, Expression, EXPRESSION_PREFIX, Index, OperateOption, Q_FullTextValue, Ref, RefOrExpression, SelectOption, StorageSchema } from "oak-domain/lib/types";
5
+ import { DataType } from "oak-domain/lib/types/schema/DataTypes";
6
+ import { judgeRelation } from 'oak-domain/lib/store/relation';
7
+
8
+ export interface SqlSelectOption extends SelectOption {
9
+ };
10
+ export interface SqlOperateOption extends OperateOption {
11
+ };
12
+
13
+ export abstract class SqlTranslator<ED extends EntityDict> {
14
+ readonly schema: StorageSchema<ED>;
15
+ constructor(schema: StorageSchema<ED>) {
16
+ this.schema = this.makeFullSchema(schema);
17
+ }
18
+
19
+ private makeFullSchema(schema2: StorageSchema<ED>) {
20
+ const schema = cloneDeep(schema2);
21
+ for (const entity in schema) {
22
+ const { attributes, indexes } = schema[entity];
23
+ // 增加默认的属性
24
+ assign(attributes, {
25
+ id: {
26
+ type: 'char',
27
+ params: {
28
+ length: 36,
29
+ },
30
+ } as Attribute,
31
+ $$createAt$$: {
32
+ type: 'date',
33
+ notNull: true,
34
+ } as Attribute,
35
+ $$updateAt$$: {
36
+ type: 'date',
37
+ notNull: true,
38
+ } as Attribute,
39
+ $$deleteAt$$: {
40
+ type: 'date',
41
+ } as Attribute,
42
+ $$triggerData$$: {
43
+ type: 'object',
44
+ } as Attribute,
45
+ $$triggerTimestamp$$: {
46
+ type: 'date',
47
+ } as Attribute,
48
+ });
49
+
50
+ // 增加默认的索引
51
+ const intrinsticIndexes: Index<ED[keyof ED]['OpSchema']>[] = [
52
+ {
53
+ name: `${entity}_create_at`,
54
+ attributes: [{
55
+ name: '$$createAt$$',
56
+ }]
57
+ }, {
58
+ name: `${entity}_update_at`,
59
+ attributes: [{
60
+ name: '$$updateAt$$',
61
+ }],
62
+ }, {
63
+ name: `${entity}_trigger_ts`,
64
+ attributes: [{
65
+ name: '$$triggerTimestamp$$',
66
+ }],
67
+ }
68
+ ];
69
+
70
+ // 增加外键上的索引
71
+ for (const attr in attributes) {
72
+ if (attributes[attr].type === 'ref') {
73
+ if (!(indexes?.find(
74
+ ele => ele.attributes[0].name === attr
75
+ ))) {
76
+ intrinsticIndexes.push({
77
+ name: `${entity}_fk_${attr}`,
78
+ attributes: [{
79
+ name: attr,
80
+ }]
81
+ });
82
+ }
83
+ }
84
+
85
+ if (attr === 'entity' && attributes[attr].type === 'varchar') {
86
+ const entityIdDef = attributes.entityId;
87
+ if (entityIdDef?.type === 'varchar') {
88
+ if (!(indexes?.find(
89
+ ele => ele.attributes[0].name === 'entity' && ele.attributes[1]?.name === 'entityId'
90
+ ))) {
91
+ intrinsticIndexes.push({
92
+ name: `${entity}_fk_entity_entityId`,
93
+ attributes: [{
94
+ name: 'entity',
95
+ }, {
96
+ name: 'entityId',
97
+ }]
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ if (indexes) {
105
+ indexes.push(...intrinsticIndexes);
106
+ }
107
+ else {
108
+ assign(schema[entity], {
109
+ indexes: intrinsticIndexes,
110
+ });
111
+ }
112
+ }
113
+
114
+ return schema;
115
+ }
116
+
117
+ protected abstract getDefaultSelectFilter<OP extends SqlSelectOption>(alias: string, option?: OP): string;
118
+
119
+ protected abstract translateAttrProjection(dataType: DataType, alias: string, attr: string): string;
120
+
121
+ protected abstract translateAttrValue(dataType: DataType | Ref, value: any): string;
122
+
123
+ protected abstract translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string;
124
+
125
+ abstract translateCreateEntity<T extends keyof ED>(entity: T, option: { replace?: boolean }): string[];
126
+
127
+ protected abstract populateSelectStmt<T extends keyof ED, OP extends SqlSelectOption>(
128
+ projectionText: string,
129
+ fromText: string,
130
+ selection: ED[T]['Selection'],
131
+ aliasDict: Record<string, string>,
132
+ filterText: string,
133
+ sorterText?: string,
134
+ indexFrom?: number,
135
+ count?: number,
136
+ option?: OP): string;
137
+
138
+ protected abstract populateUpdateStmt<OP extends SqlOperateOption>(
139
+ updateText: string,
140
+ fromText: string,
141
+ aliasDict: Record<string, string>,
142
+ filterText: string,
143
+ sorterText?: string,
144
+ indexFrom?: number,
145
+ count?: number,
146
+ option?: OP): string;
147
+
148
+ protected abstract populateRemoveStmt<OP extends SqlOperateOption>(
149
+ removeText: string,
150
+ fromText: string,
151
+ aliasDict: Record<string, string>,
152
+ filterText: string,
153
+ sorterText?: string,
154
+ indexFrom?: number,
155
+ count?: number,
156
+ option?: OP): string;
157
+
158
+ protected abstract translateExpression<T extends keyof ED>(alias: string, expression: RefOrExpression<keyof ED[T]['OpSchema']>, refDict: Record<string, string>): string;
159
+
160
+ private getStorageName<T extends keyof ED>(entity: T) {
161
+ const { storageName } = this.schema[entity];
162
+ return (storageName || entity) as string;
163
+ }
164
+
165
+ translateInsert<T extends keyof ED>(entity: T, data: DeduceCreateOperationData<ED[T]['OpSchema']>[]): string {
166
+ const { schema } = this;
167
+ const { attributes, storageName = entity } = schema[entity];
168
+
169
+ let sql = `insert into \`${storageName as string}\`(`;
170
+
171
+ const attrs = Object.keys(data[0]).filter(
172
+ ele => attributes.hasOwnProperty(ele)
173
+ );
174
+ attrs.forEach(
175
+ (attr, idx) => {
176
+ sql += ` \`${attr}\``;
177
+ if (idx < attrs.length - 1) {
178
+ sql += ',';
179
+ }
180
+ }
181
+ );
182
+
183
+ sql += ', `$$createAt$$`, `$$updateAt$$`) values ';
184
+
185
+ const now = DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
186
+ data.forEach(
187
+ (d, dataIndex) => {
188
+ sql += '(';
189
+ attrs.forEach(
190
+ (attr, attrIdx) => {
191
+ const attrDef = attributes[attr];
192
+ const { type: dataType } = attrDef;
193
+ const value = this.translateAttrValue(dataType as DataType, d[attr]);
194
+ sql += value;
195
+ if (attrIdx < attrs.length - 1) {
196
+ sql += ',';
197
+ }
198
+ }
199
+ );
200
+ sql += `, '${now}', '${now}')`;
201
+ if (dataIndex < data.length - 1) {
202
+ sql += ',';
203
+ }
204
+ }
205
+ );
206
+
207
+ return sql;
208
+ }
209
+
210
+ /**
211
+ * analyze the join relations in projection/query/sort
212
+ * 所有的层次关系都当成left join处理,如果有内表为空的情况,请手动处理
213
+ * {
214
+ * b: {
215
+ * name: {
216
+ * $exists: false,
217
+ * }
218
+ * }
219
+ * }
220
+ * 这样的query会把内表为空的行也返回
221
+ * @param param0
222
+ */
223
+ private analyzeJoin<T extends keyof ED>(entity: T, { projection, filter, sorter, isStat }: {
224
+ projection?: ED[T]['Selection']['data'];
225
+ filter?: ED[T]['Selection']['filter'];
226
+ sorter?: ED[T]['Selection']['sorter'];
227
+ isStat?: true;
228
+ }, initialNumber?: number): {
229
+ aliasDict: Record<string, string>;
230
+ projectionRefAlias: Record<string, string>;
231
+ filterRefAlias: Record<string, string>;
232
+ from: string;
233
+ extraWhere: string;
234
+ currentNumber: number;
235
+ } {
236
+ const { schema } = this;
237
+ let number = initialNumber || 1;
238
+ const projectionRefAlias: Record<string, string> = {};
239
+ const filterRefAlias: Record<string, string> = {};
240
+ let extraWhere = '';
241
+
242
+ const alias = `${entity as string}_${number++}`;
243
+ let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
244
+ const aliasDict: Record<string, string> = {
245
+ './': alias,
246
+ };
247
+
248
+ const analyzeFilterNode = <E extends keyof ED>({ node, path, entityName, alias }: {
249
+ node: ED[E]['Selection']['filter'];
250
+ path: string;
251
+ entityName: E;
252
+ alias: string,
253
+ }): void => {
254
+ Object.keys(node!).forEach(
255
+ (op) => {
256
+ if (['$and', '$or'].includes(op)) {
257
+ (node![op] as ED[E]['Selection']['filter'][]).forEach(
258
+ (subNode) => analyzeFilterNode({
259
+ node: subNode,
260
+ path,
261
+ entityName,
262
+ alias,
263
+ })
264
+ );
265
+ }
266
+ else {
267
+ const rel = judgeRelation(this.schema, entityName, op);
268
+ if (typeof rel === 'string') {
269
+ let alias2: string;
270
+ const pathAttr = `${path}${op}/`;
271
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
272
+ alias2 = `${rel}_${number++}`;
273
+ assign(aliasDict, {
274
+ [pathAttr]: alias2,
275
+ });
276
+ from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${op}Id\` = \`${alias2}\`.\`id\``;
277
+ }
278
+ else {
279
+ alias2 = aliasDict[pathAttr];
280
+ }
281
+ analyzeFilterNode({
282
+ node: node![op],
283
+ path: pathAttr,
284
+ entityName: rel,
285
+ alias: alias2,
286
+ });
287
+ }
288
+ else if (rel === 2) {
289
+ let alias2: string;
290
+ const pathAttr = `${path}${op}/`;
291
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
292
+ alias2 = `${op}_${number++}`;
293
+ assign(aliasDict, {
294
+ [pathAttr]: alias2,
295
+ });
296
+ from += ` left join \`${this.getStorageName(op)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
297
+ extraWhere += `\`${alias}\`.\`entity\` = '${op}'`;
298
+ }
299
+ else {
300
+ alias2 = aliasDict[pathAttr];
301
+ }
302
+ analyzeFilterNode({
303
+ node: node![op],
304
+ path: pathAttr,
305
+ entityName: rel,
306
+ alias: alias2,
307
+ });
308
+ }
309
+ else {
310
+ // 不支持一对多
311
+ assert(rel === 0 || rel === 1);
312
+ }
313
+ }
314
+ }
315
+ );
316
+ if (node!['#id']) {
317
+ assert(!filterRefAlias[node!['#id']]);
318
+ assign(filterRefAlias, {
319
+ [node!['#id']]: alias,
320
+ });
321
+ }
322
+ };
323
+ if (filter) {
324
+ analyzeFilterNode({
325
+ node: filter,
326
+ path: './',
327
+ entityName: entity,
328
+ alias,
329
+ });
330
+ }
331
+
332
+ const analyzeSortNode = <E extends keyof ED>({ node, path, entityName, alias }: {
333
+ node: DeduceSorterAttr<ED[E]['Schema']>;
334
+ path: string;
335
+ entityName: E;
336
+ alias: string;
337
+ }): void => {
338
+ const attr = keys(node)[0];
339
+
340
+ const rel = judgeRelation(this.schema, entityName, attr);
341
+ if (typeof rel === 'string') {
342
+ const pathAttr = `${path}${attr}/`;
343
+ let alias2: string;
344
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
345
+ alias2 = `${rel}_${number++}`;
346
+ assign(aliasDict, {
347
+ [pathAttr]: alias2,
348
+ });
349
+ from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
350
+ }
351
+ else {
352
+ alias2 = aliasDict[pathAttr];
353
+ }
354
+ analyzeSortNode({
355
+ node: node[attr] as any,
356
+ path: pathAttr,
357
+ entityName: rel,
358
+ alias: alias2,
359
+ });
360
+ }
361
+ else if (rel === 2) {
362
+ const pathAttr = `${path}${attr}/`;
363
+ let alias2: string;
364
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
365
+ alias2 = `${attr}_${number++}`;
366
+ assign(aliasDict, {
367
+ [pathAttr]: alias2,
368
+ });
369
+ from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
370
+ extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
371
+ }
372
+ else {
373
+ alias2 = aliasDict[pathAttr];
374
+ }
375
+ analyzeSortNode({
376
+ node: node[attr] as any,
377
+ path: pathAttr,
378
+ entityName: attr,
379
+ alias: alias2,
380
+ });
381
+ }
382
+ else {
383
+ assert(rel === 0 || rel === 1);
384
+ }
385
+ };
386
+ if (sorter) {
387
+ sorter.forEach(
388
+ (sortNode) => {
389
+ analyzeSortNode({
390
+ node: sortNode.$attr,
391
+ path: './',
392
+ entityName: entity,
393
+ alias,
394
+ });
395
+ }
396
+ );
397
+ }
398
+
399
+ const analyzeProjectionNode = <E extends keyof ED>({ node, path, entityName, alias }: {
400
+ node: ED[E]['Selection']['data'];
401
+ path: string;
402
+ entityName: E;
403
+ alias: string;
404
+ }): void => {
405
+ const { attributes } = schema[entityName];
406
+
407
+ Object.keys(node).forEach(
408
+ (attr) => {
409
+ const rel = judgeRelation(this.schema, entityName, attr);
410
+ if (typeof rel === 'string') {
411
+ const pathAttr = `${path}${attr}/`;
412
+
413
+ let alias2: string;
414
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
415
+ alias2 = `${rel}_${number++}`;
416
+ assign(aliasDict, {
417
+ [pathAttr]: alias2,
418
+ });
419
+ from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
420
+ }
421
+ else {
422
+ alias2 = aliasDict[pathAttr];
423
+ }
424
+
425
+ analyzeProjectionNode({
426
+ node: node[attr],
427
+ path: pathAttr,
428
+ entityName: rel,
429
+ alias: alias2,
430
+ });
431
+ }
432
+ else if (rel === 2) {
433
+ const pathAttr = `${path}${attr}/`;
434
+
435
+ let alias2: string;
436
+ if (!aliasDict.hasOwnProperty(pathAttr)) {
437
+ alias2 = `${attr}_${number++}`;
438
+ assign(aliasDict, {
439
+ [pathAttr]: alias2,
440
+ });
441
+ from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
442
+ }
443
+ else {
444
+ alias2 = aliasDict[pathAttr];
445
+ }
446
+
447
+ analyzeProjectionNode({
448
+ node: node[attr],
449
+ path: pathAttr,
450
+ entityName: attr,
451
+ alias: alias2,
452
+ });
453
+ extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
454
+ }
455
+ }
456
+ );
457
+ if (node['#id']) {
458
+ assert(!projectionRefAlias[node['#id']]);
459
+ assign(projectionRefAlias, {
460
+ [node['#id']]: alias,
461
+ });
462
+ }
463
+ };
464
+
465
+ if (projection) {
466
+ analyzeProjectionNode({ node: projection, path: './', entityName: entity, alias });
467
+ }
468
+
469
+ return {
470
+ aliasDict,
471
+ from,
472
+ projectionRefAlias,
473
+ filterRefAlias,
474
+ extraWhere,
475
+ currentNumber: number,
476
+ };
477
+ }
478
+
479
+ private translateComparison(attr: string, value: any, type: DataType | Ref): string {
480
+ const SQL_OP: {
481
+ [op: string]: string,
482
+ } = {
483
+ $gt: '>',
484
+ $lt: '<',
485
+ $gte: '>=',
486
+ $lte: '<=',
487
+ $eq: '=',
488
+ $ne: '<>',
489
+ };
490
+
491
+ if (Object.keys(SQL_OP).includes(attr)) {
492
+ return ` ${SQL_OP[attr]} ${this.translateAttrValue(type, value)}`;
493
+ }
494
+
495
+ switch (attr) {
496
+ case '$startsWith': {
497
+ return ` like '${value}%'`;
498
+ }
499
+ case '$endsWith': {
500
+ return ` like '%${value}'`;
501
+ }
502
+ case '$includes': {
503
+ return ` like '%${value}%'`;
504
+ }
505
+ default: {
506
+ throw new Error(`unrecoganized comparison operator ${attr}`);
507
+ }
508
+ }
509
+ }
510
+
511
+ private translateElement(attr: string, value: boolean): string {
512
+ assert(attr === '$exists'); // only support one operator now
513
+ if (value) {
514
+ return ' is not null';
515
+ }
516
+ return ' is null';
517
+ }
518
+
519
+ private translateEvaluation<T extends keyof ED>(attr: string, value: any, entity: T, alias: string, type: DataType | Ref, initialNumber: number, refAlias: Record<string, string>): {
520
+ stmt: string;
521
+ currentNumber: number;
522
+ } {
523
+ switch (attr) {
524
+ case '$text': {
525
+ // fulltext search
526
+ return {
527
+ stmt: this.translateFullTextSearch(value, entity, alias),
528
+ currentNumber: initialNumber,
529
+ };
530
+ }
531
+ case '$in':
532
+ case '$nin': {
533
+ const IN_OP = {
534
+ $in: 'in',
535
+ $nin: 'not in',
536
+ };
537
+ if (value instanceof Array) {
538
+ const values = value.map(
539
+ (v) => {
540
+ if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type as string)) {
541
+ return `'${v}'`;
542
+ }
543
+ else {
544
+ return `${v}`;
545
+ }
546
+ }
547
+ );
548
+ if (values.length > 0) {
549
+ return {
550
+ stmt: ` ${IN_OP[attr]}(${values.join(',')})`,
551
+ currentNumber: initialNumber,
552
+ };
553
+ }
554
+ else {
555
+ if (attr === '$in') {
556
+ return {
557
+ stmt: ' in (null)',
558
+ currentNumber: initialNumber,
559
+ };
560
+ }
561
+ else {
562
+ return {
563
+ stmt: ' is not null',
564
+ currentNumber: initialNumber,
565
+ };
566
+ }
567
+ }
568
+ }
569
+ else {
570
+ // sub query
571
+ const {stmt: subQueryStmt, currentNumber } = this.translateSelectInner(value.entity, value, initialNumber, refAlias, undefined);
572
+ return {
573
+ stmt: ` ${IN_OP[attr]}(${subQueryStmt})`,
574
+ currentNumber,
575
+ };
576
+ }
577
+ }
578
+ default: {
579
+ assert('$between' === attr);
580
+ const values = value.map(
581
+ (v: string | number) => {
582
+ if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type as string)) {
583
+ return `'${v}'`;
584
+ }
585
+ else {
586
+ return `${v}`;
587
+ }
588
+ }
589
+ );
590
+ return {
591
+ stmt: ` between ${values[0]} and ${values[1]}`,
592
+ currentNumber: initialNumber,
593
+ };
594
+ }
595
+ }
596
+ }
597
+
598
+ private translateFilter<T extends keyof ED, OP extends SqlSelectOption>(
599
+ entity: T,
600
+ selection: Pick<ED[T]['Selection'], 'filter'>,
601
+ aliasDict: Record<string, string>,
602
+ filterRefAlias: Record<string, string>,
603
+ initialNumber: number,
604
+ extraWhere?: string,
605
+ option?: OP): {
606
+ stmt: string;
607
+ currentNumber: number;
608
+ } {
609
+ const { schema } = this;
610
+ const { filter } = selection;
611
+
612
+ let currentNumber = initialNumber;
613
+ const translateInner = <E extends keyof ED>(entity2: E, path: string, filter2?: ED[E]['Selection']['filter'], type?: DataType | Ref): string => {
614
+ const alias = aliasDict[path];
615
+ const { attributes } = schema[entity2];
616
+ let whereText = type ? '' : this.getDefaultSelectFilter(alias, option);
617
+ if (filter2) {
618
+ const attrs = Object.keys(filter2).filter(
619
+ ele => !ele.startsWith('#')
620
+ );
621
+ attrs.forEach(
622
+ (attr) => {
623
+ if (whereText) {
624
+ whereText += ' and '
625
+ }
626
+ whereText + '(';
627
+ if (['$and', '$or', '$xor', '$not'].includes(attr)) {
628
+ let result = '';
629
+ switch (attr) {
630
+ case '$and':
631
+ case '$or':
632
+ case '$xor': {
633
+ const logicQueries = filter2[attr];
634
+ logicQueries.forEach(
635
+ (logicQuery: ED[E]['Selection']['filter'], index: number) => {
636
+ const sql = translateInner(entity2, path, logicQuery);
637
+ if (sql) {
638
+ whereText += ` (${sql})`;
639
+ if (index < logicQueries.length - 1) {
640
+ whereText += ` ${attr.slice(1)}`;
641
+ }
642
+ }
643
+ }
644
+ );
645
+ break;
646
+ }
647
+ default: {
648
+ assert(attr === '$not');
649
+ const logicQuery = filter2[attr];
650
+ const sql = translateInner(entity2, path, logicQuery);
651
+ if (sql) {
652
+ whereText += ` not (${translateInner(entity2, path, logicQuery)})`;
653
+ break;
654
+ }
655
+ }
656
+ }
657
+ }
658
+ else if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
659
+ // expression
660
+ whereText += ` (${this.translateExpression(alias, filter2[attr], filterRefAlias)})`;
661
+ }
662
+ else if (['$gt', '$gte', '$lt', '$lte', '$eq', '$ne', '$startsWith', '$endsWith', '$includes'].includes(attr)) {
663
+ whereText += this.translateComparison(attr, filter2[attr], type!);
664
+ }
665
+ else if (['$exists'].includes(attr)) {
666
+ whereText += this.translateElement(attr, filter2[attr]);
667
+ }
668
+ else if (['$text', '$in', '$nin', '$between'].includes(attr)) {
669
+ const { stmt, currentNumber: cn2 } = this.translateEvaluation(attr, filter2[attr], entity2, alias, type!, initialNumber, filterRefAlias);
670
+ whereText += stmt;
671
+ currentNumber = cn2;
672
+ }
673
+ else {
674
+ const rel = judgeRelation(this.schema, entity, attr);
675
+ if (rel === 2) {
676
+ whereText += ` ${translateInner(attr, `${path}${attr}/`, filter2[attr])}`;
677
+ }
678
+ else if (typeof rel === 'string') {
679
+ whereText += ` ${translateInner(rel, `${path}${attr}/`, filter2[attr])}`;
680
+ }
681
+ else {
682
+ assert(attributes.hasOwnProperty(attr), `非法的属性${attr}`);
683
+ const { type: type2 } = attributes[attr];
684
+ // assert (type2 !== 'ref');
685
+ if (typeof filter2[attr] === 'object' && Object.keys(filter2[attr])[0] && Object.keys(filter2[attr])[0].startsWith('$')) {
686
+ whereText += ` \`${alias}\`.\`${attr}\` ${translateInner(entity2, path, filter2[attr], type2)}`
687
+ }
688
+ else {
689
+ whereText += ` \`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])}`;
690
+ }
691
+ }
692
+ }
693
+
694
+ whereText + ')';
695
+ }
696
+ );
697
+ }
698
+ if (!whereText) {
699
+ whereText = 'true'; // 如果为空就赋一个永真条件,以便处理and
700
+ }
701
+ return whereText;
702
+ };
703
+
704
+ const where = translateInner(entity, './', filter);
705
+ if (extraWhere && where) {
706
+ return {
707
+ stmt: `${extraWhere} and ${where}`,
708
+ currentNumber,
709
+ };
710
+ }
711
+ return {
712
+ stmt: extraWhere || where,
713
+ currentNumber,
714
+ };
715
+ }
716
+
717
+ private translateSorter<T extends keyof ED>(entity: T, sorter: ED[T]['Selection']['sorter'], aliasDict: Record<string, string>): string {
718
+ const translateInner = <E extends keyof ED>(entity2: E, sortAttr: DeduceSorterAttr<ED[E]['Schema']>, path: string): string => {
719
+ assert(Object.keys(sortAttr).length === 1);
720
+ const attr = Object.keys(sortAttr)[0];
721
+ const alias = aliasDict[path];
722
+
723
+ if (attr.toLocaleLowerCase().startsWith(EXPRESSION_PREFIX)) {
724
+ return this.translateExpression(alias, sortAttr[attr] as any, {});
725
+ }
726
+ else if (sortAttr[attr] === 1) {
727
+ return `\`${alias}\`.\`${attr}\``;
728
+ }
729
+ else {
730
+ const rel = judgeRelation(this.schema, entity2, attr);
731
+ if (typeof rel === 'string') {
732
+ return translateInner(rel, sortAttr[attr] as any, `${path}${attr}/`);
733
+ }
734
+ else {
735
+ assert(rel === 2);
736
+ return translateInner(attr, sortAttr[attr] as any, `${path}${attr}/`);
737
+ }
738
+ }
739
+ };
740
+
741
+ let sortText = '';
742
+ sorter!.forEach(
743
+ (sortNode, index) => {
744
+ const { $attr, $direction } = sortNode;
745
+ sortText += translateInner(entity, $attr, './');
746
+ if ($direction) {
747
+ sortText += ` ${$direction}`;
748
+ }
749
+
750
+ if (index < sorter!.length - 1) {
751
+ sortText += ',';
752
+ }
753
+ }
754
+ );
755
+
756
+ return sortText;
757
+ }
758
+
759
+ private translateProjection<T extends keyof ED>(
760
+ entity: T,
761
+ projection: ED[T]['Selection']['data'],
762
+ aliasDict: Record<string, string>,
763
+ projectionRefAlias: Record<string, string>): string {
764
+ const { schema } = this;
765
+ const translateInner = <E extends keyof ED>(entity2: E, projection2: ED[E]['Selection']['data'], path: string): string => {
766
+ const alias = aliasDict[path];
767
+ const { attributes } = schema[entity2];
768
+ let projText = '';
769
+
770
+ let prefix = path.slice(2).replace(/\//g, '.');
771
+ const attrs = Object.keys(projection2).filter(
772
+ (attr) => {
773
+ if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
774
+ return true;
775
+ }
776
+ const rel = judgeRelation(this.schema, entity2, attr);
777
+ return [1, 2].includes(rel as number) || typeof rel === 'string';
778
+ }
779
+ );
780
+ attrs.forEach(
781
+ (attr, idx) => {
782
+ if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
783
+ const exprText = this.translateExpression(alias, projection2[attr], projectionRefAlias);
784
+ projText += ` ${exprText} as ${prefix}${attr}`;
785
+ }
786
+ else {
787
+ const rel = judgeRelation(this.schema, entity2, attr);
788
+ if (typeof rel === 'string') {
789
+ projText += translateInner(rel, projection2[attr], `${path}${attr}/`);
790
+ }
791
+ else if (rel === 2) {
792
+ projText += translateInner(attr, projection2[attr], `${path}${attr}/`);
793
+ }
794
+ else if (rel === 1) {
795
+ const { type } = attributes[attr];
796
+ if (projection2[attr] === 1) {
797
+ projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix}${attr}\``;
798
+ }
799
+ else {
800
+ assert(typeof projection2 === 'string');
801
+ projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix}${projection2[attr]}\``;
802
+ }
803
+ }
804
+ }
805
+ if (idx < attrs.length - 1) {
806
+ projText += ',';
807
+ }
808
+ }
809
+ );
810
+
811
+ return projText;
812
+ };
813
+
814
+ return translateInner(entity, projection, './');
815
+ }
816
+
817
+ private translateSelectInner<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: ED[T]['Selection'], initialNumber: number, refAlias: Record<string, string>, option?: OP): {
818
+ stmt: string;
819
+ currentNumber: number;
820
+ } {
821
+ const { data, filter, sorter, indexFrom, count } = selection;
822
+ const { from: fromText, aliasDict, projectionRefAlias, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
823
+ projection: data,
824
+ filter,
825
+ sorter,
826
+ }, initialNumber);
827
+ assert(intersection(keys(refAlias), keys(filterRefAlias)).length === 0, 'filter中的#node结点定义有重复');
828
+ assign(refAlias, filterRefAlias);
829
+
830
+ const projText = this.translateProjection(entity, data, aliasDict, projectionRefAlias);
831
+
832
+ const { stmt: filterText, currentNumber: currentNumber2 } = this.translateFilter(entity, selection, aliasDict, refAlias, currentNumber, extraWhere, option);
833
+
834
+ const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
835
+
836
+ return {
837
+ stmt: this.populateSelectStmt(projText, fromText, selection, aliasDict, filterText, sorterText, indexFrom, count, option),
838
+ currentNumber: currentNumber2,
839
+ };
840
+ }
841
+
842
+ translateSelect<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: ED[T]['Selection'], option?: OP): string {
843
+ const { stmt } = this.translateSelectInner(entity, selection, 1, {}, option);
844
+ return stmt;
845
+ }
846
+
847
+ translateCount<T extends keyof ED, OP extends SqlSelectOption>(entity: T, selection: Pick<ED[T]['Selection'], 'filter' | 'count'>, option?: OP): string {
848
+ const { filter, count } = selection;
849
+ const { from: fromText, aliasDict, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
850
+ filter,
851
+ });
852
+
853
+ const projText = 'count(1)';
854
+
855
+ const { stmt: filterText } = this.translateFilter(entity, selection as ED[T]['Selection'], aliasDict, filterRefAlias, currentNumber, extraWhere, option);
856
+
857
+ if (count) {
858
+ const subQuerySql = this.populateSelectStmt('1', fromText, Object.assign({}, selection, { indexFrom: 0 }) as ED[T]['Selection'], aliasDict, filterText, undefined, undefined, undefined, option);
859
+ return `select count(1) from (${subQuerySql})`;
860
+ }
861
+ return this.populateSelectStmt(projText, fromText, selection as ED[T]['Selection'], aliasDict, filterText, undefined, undefined, undefined, option);
862
+ }
863
+
864
+ translateRemove<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Remove'], option?: OP): string {
865
+ const { filter, sorter, indexFrom, count } = operation;
866
+ const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
867
+
868
+ const alias = aliasDict['./'];
869
+
870
+ const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
871
+
872
+ const sorterText = sorter && sorter.length > 0 ? this.translateSorter(entity, sorter, aliasDict) : undefined;
873
+
874
+ return this.populateRemoveStmt(alias, fromText, aliasDict, filterText, sorterText, indexFrom, count, option);
875
+ }
876
+
877
+ translateUpdate<T extends keyof ED, OP extends SqlOperateOption>(entity: T, operation: ED[T]['Update'], option?: OP): string {
878
+ const { attributes } = this.schema[entity];
879
+ const { filter, sorter, indexFrom, count, data } = operation;
880
+ const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
881
+
882
+ const alias = aliasDict['./'];
883
+
884
+ let updateText = '';
885
+ for (const attr in data) {
886
+ if (updateText) {
887
+ updateText += ',';
888
+ }
889
+ assert(attributes.hasOwnProperty(attr) && attributes[attr].type !== 'ref');
890
+ const value = this.translateAttrValue(attributes[attr].type as DataType, data[attr]);
891
+ updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
892
+ }
893
+
894
+ const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
895
+ const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
896
+
897
+ return this.populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, option);
898
+ }
899
+
900
+ translateDestroyEntity(entity: string, truncate?: boolean): string {
901
+ const { schema } = this;
902
+ const { storageName = entity, view } = schema[entity];
903
+
904
+ let sql: string;
905
+ if (view) {
906
+ sql = `drop view if exists \`${storageName}\``;
907
+ }
908
+ else {
909
+ sql = truncate ? `truncate table \`${storageName}\`` : `drop table if exists \`${storageName}\``;
910
+ }
911
+
912
+ return sql;
913
+ }
914
+
915
+
916
+ escapeStringValue(value: string): string {
917
+ const result = `'${value.replace(/'/g, '\\\'')}'`;
918
+ return result;
919
+ }
920
+ }