xcraft-core-pickaxe 0.1.22 → 0.1.23
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/lib/operator-to-sql.js +4 -0
- package/lib/operators.js +10 -3
- package/lib/picks.js +22 -1
- package/lib/query-builder.js +57 -4
- package/lib/query-to-sql.js +28 -2
- package/package.json +1 -1
- package/test/sql.spec.js +142 -5
package/lib/operator-to-sql.js
CHANGED
package/lib/operators.js
CHANGED
|
@@ -19,8 +19,8 @@ function op(value) {
|
|
|
19
19
|
if (type === 'object' && 'operator' in value) {
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
|
-
const {
|
|
23
|
-
if (value
|
|
22
|
+
const {isAnyPick} = require('./picks.js');
|
|
23
|
+
if (isAnyPick(value)) {
|
|
24
24
|
return value.value;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -28,6 +28,13 @@ function op(value) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const operators = {
|
|
31
|
+
unsafeSql(sql) {
|
|
32
|
+
return /** @type {const} */ ({
|
|
33
|
+
operator: 'unsafeSql',
|
|
34
|
+
sql,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
|
|
31
38
|
value(value) {
|
|
32
39
|
return /** @type {const} */ ({
|
|
33
40
|
operator: 'value',
|
|
@@ -407,7 +414,7 @@ const operators = {
|
|
|
407
414
|
* @typedef {Operators["count"] | Operators["avg"] | Operators["max"] | Operators["min"] | Operators["sum"] | Operators["groupArray"]} Aggregator
|
|
408
415
|
*/
|
|
409
416
|
/**
|
|
410
|
-
* @typedef {Operators["eq"] | Operators["neq"] | Operators["and"] | Operators["or"] | Operators["not"] | Operators["gt"] | Operators["gte"] | Operators["lt"] | Operators["lte"] | Operators["in"] | Operators["includes"] | Operators["some"] | Operators["glob"] | Operators["like"] | Operators["match"]} BooleanOperator
|
|
417
|
+
* @typedef {Operators["eq"] | Operators["neq"] | Operators["and"] | Operators["or"] | Operators["not"] | Operators["gt"] | Operators["gte"] | Operators["lt"] | Operators["lte"] | Operators["in"] | Operators["includes"] | Operators["some"] | Operators["glob"] | Operators["like"] | Operators["match"] | Operators["unsafeSql"]} BooleanOperator
|
|
411
418
|
*/
|
|
412
419
|
/**
|
|
413
420
|
* @typedef {Operators["abs"] | Operators["plus"] | Operators["minus"]} MathOperator
|
package/lib/picks.js
CHANGED
|
@@ -517,7 +517,8 @@ function isAnyPick(value) {
|
|
|
517
517
|
value instanceof ValuePick ||
|
|
518
518
|
value instanceof ObjectPick ||
|
|
519
519
|
value instanceof RecordPick ||
|
|
520
|
-
value instanceof ArrayPick
|
|
520
|
+
value instanceof ArrayPick ||
|
|
521
|
+
value instanceof RowPick
|
|
521
522
|
);
|
|
522
523
|
}
|
|
523
524
|
|
|
@@ -539,6 +540,21 @@ class RowPick {
|
|
|
539
540
|
this.#context = context;
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
get type() {
|
|
544
|
+
return this.#type;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
get value() {
|
|
548
|
+
const {tableName, field, path} = this.#context;
|
|
549
|
+
if (path && path.length > 0) {
|
|
550
|
+
return $o.get($o.field(field, tableName), path);
|
|
551
|
+
}
|
|
552
|
+
if (field) {
|
|
553
|
+
return $o.field(field, tableName);
|
|
554
|
+
}
|
|
555
|
+
throw new Error('Bad RowPick value');
|
|
556
|
+
}
|
|
557
|
+
|
|
542
558
|
/**
|
|
543
559
|
* @template {keyof T} K
|
|
544
560
|
* @param {K} fieldName
|
|
@@ -568,6 +584,11 @@ class RowPick {
|
|
|
568
584
|
get(fieldName) {
|
|
569
585
|
return this.field(fieldName);
|
|
570
586
|
}
|
|
587
|
+
|
|
588
|
+
isRoot() {
|
|
589
|
+
const path = this.#context.path;
|
|
590
|
+
return !path || path.length === 0;
|
|
591
|
+
}
|
|
571
592
|
}
|
|
572
593
|
|
|
573
594
|
/**
|
package/lib/query-builder.js
CHANGED
|
@@ -28,6 +28,10 @@ const {queryToSql} = require('./query-to-sql.js');
|
|
|
28
28
|
* @typedef {import("./picks.js").BooleanValue} BooleanValue
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {{name: string, query: QueryObj}} WithResult
|
|
33
|
+
*/
|
|
34
|
+
|
|
31
35
|
/**
|
|
32
36
|
* TODO: add FinalQuery
|
|
33
37
|
* @typedef {Operators["each"]} Subquery
|
|
@@ -112,7 +116,7 @@ function getTableName(table) {
|
|
|
112
116
|
if (typeof table === 'string') {
|
|
113
117
|
return table;
|
|
114
118
|
}
|
|
115
|
-
if ('alias' in table) {
|
|
119
|
+
if ('alias' in table && table.alias !== undefined) {
|
|
116
120
|
return table.alias;
|
|
117
121
|
}
|
|
118
122
|
if ('table' in table) {
|
|
@@ -137,10 +141,11 @@ function getTableName(table) {
|
|
|
137
141
|
/**
|
|
138
142
|
* @typedef {{
|
|
139
143
|
* explain?: boolean,
|
|
144
|
+
* withs?: WithResult[],
|
|
140
145
|
* from: QueryTable,
|
|
141
146
|
* scope?: ObjectPick<any>
|
|
142
147
|
* joins?: JoinsResult[],
|
|
143
|
-
* select: SelectResult,
|
|
148
|
+
* select: SelectResult | '*',
|
|
144
149
|
* selectOneField?: boolean,
|
|
145
150
|
* distinct?: boolean,
|
|
146
151
|
* where?: Operator,
|
|
@@ -643,6 +648,29 @@ class FromQuery {
|
|
|
643
648
|
return new SelectQuery(this.#picks, queryParts, this.#database);
|
|
644
649
|
}
|
|
645
650
|
|
|
651
|
+
/**
|
|
652
|
+
* @returns {SelectQuery<T, t<T[number]>>}
|
|
653
|
+
*/
|
|
654
|
+
selectAll() {
|
|
655
|
+
const isRoot = this.#picks.every((pick) => pick.isRoot());
|
|
656
|
+
/** @type {QueryObj["select"]} */
|
|
657
|
+
let select;
|
|
658
|
+
if (isRoot) {
|
|
659
|
+
select = '*';
|
|
660
|
+
} else {
|
|
661
|
+
select = Object.fromEntries(
|
|
662
|
+
this.#picks.flatMap((pick) =>
|
|
663
|
+
Object.keys(pick.type.properties).map((key) => [key, pick.get(key)])
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const queryParts = {
|
|
668
|
+
...this.#queryParts,
|
|
669
|
+
select,
|
|
670
|
+
};
|
|
671
|
+
return new SelectQuery(this.#picks, queryParts, this.#database);
|
|
672
|
+
}
|
|
673
|
+
|
|
646
674
|
/**
|
|
647
675
|
* @param {(...args: FctArgs<T>) => BooleanValue} fct
|
|
648
676
|
* @returns {FromQuery<T>}
|
|
@@ -664,18 +692,21 @@ class FromQuery {
|
|
|
664
692
|
class QueryBuilder {
|
|
665
693
|
#database;
|
|
666
694
|
#getTableSchema;
|
|
695
|
+
#withs;
|
|
667
696
|
|
|
668
697
|
/**
|
|
669
698
|
* @param {Object} options
|
|
670
699
|
* @param {*} [options.database]
|
|
671
700
|
* @param {DbSchema} [options.schema]
|
|
672
701
|
* @param {(tableName: string, shape: AnyObjectShape) => AnyTableSchema} [options.getTableSchema]
|
|
702
|
+
* @param {WithResult[]} [options.withs]
|
|
673
703
|
*/
|
|
674
|
-
constructor({database, schema, getTableSchema} = {}) {
|
|
704
|
+
constructor({database, schema, getTableSchema, withs} = {}) {
|
|
675
705
|
this.#database = database;
|
|
676
706
|
this.#getTableSchema =
|
|
677
707
|
getTableSchema ??
|
|
678
708
|
((tableName) => (schema ? schema[tableName] : {table: tableName}));
|
|
709
|
+
this.#withs = withs;
|
|
679
710
|
}
|
|
680
711
|
|
|
681
712
|
/**
|
|
@@ -692,6 +723,25 @@ class QueryBuilder {
|
|
|
692
723
|
return this.#database;
|
|
693
724
|
}
|
|
694
725
|
|
|
726
|
+
/**
|
|
727
|
+
* @param {string} name
|
|
728
|
+
* @param {FinalQuery<any>} query
|
|
729
|
+
* @returns {QueryBuilder}
|
|
730
|
+
*/
|
|
731
|
+
with(name, query) {
|
|
732
|
+
return new QueryBuilder({
|
|
733
|
+
database: this.#database,
|
|
734
|
+
getTableSchema: this.#getTableSchema,
|
|
735
|
+
withs: [
|
|
736
|
+
...(this.#withs ?? []),
|
|
737
|
+
{
|
|
738
|
+
name,
|
|
739
|
+
query: query.query,
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
695
745
|
/**
|
|
696
746
|
* @template {AnyObjectShape} T
|
|
697
747
|
* @param {string} tableName
|
|
@@ -701,7 +751,10 @@ class QueryBuilder {
|
|
|
701
751
|
from(tableName, shape) {
|
|
702
752
|
const tableSchema = this.#getTableSchema(tableName, shape);
|
|
703
753
|
const from = tableSchema;
|
|
704
|
-
const {pick, queryParts} = useTableSchema(tableSchema, shape, {
|
|
754
|
+
const {pick, queryParts} = useTableSchema(tableSchema, shape, {
|
|
755
|
+
from,
|
|
756
|
+
withs: this.#withs,
|
|
757
|
+
});
|
|
705
758
|
return new FromQuery(
|
|
706
759
|
this.#getTableSchema,
|
|
707
760
|
[pick],
|
package/lib/query-to-sql.js
CHANGED
|
@@ -7,6 +7,28 @@
|
|
|
7
7
|
const {joinOperators} = require('./join-operators.js');
|
|
8
8
|
const {sql} = require('./operator-to-sql.js');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @param {QueryObj["withs"]} withs
|
|
12
|
+
* @param {OperatorToSqlContext} context
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function withSql(withs, context) {
|
|
16
|
+
if (!withs || withs.length === 0) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
let resultSql = 'WITH ';
|
|
20
|
+
resultSql += withs
|
|
21
|
+
.map(({name, query}) => {
|
|
22
|
+
let sql = '';
|
|
23
|
+
sql += `${name} AS (\n`;
|
|
24
|
+
sql += queryToSql(query, context.values).sql;
|
|
25
|
+
sql += '\n)';
|
|
26
|
+
return sql;
|
|
27
|
+
})
|
|
28
|
+
.join(', ');
|
|
29
|
+
resultSql += '\n';
|
|
30
|
+
return resultSql;
|
|
31
|
+
}
|
|
10
32
|
/**
|
|
11
33
|
* @param {QueryObj["from"]} table
|
|
12
34
|
* @param {OperatorToSqlContext} context
|
|
@@ -41,6 +63,9 @@ function tableSql(table, context) {
|
|
|
41
63
|
* @returns {string}
|
|
42
64
|
*/
|
|
43
65
|
function selectFields(select, context) {
|
|
66
|
+
if (select === '*') {
|
|
67
|
+
return '*';
|
|
68
|
+
}
|
|
44
69
|
if (Array.isArray(select)) {
|
|
45
70
|
return select.map((value) => sql(value, context)).join(', ');
|
|
46
71
|
}
|
|
@@ -107,11 +132,12 @@ function queryToSql(query, values = []) {
|
|
|
107
132
|
useTableNames: query.joins && query.joins.length > 0,
|
|
108
133
|
equalOperator: 'IS',
|
|
109
134
|
};
|
|
110
|
-
const explain = query.explain ? 'EXPLAIN QUERY PLAN
|
|
135
|
+
const explain = query.explain ? 'EXPLAIN QUERY PLAN\n' : '';
|
|
136
|
+
const withs = withSql(query.withs, context);
|
|
111
137
|
const distinct = query.distinct ? 'DISTINCT ' : '';
|
|
112
138
|
// Note: query.from is not validated
|
|
113
139
|
const from = tableSql(query.from, context);
|
|
114
|
-
let result = `${explain}SELECT ${distinct}${selectFields(
|
|
140
|
+
let result = `${explain}${withs}SELECT ${distinct}${selectFields(
|
|
115
141
|
query.select,
|
|
116
142
|
context
|
|
117
143
|
)}`;
|
package/package.json
CHANGED
package/test/sql.spec.js
CHANGED
|
@@ -101,10 +101,11 @@ describe('xcraft.pickaxe', function () {
|
|
|
101
101
|
* @returns {FromQuery<[GetShape<T>]>}
|
|
102
102
|
*/
|
|
103
103
|
function queryAction(tableName, shape) {
|
|
104
|
+
const mainDb = getDb(tableName);
|
|
104
105
|
const builder = new QueryBuilder({
|
|
105
106
|
getTableSchema: (name, shape) => {
|
|
106
107
|
const db = getDb(name);
|
|
107
|
-
const isJoinedDb =
|
|
108
|
+
const isJoinedDb = db !== mainDb;
|
|
108
109
|
if (isJoinedDb) {
|
|
109
110
|
console.log(`Attach ${name}`);
|
|
110
111
|
}
|
|
@@ -207,6 +208,40 @@ describe('xcraft.pickaxe', function () {
|
|
|
207
208
|
expect(result.values).to.be.deep.equal(values);
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
it('select all', function () {
|
|
212
|
+
const builder = new QueryBuilder()
|
|
213
|
+
.from('test_table', TestUserShape)
|
|
214
|
+
.selectAll()
|
|
215
|
+
.where((user, $) => user.field('role').eq('admin'));
|
|
216
|
+
|
|
217
|
+
const result = queryToSql(builder.query, null);
|
|
218
|
+
|
|
219
|
+
const sql = `
|
|
220
|
+
SELECT *
|
|
221
|
+
FROM test_table
|
|
222
|
+
WHERE role IS 'admin'
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('includes unsafe sql', function () {
|
|
229
|
+
const builder = new QueryBuilder()
|
|
230
|
+
.from('test_table', TestUserShape)
|
|
231
|
+
.selectAll()
|
|
232
|
+
.where((user, $) => $.unsafeSql(`role IS 'admin'`));
|
|
233
|
+
|
|
234
|
+
const result = queryToSql(builder.query, null);
|
|
235
|
+
|
|
236
|
+
const sql = `
|
|
237
|
+
SELECT *
|
|
238
|
+
FROM test_table
|
|
239
|
+
WHERE role IS 'admin'
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
243
|
+
});
|
|
244
|
+
|
|
210
245
|
it('pick string', function () {
|
|
211
246
|
const builder = new QueryBuilder()
|
|
212
247
|
.from('test_table', TestUserShape)
|
|
@@ -422,6 +457,31 @@ describe('xcraft.pickaxe', function () {
|
|
|
422
457
|
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
423
458
|
});
|
|
424
459
|
|
|
460
|
+
it('select all in query action', function () {
|
|
461
|
+
const builder = queryAction('users', TestUserShape)
|
|
462
|
+
.selectAll()
|
|
463
|
+
.where((user) => user.get('age').gt(10));
|
|
464
|
+
|
|
465
|
+
const result = queryToSql(builder.query, null);
|
|
466
|
+
|
|
467
|
+
const keys = Object.keys(new TestUserShape());
|
|
468
|
+
const sql = `
|
|
469
|
+
SELECT
|
|
470
|
+
${keys
|
|
471
|
+
.map(
|
|
472
|
+
(key) => `json_extract(action, '$.payload.state.${key}') AS ${key}`
|
|
473
|
+
)
|
|
474
|
+
.join(', ')}
|
|
475
|
+
FROM action_table
|
|
476
|
+
WHERE (
|
|
477
|
+
entityType IS 'users' AND
|
|
478
|
+
json_extract(action, '$.payload.state.age') > 10
|
|
479
|
+
)
|
|
480
|
+
`;
|
|
481
|
+
|
|
482
|
+
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
483
|
+
});
|
|
484
|
+
|
|
425
485
|
it('join query action', function () {
|
|
426
486
|
const builder = queryAction('users', TestUserShape)
|
|
427
487
|
.leftJoin('notes', TestNoteShape, (user, note) =>
|
|
@@ -440,17 +500,94 @@ describe('xcraft.pickaxe', function () {
|
|
|
440
500
|
|
|
441
501
|
const sql = `
|
|
442
502
|
SELECT
|
|
443
|
-
json_extract(action, '$.payload.state.id') AS id,
|
|
503
|
+
json_extract(action_table.action, '$.payload.state.id') AS id,
|
|
444
504
|
json_extract(notes.action, '$.payload.state.text') AS noteText
|
|
445
505
|
FROM action_table
|
|
446
506
|
LEFT JOIN notes_db.action_table AS notes ON
|
|
447
|
-
SUBSTR(json_extract(notes.action, '$.payload.state.id'), 0, 6) = json_extract(action, '$.payload.state.id')
|
|
507
|
+
SUBSTR(json_extract(notes.action, '$.payload.state.id'), 0, 6) = json_extract(action_table.action, '$.payload.state.id')
|
|
448
508
|
WHERE (
|
|
449
|
-
(entityType IS 'users' AND notes.entityType IS 'notes') AND
|
|
450
|
-
json_extract(action, '$.payload.state.age') > 10
|
|
509
|
+
(action_table.entityType IS 'users' AND notes.entityType IS 'notes') AND
|
|
510
|
+
json_extract(action_table.action, '$.payload.state.age') > 10
|
|
451
511
|
)
|
|
452
512
|
`;
|
|
453
513
|
|
|
454
514
|
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
455
515
|
});
|
|
516
|
+
|
|
517
|
+
it('with clause', function () {
|
|
518
|
+
const builder = new QueryBuilder()
|
|
519
|
+
.with(
|
|
520
|
+
'admins',
|
|
521
|
+
new QueryBuilder()
|
|
522
|
+
.from('test_table', TestUserShape)
|
|
523
|
+
.selectAll()
|
|
524
|
+
.where((user) => user.field('role').eq('admin'))
|
|
525
|
+
)
|
|
526
|
+
.from('admins', TestUserShape)
|
|
527
|
+
.field('id')
|
|
528
|
+
.where((user) => user.field('age').gt(42));
|
|
529
|
+
|
|
530
|
+
const result = queryToSql(builder.query, null);
|
|
531
|
+
|
|
532
|
+
const sql = `
|
|
533
|
+
WITH admins AS (
|
|
534
|
+
SELECT *
|
|
535
|
+
FROM test_table
|
|
536
|
+
WHERE role IS 'admin'
|
|
537
|
+
)
|
|
538
|
+
SELECT
|
|
539
|
+
id
|
|
540
|
+
FROM admins
|
|
541
|
+
WHERE age > 42
|
|
542
|
+
`;
|
|
543
|
+
|
|
544
|
+
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('with clause in query action', function () {
|
|
548
|
+
const builder = new QueryBuilder()
|
|
549
|
+
.with(
|
|
550
|
+
'admins',
|
|
551
|
+
new QueryBuilder()
|
|
552
|
+
.from('action_table', ActionShape(TestUserShape))
|
|
553
|
+
.selectAll()
|
|
554
|
+
.where((row, $) =>
|
|
555
|
+
$.and(
|
|
556
|
+
row.get('entityType').eq('users'),
|
|
557
|
+
row
|
|
558
|
+
.get('action')
|
|
559
|
+
.get('payload')
|
|
560
|
+
.get('state')
|
|
561
|
+
.get('role')
|
|
562
|
+
.eq('admin')
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
)
|
|
566
|
+
.from('admins', ActionShape(TestUserShape))
|
|
567
|
+
.select((row) => ({
|
|
568
|
+
id: row.get('action').get('payload').get('state').get('id'),
|
|
569
|
+
}))
|
|
570
|
+
.where((row) =>
|
|
571
|
+
row.get('action').get('payload').get('state').get('age').gt(10)
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const result = queryToSql(builder.query, null);
|
|
575
|
+
|
|
576
|
+
const sql = `
|
|
577
|
+
WITH admins AS (
|
|
578
|
+
SELECT *
|
|
579
|
+
FROM action_table
|
|
580
|
+
WHERE (
|
|
581
|
+
entityType IS 'users' AND
|
|
582
|
+
json_extract(action, '$.payload.state.role') IS 'admin'
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
SELECT
|
|
586
|
+
json_extract(action, '$.payload.state.id') AS id
|
|
587
|
+
FROM admins
|
|
588
|
+
WHERE json_extract(action, '$.payload.state.age') > 10
|
|
589
|
+
`;
|
|
590
|
+
|
|
591
|
+
expect(trimSql(result.sql)).to.be.equal(trimSql(sql));
|
|
592
|
+
});
|
|
456
593
|
});
|