happy-sql 2.4.1 → 2.5.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.
package/ChangeLog CHANGED
@@ -1,3 +1,9 @@
1
+ 2026.11.15, v2.5.0
2
+
3
+ feature:
4
+ - 1c961c1 happy-sql: apply type checker
5
+ - a4f7d90 happy-sql: where: keep parentheses in nested expressions
6
+
1
7
  2026.11.14, v2.4.1
2
8
 
3
9
  fix:
@@ -89,13 +89,22 @@ const convertCollate = (node) => callExpression(identifier('collate'), [
89
89
  convertOperand(node.right),
90
90
  ]);
91
91
 
92
- const convertIsNull = (node) => callExpression(identifier('isNull'), [
93
- convertOperand(node.left),
94
- ]);
92
+ const isNullLiteral = ({type}) => type === 'null_literal';
95
93
 
96
- const convertIsNotNull = (node) => callExpression(identifier('isNotNull'), [
97
- convertOperand(node.left),
98
- ]);
94
+ const createIs = (nullName, name) => (node) => {
95
+ if (isNullLiteral(node.right))
96
+ return callExpression(identifier(nullName), [
97
+ convertOperand(node.left),
98
+ ]);
99
+
100
+ return callExpression(identifier(name), [
101
+ convertOperand(node.left),
102
+ convertOperand(node.right),
103
+ ]);
104
+ };
105
+
106
+ const convertIsNull = createIs('isNull', 'is');
107
+ const convertIsNotNull = createIs('isNotNull', 'isNot');
99
108
 
