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.
@@ -17,6 +17,10 @@ function escape(value) {
17
17
  }
18
18
 
19
19
  const operators = {
20
+ unsafeSql({sql}, context) {
21
+ return sql;
22
+ },
23
+
20
24
  value({value}, context) {
21
25
  if (typeof value === 'boolean') {
22
26
  return value ? 'TRUE' : 'FALSE';
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 {ValuePick} = require('./picks.js');
23
- if (value instanceof ValuePick) {
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
  /**
@@ -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, {from});
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],
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xcraft-core-pickaxe",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Query builder",
5
5
  "main": "index.js",
6
6
  "scripts": {
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 = name !== tableName; // or compare on db
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
  });