100
109
  const convertIn = (node) => {
101
110
  const inner = node.right.expr;
@@ -1,7 +1,9 @@
1
1
  import {types} from '@putout/babel';
2
- import {convertBetween} from '#parser/clause/where';
2
+ import {
3
+ convertCondition,
4
+ convertWhere,
5
+ } from '#parser/clause/where';
3
6
  import {convertClause} from '#parser/clause';
4
- import {convertBinary} from '#parser/clause/binary';
5
7
  import {
6
8
  convertOperand,
7
9
  convertMemberExpr,
@@ -20,59 +22,13 @@ import {
20
22
  const {
21
23
  identifier,
22
24
  callExpression,
23
- logicalExpression,
24
25
  stringLiteral,
25
26
  tsAsExpression,
26
27
  tsLiteralType,
27
28
  } = types;
28
29
 
29
- const isExists = (node) => node.type === 'prefix_op_expr' && node.operator.name === 'EXISTS';
30
- const isNotExists = (node) => node.type === 'prefix_op_expr' && node.operator.name === 'NOT' && isExists(node.expr);
31
30
  const isParenExpr = (node) => node.type === 'paren_expr';
32
31
 
33
- const POSTFIX_OP = {
34
- ISNULL: 'isNull',
35
- NOTNULL: 'isNotNull',
36
- };
37
-
38
- const convertPostfix = (node) => {
39
- const {name} = node.operator;
40
- const fn = POSTFIX_OP[name];
41
-
42
- return callExpression(identifier(fn), [
43
- convertOperand(node.expr),
44
- ]);
45
- };
46
-
47
- const convertCondition = (node) => {
48
- if (node.operator && node.operator.name === 'AND')
49
- return logicalExpression('&&', convertCondition(node.left), convertCondition(node.right));
50
-
51
- if (node.operator && node.operator.name === 'OR')
52
- return logicalExpression('||', convertCondition(node.left), convertCondition(node.right));
53
-
54
- if (node.type === 'between_expr')
55
- return convertBetween(node);
56
-
57
- if (node.type === 'postfix_op_expr')
58
- return convertPostfix(node);
59
-
60
- if (isNotExists(node))
61
- return callExpression(identifier('notExists'), [
62
- convertClause(node.expr.expr.expr),
63
- ]);
64
-
65
- if (isExists(node))
66
- return callExpression(identifier('exists'), [
67
- convertClause(node.expr.expr),
68
- ]);
69
-
70
- if (isParenExpr(node))
71
- return convertCondition(node.expr);
72
-
73
- return convertBinary(node);
74
- };
75
-
76
32
  const convertOn = (spec) => callExpression(identifier('on'), [
77
33
  convertCondition(spec.expr),
78
34
  ]);
@@ -241,9 +197,7 @@ export const convertFrom = ({expr, whereClause, groupByClause, havingClause, ord
241
197
  const fromArgs = [];
242
198
 
243
199
  if (whereClause)
244
- fromArgs.push(callExpression(identifier('where'), [
245
- convertCondition(whereClause.expr),
246
- ]));
200
+ fromArgs.push(convertWhere(whereClause));
247
201
 
248
202
  if (groupByClause)
249
203
  fromArgs.push(convertGroupBy(groupByClause));
@@ -131,6 +131,16 @@ export const convertOperand = (node) => {
131
131
  convertClause(node.expr),
132
132
  ]);
133
133
 
134
+ if (node.type === 'paren_expr') {
135
+ const inner = convertOperand(node.expr);
136
+
137
+ inner.extra = {
138
+ parenthesized: true,
139
+ };
140
+
141
+ return inner;
142
+ }
143
+
134
144
  if (node.type === 'parameter')
135
145
  return stringLiteral(node.text);
136
146
 
@@ -10,6 +10,7 @@ import {convertBinary} from '#parser/clause/binary';
10
10
  import {
11
11
  convertCondition,
12
12
  convertBetween,
13
+ convertWhere,
13
14
  } from '#parser/clause/where';
14
15
  import {findClause} from '#parser/clause/find';
15
16
  import {convertOrderBy} from '../from/order-by.js';
@@ -312,6 +313,9 @@ export const convertSelect = ({clauses}) => {
312
313
  ].filter(Boolean);
313
314
 
314
315
  if (!fromClause) {
316
+ if (whereClause)
317
+ args.push(convertWhere(whereClause));
318
+
315
319
  if (limitClause)
316
320
  args.push(convertLimit(limitClause));
317
321
 
@@ -7,11 +7,22 @@ const {
7
7
  logicalExpression,
8
8
  identifier,
9
9
  callExpression,
10
+ isLogicalExpression,
10
11
  } = types;
11
12
 
12
13
  const convertAnd = (node) => logicalExpression('&&', convertCondition(node.left), convertCondition(node.right));
13
14
  const convertOr = (node) => logicalExpression('||', convertCondition(node.left), convertCondition(node.right));
14
15
 
16
+ const isNot = (node) => node.type === 'prefix_op_expr' && node.operator.name === 'NOT';
17
+
18
+ const keepParens = (node) => {
19
+ node.extra = {
20
+ parenthesized: true,
21
+ };
22
+
23
+ return node;
24
+ };
25
+
15
26
  const isNotBetween = (node) => Array.isArray(node.betweenKw) && node.betweenKw[0].name === 'NOT';
16
27
 
17
28
  export const convertBetween = (node) => {
@@ -68,12 +79,28 @@ export const convertCondition = (node) => {
68
79
  convertClause(node.expr.expr),
69
80
  ]);
70
81
 
71
- if (isParenExpr(node))
72
- return convertCondition(node.expr);
82
+ if (isNot(node) && isParenExpr(node.expr))
83
+ return callExpression(identifier('not'), [
84
+ convertCondition(node.expr.expr),
85
+ ]);
86
+
87
+ if (isParenExpr(node)) {
88
+ const result = convertCondition(node.expr);
89
+
90
+ if (isLogicalExpression(result))
91
+ return keepParens(result);
92
+
93
+ return result;
94
+ }
73
95
 
74
96
  return convertBinary(node);
75
97
  };
76
98
 
99
+ const stripParens = (node) => {
100
+ delete node.extra;
101
+ return node;
102
+ };
103
+
77
104
  export const convertWhere = (node) => callExpression(identifier('where'), [
78
- convertCondition(node.expr),
105
+ stripParens(convertCondition(node.expr)),
79
106
  ]);
@@ -1,3 +1,7 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {isBinaryExpression} = types;
4
+
1
5
  const JS_TO_SQL_OP = {
2
6
  '===': '=',
3
7
  '!==': '!=',
@@ -7,11 +11,30 @@ const JS_TO_SQL_OP = {
7
11
  '<': '<',
8
12
  };
9
13
 
10
- export const BinaryExpression = (path, {write, traverse}) => {
11
- traverse(path.get('left'));
14
+ const ADDITIVE = ['+', '-'];
15
+ const MULTIPLICATIVE = ['*', '/', '%'];
16
+
17
+ const needsParens = (node, operator) => {
18
+ if (!isBinaryExpression(node))
19
+ return false;
20
+
21
+ return MULTIPLICATIVE.includes(operator) && ADDITIVE.includes(node.operator);
22
+ };
23
+
24
+ const print = (path, operator, {write, traverse}) => {
25
+ if (!needsParens(path.node, operator))
26
+ return traverse(path);
12
27
 
13
- const op = path.node.operator;
28
+ write('(');
29
+ traverse(path);
30
+ write(')');
31
+ };
32
+
33
+ export const BinaryExpression = (path, printer) => {
34
+ const {write} = printer;
35
+ const {operator} = path.node;
14
36
 
15
- write(` ${JS_TO_SQL_OP[op] || op} `);
16
- traverse(path.get('right'));
37
+ print(path.get('left'), operator, printer);
38
+ write(` ${JS_TO_SQL_OP[operator] || operator} `);
39
+ print(path.get('right'), operator, printer);
17
40
  };
@@ -138,10 +138,15 @@ import {lastInsertRowid} from './last-insert-rowid/last-insert-rowid.js';
138
138
  import {lastval} from './lastval/lastval.js';
139
139
  import {defaultNextval} from './default-nextval/default-nextval.js';
140
140
  import {like} from './like/like.js';
141
- import {isNull, isNotNull} from './is-null/is-null.js';
142
141
  import {orReplace} from './or-replace/or-replace.js';
143
142
  import {orIgnore} from './or-ignore/or-ignore.js';
144
143
  import {inList} from './in-list/in-list.js';
144
+ import {
145
+ isNull,
146
+ isNotNull,
147
+ is,
148
+ isNot,
149
+ } from './is-null/is-null.js';
145
150
  import {
146
151
  identity,
147
152
  identityByDefault,
@@ -315,6 +320,8 @@ export const clauses = {
315
320
  isDistinctFrom,
316
321
  collate,
317
322
  isNull,
323
+ is,
324
+ isNot,
318
325
  isNotNull,
319
326
  inList,
320
327
  identity,
@@ -1,4 +1,5 @@
1
1
  import {types} from '@putout/babel';
2
+ import {createTypeChecker} from '@putout/printer/type-checker';
2
3
 
3
4
  const {isCallExpression} = types;
4
5
 
@@ -6,7 +7,15 @@ const isConcurrently = (arg) => isCallExpression(arg.node) && arg.node.callee.na
6
7
  const isIfNotExists = (arg) => isCallExpression(arg.node) && arg.node.callee.name === 'ifNotExists';
7
8
  const isWhere = (arg) => isCallExpression(arg.node) && arg.node.callee.name === 'where';
8
9
  const isAccessMethod = (arg) => isCallExpression(arg.node) && arg.node.callee.name === 'accessMethod';
9
- const isCol = (arg) => !isConcurrently(arg) && !isIfNotExists(arg) && !isWhere(arg) && !isAccessMethod(arg);
10
+
11
+ const isOption = createTypeChecker([
12
+ ['+: node.callee.name', '=', 'concurrently'],
13
+ ['+: node.callee.name', '=', 'ifNotExists'],
14
+ ['+: node.callee.name', '=', 'where'],
15
+ ['+: node.callee.name', '=', 'accessMethod'],
16
+ ]);
17
+
18
+ const isCol = (arg) => !isOption(arg);
10
19
 
11
20
  export const printIndexBody = (path, {write, traverse}) => {
12
21
  const [name, table, ...rest] = path.get('arguments');
@@ -1,14 +1,12 @@
1
1
  import {types} from '@putout/babel';
2
+ import {createTypeChecker} from '@putout/printer/type-checker';
2
3
 
3
- const {
4
- isTSAsExpression,
5
- isCallExpression,
6
- } = types;
4
+ const {isTSAsExpression} = types;
7
5
 
8
6
  const printAsExpression = (path, {write, traverse}) => {
9
7
  const expr = path.get('expression');
10
8
 
11
- if (isTableFunc(expr.node))
9
+ if (isTableFunc(expr))
12
10
  printTableFunc(expr, {
13
11
  write,
14
12
  traverse,
@@ -16,7 +14,7 @@ const printAsExpression = (path, {write, traverse}) => {
16
14
  else
17
15
  traverse(expr);
18
16
 
19
- if (isSubqueryAlias(path.node.expression))
17
+ if (isSubqueryAlias(expr))
20
18
  write(' ');
21
19
  else
22
20
  write(' AS ');
@@ -24,44 +22,52 @@ const printAsExpression = (path, {write, traverse}) => {
24
22
  write(path.node.typeAnnotation.literal.value);
25
23
  };
26
24
 
27
- const isSubqueryAlias = (node) => isCallExpression(node) && [
28
- 'subquery',
29
- 'lateralSubquery',
30
- ].includes(node.callee.name);
25
+ const isSubqueryAlias = createTypeChecker([
26
+ ['+: node.callee.name', '=', 'subquery'],
27
+ ['+: node.callee.name', '=', 'lateralSubquery'],
28
+ ]);
31
29
 
32
- const CLAUSE_NAMES = [
33
- 'where',
34
- 'groupBy',
35
- 'having',
36
- 'namedWindow',
37
- 'orderBy',
38
- 'limit',
39
- 'limitAll',
40
- 'offset',
41
- 'fetchFirst',
42
- 'forUpdate',
43
- ];
30
+ const isTableFunc = createTypeChecker([
31
+ ['-: node.callee.name', '=', 'where'],
32
+ ['-: node.callee.name', '=', 'groupBy'],
33
+ ['-: node.callee.name', '=', 'having'],
34
+ ['-: node.callee.name', '=', 'namedWindow'],
35
+ ['-: node.callee.name', '=', 'orderBy'],
36
+ ['-: node.callee.name', '=', 'limit'],
37
+ ['-: node.callee.name', '=', 'limitAll'],
38
+ ['-: node.callee.name', '=', 'offset'],
39
+ ['-: node.callee.name', '=', 'fetchFirst'],
40
+ ['-: node.callee.name', '=', 'forUpdate'],
41
+ ['-: node.callee.name', '=', 'join'],
42
+ ['-: node.callee.name', '=', 'leftJoin'],
43
+ ['-: node.callee.name', '=', 'rightJoin'],
44
+ ['-: node.callee.name', '=', 'rightOuterJoin'],
45
+ ['-: node.callee.name', '=', 'innerJoin'],
46
+ ['-: node.callee.name', '=', 'leftOuterJoin'],
47
+ ['-: node.callee.name', '=', 'fullOuterJoin'],
48
+ ['-: node.callee.name', '=', 'naturalJoin'],
49
+ ['-: node.callee.name', '=', 'crossJoin'],
50
+ ['-: node.callee.name', '=', 'on'],
51
+ ['-: node.callee.name', '=', 'subquery'],
52
+ ['-: node.callee.name', '=', 'lateralJoin'],
53
+ ['-: node.callee.name', '=', 'lateralSubquery'],
54
+ ['-: node.callee.name', '=', 'indexedBy'],
55
+ ['-: node.callee.name', '=', 'notIndexed'],
56
+ '+: node -> CallExpression',
57
+ ]);
44
58
 
45
- const TABLE_ARG_NAMES = [
46
- ...CLAUSE_NAMES,
47
- 'join',
48
- 'leftJoin',
49
- 'rightJoin',
50
- 'rightOuterJoin',
51
- 'innerJoin',
52
- 'leftOuterJoin',
53
- 'fullOuterJoin',
54
- 'naturalJoin',
55
- 'crossJoin',
56
- 'on',
57
- 'subquery',
58
- 'lateralJoin',
59
- 'lateralSubquery',
60
- 'indexedBy',
61
- 'notIndexed',
62
- ];
63
-
64
- const isClause = ({node}) => isCallExpression(node) && CLAUSE_NAMES.includes(node.callee.name);
59
+ const isClause = createTypeChecker([
60
+ ['+: node.callee.name', '=', 'where'],
61
+ ['+: node.callee.name', '=', 'groupBy'],
62
+ ['+: node.callee.name', '=', 'having'],
63
+ ['+: node.callee.name', '=', 'namedWindow'],
64
+ ['+: node.callee.name', '=', 'orderBy'],
65
+ ['+: node.callee.name', '=', 'limit'],
66
+ ['+: node.callee.name', '=', 'limitAll'],
67
+ ['+: node.callee.name', '=', 'offset'],
68
+ ['+: node.callee.name', '=', 'fetchFirst'],
69
+ ['+: node.callee.name', '=', 'forUpdate'],
70
+ ]);
65
71
 
66
72
  const CAMEL_RE = /([A-Z])/g;
67
73
  const toSnake = (a) => a.replace(CAMEL_RE, (_, c) => `_${c.toLowerCase()}`);
@@ -76,8 +82,6 @@ const toFuncName = (name) => {
76
82
  return toSnake(name);
77
83
  };
78
84
 
79
- const isTableFunc = (node) => isCallExpression(node) && !TABLE_ARG_NAMES.includes(node.callee.name);
80
-
81
85
  const printTableFunc = (path, {write, traverse}) => {
82
86
  const {name} = path.node.callee;
83
87
  const args = path.get('arguments');
@@ -102,7 +106,7 @@ const printArg = (path, printer) => {
102
106
  if (isTSAsExpression(node))
103
107
  return printAsExpression(path, printer);
104
108
 
105
- if (isTableFunc(node))
109
+ if (isTableFunc(path))
106
110
  return printTableFunc(path, printer);
107
111
 
108
112
  traverse(path);
@@ -9,3 +9,17 @@ export const isNotNull = (path, {write, traverse}) => {
9
9
  traverse(args[0]);
10
10
  write(' IS NOT NULL');
11
11
  };
12
+
13
+ export const is = (path, {write, traverse}) => {
14
+ const args = path.get('arguments');
15
+ traverse(args[0]);
16
+ write(' IS ');
17
+ traverse(args[1]);
18
+ };
19
+
20
+ export const isNot = (path, {write, traverse}) => {
21
+ const args = path.get('arguments');
22
+ traverse(args[0]);
23
+ write(' IS NOT ');
24
+ traverse(args[1]);
25
+ };
@@ -1,21 +1,17 @@
1
1
  import {types} from '@putout/babel';
2
+ import {createTypeChecker} from '@putout/printer/type-checker';
2
3
 
3
- const {
4
- isTSAsExpression,
5
- isCallExpression,
6
- } = types;
4
+ const {isTSAsExpression} = types;
7
5
 
8
- const isSubqueryAlias = (node) => isCallExpression(node) && [
9
- 'subquery',
10
- 'lateralSubquery',
11
- ].includes(node.callee.name);
6
+ const isSubqueryAlias = createTypeChecker([
7
+ ['+: node.callee.name', '=', 'subquery'],
8
+ ['+: node.callee.name', '=', 'lateralSubquery'],
9
+ ]);
12
10
 
13
11
  const printAsExpression = (path, {write, traverse}) => {
14
- const {expression} = path.node;
15
-
16
12
  traverse(path.get('expression'));
17
13
 
18
- if (isSubqueryAlias(expression))
14
+ if (isSubqueryAlias(path.get('expression')))
19
15
  write(' ');
20
16
  else
21
17
  write(' AS ');
@@ -1,4 +1,16 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {isLogicalExpression} = types;
4
+
1
5
  export const not = (path, {write, traverse}) => {
6
+ const [arg] = path.get('arguments');
7
+
2
8
  write('NOT ');
3
- traverse(path.get('arguments')[0]);
9
+
10
+ if (!isLogicalExpression(arg.node))
11
+ return traverse(arg);
12
+
13
+ write('(');
14
+ traverse(arg);
15
+ write(')');
4
16
  };
@@ -29,12 +29,13 @@ const isFrom = createTypeChecker([
29
29
  ['+: node.callee.name', '=', 'from'],
30
30
  ]);
31
31
 
32
- const isClauseName = (path) => isCallExpression(path.node) && [
33
- 'limitAll',
34
- 'offset',
35
- 'fetchFirst',
36
- 'forUpdate',
37
- ].includes(path.node.callee.name);
32
+ const isClauseName = createTypeChecker([
33
+ ['+: node.callee.name', '=', 'limitAll'],
34
+ ['+: node.callee.name', '=', 'offset'],
35
+ ['+: node.callee.name', '=', 'fetchFirst'],
36
+ ['+: node.callee.name', '=', 'forUpdate'],
37
+ ['+: node.callee.name', '=', 'where'],
38
+ ]);
38
39
 
39
40
  const isDistinctOn = (path) => isCallExpression(path.node) && path.node.callee.name === 'distinctOn';
40
41
 
@@ -1,15 +1,11 @@
1
- import {types} from '@putout/babel';
1
+ import {createTypeChecker} from '@putout/printer/type-checker';
2
2
 
3
- const {isCallExpression} = types;
4
-
5
- const OR_ACTION_NAMES = [
6
- 'orReplace',
7
- 'orIgnore',
8
- 'orAbort',
9
- 'orFail',
10
- ];
11
-
12
- const isOrAction = (arg) => isCallExpression(arg.node) && OR_ACTION_NAMES.includes(arg.node.callee.name);
3
+ const isOrAction = createTypeChecker([
4
+ ['+: node.callee.name', '=', 'orReplace'],
5
+ ['+: node.callee.name', '=', 'orIgnore'],
6
+ ['+: node.callee.name', '=', 'orAbort'],
7
+ ['+: node.callee.name', '=', 'orFail'],
8
+ ]);
13
9
 
14
10
  export const update = (path, {write, traverse}) => {
15
11
  const args = path.get('arguments');
@@ -1,6 +1,11 @@
1
1
  import {types} from '@putout/babel';
2
2
 
3
- const {isLogicalExpression} = types;
3
+ const {
4
+ isLogicalExpression,
5
+ isCallExpression,
6
+ } = types;
7
+
8
+ const isInNot = ({parent}) => isCallExpression(parent) && parent.callee.name === 'not';
4
9
  const needsParensIn = (node, op) => op === '&&' && isLogicalExpression(node) && node.operator === '||';
5
10
 
6
11
  const printNestedOr = (node, printer) => {
@@ -18,7 +23,7 @@ export const LogicalExpression = (path, {write, traverse}) => {
18
23
  const left = path.get('left');
19
24
  const right = path.get('right');
20
25
  const hasNestedParen = needsParensIn(left.node, node.operator) || needsParensIn(right.node, node.operator);
21
- const separator = hasNestedParen ? ` ${op} ` : `\n${op} `;
26
+ const separator = hasNestedParen || isInNot(path) ? ` ${op} ` : `\n${op} `;
22
27
 
23
28
  if (needsParensIn(left.node, node.operator))
24
29
  printNestedOr(left, {
@@ -1,7 +1,21 @@
1
+ import {createTypeChecker} from '@putout/printer/type-checker';
2
+
3
+ const isValueContext = createTypeChecker([
4
+ '+: parent -> BinaryExpression',
5
+ [
6
+ '+: parent.callee.name',
7
+ '=',
8
+ 'inList',
9
+ ],
10
+ ]);
11
+
1
12
  export const StringLiteral = (path, {write}) => {
2
13
  const {value} = path.node;
3
14
 
4
- if (value === '*' || value.startsWith(':'))
15
+ if (value === '*' && !isValueContext(path))
16
+ return write(value);
17
+
18
+ if (value.startsWith(':'))
5
19
  return write(value);
6
20
 
7
21
  // quoted identifier like "weird col" — written as is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-sql",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "sql to js ast parser and printer",
6
6
  "exports": {