happy-sql 2.4.0 → 2.4.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.
Files changed (29) hide show
  1. package/ChangeLog +28 -0
  2. package/lib/parser/parser.js +11 -11
  3. package/lib/parser/visitors/clause/alter-table/alter-table.js +9 -6
  4. package/lib/parser/visitors/clause/binary.js +2 -2
  5. package/lib/parser/visitors/clause/column-definition/column-definition.js +22 -9
  6. package/lib/parser/visitors/clause/create-index/create-index.js +1 -1
  7. package/lib/parser/visitors/clause/create-table/create-table.js +9 -4
  8. package/lib/parser/visitors/clause/create-view/create-view.js +2 -1
  9. package/lib/parser/visitors/clause/from/from.js +16 -9
  10. package/lib/parser/visitors/clause/from/group-by.js +8 -1
  11. package/lib/parser/visitors/clause/from/limit.js +1 -1
  12. package/lib/parser/visitors/clause/from/order-by.js +2 -1
  13. package/lib/parser/visitors/clause/from/table-func.js +10 -1
  14. package/lib/parser/visitors/clause/index.js +18 -2
  15. package/lib/parser/visitors/clause/insert/insert.js +44 -6
  16. package/lib/parser/visitors/clause/operand.js +1 -1
  17. package/lib/parser/visitors/clause/select/select.js +16 -9
  18. package/lib/parser/visitors/clause/with-named/with-named.js +15 -1
  19. package/lib/printer/visitors/call-expression/clauses/add-constraint/add-constraint.js +4 -0
  20. package/lib/printer/visitors/call-expression/clauses/clauses.js +2 -0
  21. package/lib/printer/visitors/call-expression/clauses/from/from.js +16 -3
  22. package/lib/printer/visitors/call-expression/clauses/into/into.js +5 -2
  23. package/lib/printer/visitors/call-expression/clauses/json-op.js +8 -1
  24. package/lib/printer/visitors/call-expression/clauses/on-conflict/on-conflict.js +9 -2
  25. package/lib/printer/visitors/call-expression/clauses/references/references.js +26 -14
  26. package/lib/printer/visitors/call-expression/clauses/section/section.js +5 -1
  27. package/lib/printer/visitors/call-expression/clauses/window-frame/window-frame.js +2 -2
  28. package/lib/printer/visitors/logical-expression.js +4 -1
  29. package/package.json +3 -3
package/ChangeLog CHANGED
@@ -1,3 +1,31 @@
1
+ 2026.11.14, v2.4.1
2
+
3
+ fix:
4
+ - 10d5818 happy-sql: join
5
+ - 7510903 alter-table: rename table, add constraint, RLS actions
6
+ - a4e14a1 references: ON DELETE and ON UPDATE actions
7
+ - 60cbd74 from: extract capitalizeWord, remove inlined arrow
8
+ - 3ebbea9 with-named: extract convertColumn, remove inlined arrow
9
+ - 8848396 printer/from: remove inlined arrows and ternary
10
+ - 0cc326c window-frame: remove optional chaining
11
+ - 652387a references: remove optional chaining
12
+ - 344f7c3 clause/index: remove optional chaining, nullish coalescing, inlined arrows
13
+ - 313fa5f binary: remove optional chaining and nullish coalescing
14
+ - 12b6f3c insert: remove optional chaining and inlined arrows/ternaries
15
+ - 652bcad create-index: remove optional chaining
16
+ - b5b40ac limit: remove optional chaining
17
+ - 5eb1732 create-table: remove optional chaining and inlined arrows
18
+ - f078532 create-view: extract isMaterializedKind predicate
19
+ - 472e627 order-by: extract nulls order fn name
20
+ - ddcb389 group-by: remove inlined arrow in map
21
+ - 1be75b4 table-func: extract getRawName, remove inlined ternary
22
+ - 10a0b35 column-definition: remove optional chaining, nullish coalescing, inlined arrows
23
+ - 9108d3e operand: remove optional chaining and nullish coalescing
24
+
25
+ feature:
26
+ - ffaef07 happy-sql: sql-parser-cst v0.44.1
27
+ - 981cf13 happy-sql: @putout/plugin-printer v9.0.0
28
+
1
29
  2026.08.27, v2.4.0
2
30
 
3
31
  fix:
@@ -18,19 +18,17 @@ const DIALECTS = [
18
18
  'sqlite',
19
19
  ];
20
20
 
21
- const parseCstDialect = (source, dialect) => parseCst(source, {
22
- dialect,
23
- paramTypes: [':name'],
24
- includeRange: true,
25
- includeComments: true,
26
- });
27
-
28
21
  export const parseSql = (source) => {
29
22
  let cst;
30
23
  let error;
31
24
 
32
25
  for (const dialect of DIALECTS) {
33
- [error, cst] = tryCatch(parseCstDialect, source, dialect);
26
+ [error, cst] = tryCatch(parseCst, source, {
27
+ dialect,
28
+ paramTypes: [':name'],
29
+ includeRange: true,
30
+ includeComments: true,
31
+ });
34
32
 
35
33
  if (cst)
36
34
  return convertSource(cst);
@@ -47,9 +45,11 @@ const getSectionName = (leading) => {
47
45
 
48
46
  const comment = leading.find(isComment);
49
47
 
50
- return comment ? comment.text
51
- .slice(2)
52
- .trim() : null;
48
+ return comment
49
+ ? comment.text
50
+ .slice(2)
51
+ .trim()
52
+ : null;
53
53
  };
54
54
 
55
55
  const convertSource = (cst) => {
@@ -14,14 +14,17 @@ const convertRenameTable = (action) => callExpression(identifier('renameTable'),
14
14
 
15
15
  const convertAddConstraint = ({name, constraint}) => {
16
16
  const node = convertTableConstraint(constraint);
17
+ const args = [];
17
18
 
18
- if (!name)
19
- return node;
19
+ if (name)
20
+ args.push(callExpression(identifier('constraint'), [
21
+ identifier(name.name.name),
22
+ node,
23
+ ]));
24
+ else
25
+ args.push(node);
20
26
 
21
- return callExpression(identifier('constraint'), [
22
- identifier(name.name.name),
23
- node,
24
- ]);
27
+ return callExpression(identifier('addConstraint'), args);
25
28
  };
26
29
 
27
30
  const convertDropColumn = (action) => callExpression(identifier('dropColumn'), [
@@ -31,7 +31,7 @@ export const opName = (op) => {
31
31
  return op.name;
32
32
  };
33
33
 
34
- const isEscape = (node) => node?.type === 'binary_expr' && opName(node.operator) === 'ESCAPE';
34
+ const isEscape = (node) => node && node.type === 'binary_expr' && opName(node.operator) === 'ESCAPE';
35
35
 
36
36
  const convertLike = (node) => {
37
37
  const args = [
@@ -172,7 +172,7 @@ export const convertBinary = (node) => {
172
172
  if (convert)
173
173
  return convert(node);
174
174
 
175
- const jsOp = SQL_TO_JS_OP[op] ?? '===';
175
+ const jsOp = SQL_TO_JS_OP[op] || '===';
176
176
 
177
177
  return binaryExpression(
178
178
  jsOp,
@@ -31,16 +31,18 @@ const convertGenerated = (constraint) => {
31
31
  return callExpression(identifier('identity'), []);
32
32
  };
33
33
 
34
- const isGeneratedExpr = ({expr}) => expr?.type === 'paren_expr';
34
+ const isGeneratedExpr = ({expr}) => expr && expr.type === 'paren_expr';
35
+
36
+ const isStored = (constraint) => Boolean(constraint.storageKw) && constraint.storageKw.name === 'STORED';
35
37
 
36
38
  const convertGeneratedExpr = (constraint) => callExpression(identifier('generatedAlwaysAs'), [
37
39
  convertBinary(constraint.expr.expr),
38
- booleanLiteral(constraint.storageKw?.name === 'STORED'),
40
+ booleanLiteral(isStored(constraint)),
39
41
  ]);
40
42
 
41
43
  const convertDefaultNextval = (expr) => {
42
44
  const [seq] = expr.args.expr.args.items;
43
- const seqName = seq.value ?? seq.name;
45
+ const seqName = seq.value || seq.name;
44
46
 
45
47
  return callExpression(identifier('defaultNextval'), [
46
48
  identifier(seqName),
@@ -87,15 +89,26 @@ const ACTION_NAMES = {
87
89
  SET_DEFAULT: true,
88
90
  };
89
91
 
90
- const convertReferentialAction = ({eventKw, actionKw}) => {
91
- const event = eventKw.name === 'DELETE' ? 'onDelete' : 'onUpdate';
92
- const action = Array.isArray(actionKw) ? actionKw
93
- .map(({name}) => name)
94
- .join(' ') : actionKw.name;
92
+ const getActionName = (kw) => kw.name;
93
+
94
+ const normalizeAction = (a) => {
95
+ if (ACTION_NAMES[a])
96
+ return a;
95
97
 
96
- const name = ACTION_NAMES[action] ? action : action
98
+ return a
97
99
  .replace(/\s+/g, '_')
98
100
  .toUpperCase();
101
+ };
102
+
103
+ const convertReferentialAction = ({eventKw, actionKw}) => {
104
+ const event = eventKw.name === 'DELETE' ? 'onDelete' : 'onUpdate';
105
+ const action = Array.isArray(actionKw)
106
+ ? actionKw
107
+ .map(getActionName)
108
+ .join(' ')
109
+ : actionKw.name;
110
+
111
+ const name = normalizeAction(action);
99
112
 
100
113
  return callExpression(identifier(event), [
101
114
  identifier(name),
@@ -4,7 +4,7 @@ import {findClause} from '#parser/clause/find';
4
4
 
5
5
  const {identifier, callExpression} = types;
6
6
 
7
- const isDesc = (item) => item.direction?.type === 'sort_direction_desc';
7
+ const isDesc = (item) => item.direction && item.direction.type === 'sort_direction_desc';
8
8
 
9
9
  const convertColumns = (columns) => {
10
10
  const {items} = columns.expr;
@@ -12,7 +12,7 @@ const {
12
12
  const isAsClause = ({type}) => type === 'as_clause';
13
13
  const isVirtual = (stmt) => stmt.kind && stmt.kind.kindKw.name === 'VIRTUAL';
14
14
  const isColumnDefinition = ({type}) => type === 'column_definition';
15
- const isTemp = (stmt) => ['TEMP', 'TEMPORARY'].includes(stmt.kind?.kindKw.name);
15
+ const isTemp = (stmt) => stmt.kind && ['TEMP', 'TEMPORARY'].includes(stmt.kind.kindKw.name);
16
16
 
17
17
  const convertColumnListItem = (item) => {
18
18
  if (isColumnDefinition(item))
@@ -21,10 +21,15 @@ const convertColumnListItem = (item) => {
21
21
  return convertTableConstraint(item);
22
22
  };
23
23
 
24
- const isWithoutRowid = (options) => options?.items.some(({name}) => name?.some?.(({name}) => name === 'WITHOUT'));
24
+ const isWithoutName = ({name}) => name === 'WITHOUT';
25
+ const isWithoutKeyword = ({name}) => Array.isArray(name) && name.some(isWithoutName);
26
+ const isWithoutRowid = (options) => options && options.items.some(isWithoutKeyword);
25
27
 
26
28
  const convertTableArgs = (stmt) => {
27
- const colNodes = stmt.columns.expr.items.map(convertColumnListItem);
29
+ const colNodes = [];
30
+
31
+ for (const item of stmt.columns.expr.items)
32
+ colNodes.push(convertColumnListItem(item));
28
33
 
29
34
  if (isWithoutRowid(stmt.options))
30
35
  colNodes.push(callExpression(identifier('withoutRowid'), []));
@@ -54,7 +59,7 @@ export const convertCreateTable = (stmt) => {
54
59
  ]);
55
60
  }
56
61
 
57
- const asClause = stmt.clauses?.find(isAsClause);
62
+ const asClause = stmt.clauses && stmt.clauses.find(isAsClause);
58
63
 
59
64
  if (asClause)
60
65
  return callExpression(identifier('createTableAs'), [
@@ -3,7 +3,8 @@ import {convertClause} from '#parser/clause';
3
3
 
4
4
  const {identifier, callExpression} = types;
5
5
 
6
- const isMaterialized = (stmt) => stmt.kinds.some(({kindKw}) => kindKw.name === 'MATERIALIZED');
6
+ const isMaterializedKind = ({kindKw}) => kindKw.name === 'MATERIALIZED';
7
+ const isMaterialized = (stmt) => stmt.kinds.some(isMaterializedKind);
7
8
 
8
9
  export const convertCreateView = (stmt) => {
9
10
  const [asClause] = stmt.clauses;
@@ -145,6 +145,11 @@ const joinTypeParts = (operator) => {
145
145
  return parts;
146
146
  };
147
147
 
148
+ const capitalizeWord = (s) => s[0].toUpperCase() +
149
+ s
150
+ .slice(1)
151
+ .toLowerCase();
152
+
148
153
  const convertJoinType = (operator) => {
149
154
  const parts = joinTypeParts(operator);
150
155
 
@@ -155,14 +160,12 @@ const convertJoinType = (operator) => {
155
160
  return 'join';
156
161
 
157
162
  // 'LEFT OUTER' → 'leftOuter' → 'leftOuterJoin'
158
- const camel = parts[0].toLowerCase() +
159
- parts
160
- .slice(1)
161
- .map((s) => s[0].toUpperCase() +
162
- s
163
- .slice(1)
164
- .toLowerCase())
165
- .join('');
163
+ const tail = [];
164
+
165
+ for (const part of parts.slice(1))
166
+ tail.push(capitalizeWord(part));
167
+
168
+ const camel = parts[0].toLowerCase() + tail.join('');
166
169
 
167
170
  return `${camel}Join`;
168
171
  };
@@ -268,7 +271,11 @@ export const convertFrom = ({expr, whereClause, groupByClause, havingClause, ord
268
271
 
269
272
  if (expr.type === 'join_expr') {
270
273
  const parts = flattenJoins(expr);
271
- return callExpression(identifier('from'), parts);
274
+
275
+ return callExpression(identifier('from'), [
276
+ ...parts,
277
+ ...fromArgs,
278
+ ]);
272
279
  }
273
280
 
274
281
  if (expr.type === 'alias')
@@ -3,4 +3,11 @@ import {convertOperand} from '#parser/clause/operand';
3
3
 
4
4
  const {callExpression, identifier} = types;
5
5
 
6
- export const convertGroupBy = (clause) => callExpression(identifier('groupBy'), clause.columns.items.map(convertOperand));
6
+ export const convertGroupBy = (clause) => {
7
+ const args = [];
8
+
9
+ for (const item of clause.columns.items)
10
+ args.push(convertOperand(item));
11
+
12
+ return callExpression(identifier('groupBy'), args);
13
+ };
@@ -7,7 +7,7 @@ const {
7
7
  } = types;
8
8
 
9
9
  export const convertLimit = (limitClause) => {
10
- if (limitClause.count?.type === 'limit_all')
10
+ if (limitClause.count && limitClause.count.type === 'limit_all')
11
11
  return callExpression(identifier('limitAll'), []);
12
12
 
13
13
  const args = [
@@ -7,8 +7,9 @@ const isDesc = ({type}) => type === 'sort_direction_desc';
7
7
 
8
8
  const convertNullsOrder = (col, nullHandlingKw) => {
9
9
  const isLast = nullHandlingKw.at(-1).name === 'LAST';
10
+ const fnName = isLast ? 'nullsLast' : 'nullsFirst';
10
11
 
11
- return callExpression(identifier(isLast ? 'nullsLast' : 'nullsFirst'), [col]);
12
+ return callExpression(identifier(fnName), [col]);
12
13
  };
13
14
 
14
15
  const convertOrderItem = (item) => {
@@ -6,9 +6,18 @@ const {callExpression, identifier} = types;
6
6
  const SNAKE_RE = /_([a-z])/g;
7
7
  const toCamel = (s) => s.replace(SNAKE_RE, (_, c) => c.toUpperCase());
8
8
 
9
+ const getRawName = (expr) => {
10
+ const raw = expr.name.name;
11
+
12
+ if (raw.includes('_'))
13
+ return raw.toLowerCase();
14
+
15
+ return raw;
16
+ };
17
+
9
18
  export const convertTableFunc = (expr) => {
10
19
  // multi-word snake_case names are lowercased first (GENERATE_SERIES → generateSeries)
11
- const name = toCamel(expr.name.name.includes('_') ? expr.name.name.toLowerCase() : expr.name.name);
20
+ const name = toCamel(getRawName(expr));
12
21
  const args = expr.args.expr.args.items.map(convertOperand);
13
22
 
14
23
  return callExpression(identifier(name), args);
@@ -75,6 +75,8 @@ const convertSelectOrNamed = (stmt) => {
75
75
  return convertSelect(stmt);
76
76
  };
77
77
 
78
+ const getAnalyzeTables = ({tables}) => tables.items || tables;
79
+
78
80
  const clauseVisitors = {
79
81
  select_stmt: convertSelectOrNamed,
80
82
  insert_stmt: convertInsertOrNamed,
@@ -96,7 +98,14 @@ const clauseVisitors = {
96
98
  vacuum_stmt: ({file}) => callExpression(identifier('vacuum'), file ? [
97
99
  stringLiteral(file.value),
98
100
  ] : []),
99
- truncate_stmt: ({tables}) => callExpression(identifier('truncate'), tables.items.map(({name}) => identifier(name))),
101
+ truncate_stmt: ({tables}) => {
102
+ const args = [];
103
+
104
+ for (const table of tables.items)
105
+ args.push(convertTableName(table));
106
+
107
+ return callExpression(identifier('truncate'), args);
108
+ },
100
109
  detach_database_stmt: ({schema}) => callExpression(identifier('detach'), [
101
110
  identifier(schema.name),
102
111
  ]),
@@ -111,7 +120,14 @@ const clauseVisitors = {
111
120
  reindex_stmt: (stmt) => callExpression(identifier('reindex'), stmt.name ? [
112
121
  identifier(stmt.name.name),
113
122
  ] : []),
114
- analyze_stmt: (stmt) => callExpression(identifier('analyze'), (stmt.tables?.items ?? []).map(convertTableName)),
123
+ analyze_stmt: (stmt) => {
124
+ const args = [];
125
+
126
+ for (const table of getAnalyzeTables(stmt))
127
+ args.push(convertTableName(table));
128
+
129
+ return callExpression(identifier('analyze'), args);
130
+ },
115
131
  release_savepoint_stmt: ({savepoint}) => callExpression(identifier('releaseSavepoint'), [
116
132
  identifier(savepoint.name),
117
133
  ]),
@@ -53,12 +53,31 @@ const convertSource = (vc) => {
53
53
  return callExpression(identifier('values'), rowArgs);
54
54
  };
55
55
 
56
+ const getTargetArgs = (target) => {
57
+ if (!target)
58
+ return [];
59
+
60
+ const args = [];
61
+
62
+ for (const item of target.expr.items)
63
+ args.push(convertExprToIdentifier(item));
64
+
65
+ return args;
66
+ };
67
+
68
+ const convertUpsertAction = (action) => {
69
+ if (action.type === 'upsert_action_nothing')
70
+ return identifier('nothing');
71
+
72
+ return convertSet(action.set);
73
+ };
74
+
56
75
  const convertUpsert = (upsertClause) => {
57
76
  const target = upsertClause.conflictTarget;
58
77
  const {action} = upsertClause;
59
78
 
60
- const targetArgs = target ? target.expr.items.map(convertExprToIdentifier) : [];
61
- const actionNode = action.type === 'upsert_action_nothing' ? identifier('nothing') : convertSet(action.set);
79
+ const targetArgs = getTargetArgs(target);
80
+ const actionNode = convertUpsertAction(action);
62
81
 
63
82
  if (upsertClause.where)
64
83
  targetArgs.push(callExpression(identifier('where'), [
@@ -68,14 +87,26 @@ const convertUpsert = (upsertClause) => {
68
87
  return callExpression(identifier('onConflict'), [...targetArgs, actionNode]);
69
88
  };
70
89
 
71
- const isReplaceInto = ({insertKw, orAction}) => !orAction && insertKw?.name === 'REPLACE';
90
+ const isReplaceInto = ({insertKw, orAction}) => !orAction && insertKw && insertKw.name === 'REPLACE';
91
+
92
+ const colsToArg = (cols) => {
93
+ if (cols.length === 1)
94
+ return identifier(cols[0].name);
95
+
96
+ const args = [];
97
+
98
+ for (const col of cols)
99
+ args.push(convertColToIdentifier(col));
100
+
101
+ return arrayExpression(args);
102
+ };
72
103
 
73
104
  const convertReplaceInto = ({clauses}) => {
74
105
  const ic = findClause(clauses, 'insert_clause');
75
106
  const rc = findClause(clauses, 'returning_clause');
76
107
 
77
108
  const cols = ic.columns.expr.items;
78
- const colArg = cols.length === 1 ? identifier(cols[0].name) : arrayExpression(cols.map(convertColToIdentifier));
109
+ const colArg = colsToArg(cols);
79
110
  const source = convertSource(findClause(clauses, 'values_clause'));
80
111
 
81
112
  return callExpression(identifier('replaceInto'), [
@@ -88,6 +119,13 @@ const convertReplaceInto = ({clauses}) => {
88
119
  ].filter(Boolean));
89
120
  };
90
121
 
122
+ const getSource = (dc, vc) => {
123
+ if (dc)
124
+ return callExpression(identifier('defaultValues'), []);
125
+
126
+ return convertSource(vc);
127
+ };
128
+
91
129
  export const convertInsert = (stmt) => {
92
130
  if (isReplaceInto(stmt.clauses[0]))
93
131
  return convertReplaceInto(stmt);
@@ -100,7 +138,7 @@ export const convertInsert = (stmt) => {
100
138
  const dc = findClause(clauses, 'default_values');
101
139
 
102
140
  const colArgs = convertColumns(ic);
103
- const source = dc ? callExpression(identifier('defaultValues'), []) : convertSource(vc);
141
+ const source = getSource(dc, vc);
104
142
 
105
143
  const args = [
106
144
  ic.orAction && convertOrAction(ic.orAction),
@@ -134,7 +172,7 @@ const convertColumns = (ic) => {
134
172
  return [];
135
173
 
136
174
  const cols = columns.expr.items;
137
- const colArg = cols.length === 1 ? identifier(cols[0].name) : arrayExpression(cols.map(convertColToIdentifier));
175
+ const colArg = colsToArg(cols);
138
176
 
139
177
  return [
140
178
  colArg,
@@ -68,7 +68,7 @@ const convertQuantifier = (node) => {
68
68
 
69
69
  const convertPrefix = (node) => {
70
70
  const {operator} = node;
71
- const name = operator?.name ?? operator;
71
+ const name = operator.name || operator;
72
72
 
73
73
  if (name === 'EXISTS')
74
74
  return callExpression(identifier('exists'), [
@@ -145,10 +145,12 @@ const convertFrameBound = (bound) => {
145
145
 
146
146
  const name = bound.type === 'frame_bound_preceding' ? 'preceding' : 'following';
147
147
 
148
- const isUnbounded = bound.expr?.type === 'frame_unbounded';
149
- const args = !isUnbounded && bound.expr ? [
150
- numericLiteral(bound.expr.value),
151
- ] : [];
148
+ const isUnbounded = bound.expr && bound.expr.type === 'frame_unbounded';
149
+ const args = !isUnbounded && bound.expr
150
+ ? [
151
+ numericLiteral(bound.expr.value),
152
+ ]
153
+ : [];
152
154
 
153
155
  return callExpression(identifier(name), args);
154
156
  };
@@ -215,8 +217,10 @@ const convertFuncCall = (column) => {
215
217
  for (const item of items)
216
218
  args.push(convertOperand(item));
217
219
 
218
- if (column.over)
219
- return convertOver(column, callExpression(identifier(LOWERCASE_FUNCS.has(lower) ? lower : name), args));
220
+ if (column.over) {
221
+ const funcName = LOWERCASE_FUNCS.has(lower) ? lower : name;
222
+ return convertOver(column, callExpression(identifier(funcName), args));
223
+ }
220
224
 
221
225
  if (LOWERCASE_FUNCS.has(lower))
222
226
  return callExpression(identifier(lower), args);
@@ -234,13 +238,16 @@ const convertColumns = (columns) => {
234
238
  return columns.items.map(convertColumn);
235
239
  };
236
240
 
237
- const isDistinct = (selectClause) => selectClause.modifiers.some(({type}) => type === 'select_distinct');
241
+ const isSelectDistinct = ({type}) => type === 'select_distinct';
242
+ const isSelectDistinctOn = ({type}) => type === 'select_distinct_on';
243
+
244
+ const isDistinct = (selectClause) => selectClause.modifiers.some(isSelectDistinct);
238
245
 
239
246
  const convertDistinctOn = ({modifiers}) => {
240
- if (!modifiers.some(({type}) => type === 'select_distinct_on'))
247
+ if (!modifiers.some(isSelectDistinctOn))
241
248
  return null;
242
249
 
243
- const modifier = modifiers.find(({type}) => type === 'select_distinct_on');
250
+ const modifier = modifiers.find(isSelectDistinctOn);
244
251
  const onCols = [];
245
252
 
246
253
  for (const item of modifier.columns.expr.items) {
@@ -15,9 +15,23 @@ const convertTable = ({table, expr}) => {
15
15
  return assignmentExpression('=', identifier(table.name), convertClause(expr.expr));
16
16
  };
17
17
 
18
+ const convertColumn = (column) => stringLiteral(column.name);
19
+
20
+ const getColArgs = (columns) => {
21
+ if (!columns)
22
+ return [];
23
+
24
+ const args = [];
25
+
26
+ for (const col of columns.expr.items)
27
+ args.push(convertColumn(col));
28
+
29
+ return args;
30
+ };
31
+
18
32
  // recursive CTE: numbers('value', unionAll(...))
19
33
  const convertRecursiveTable = ({table, columns, expr}) => {
20
- const colArgs = columns ? columns.expr.items.map((column) => stringLiteral(column.name)) : [];
34
+ const colArgs = getColArgs(columns);
21
35
  const body = convertClause(expr.expr);
22
36
 
23
37
  return callExpression(identifier(table.name), [...colArgs, body]);
@@ -0,0 +1,4 @@
1
+ export const addConstraint = (path, {write, traverse}) => {
2
+ write('ADD ');
3
+ traverse(path.get('arguments')[0]);
4
+ };
@@ -108,6 +108,7 @@ import {withoutRowid} from './without-rowid/without-rowid.js';
108
108
  import {createTableAs} from './create-table-as/create-table-as.js';
109
109
  import {alterTable} from './alter-table/alter-table.js';
110
110
  import {addColumn} from './add-column/add-column.js';
111
+ import {addConstraint} from './add-constraint/add-constraint.js';
111
112
  import {dropColumn} from './drop-column/drop-column.js';
112
113
  import {renameColumn} from './rename-column/rename-column.js';
113
114
  import {alterColumnType} from './alter-column-type/alter-column-type.js';
@@ -278,6 +279,7 @@ export const clauses = {
278
279
  createTableAs,
279
280
  alterTable,
280
281
  addColumn,
282
+ addConstraint,
281
283
  dropColumn,
282
284
  renameColumn,
283
285
  alterColumnType,
@@ -68,7 +68,13 @@ const toSnake = (a) => a.replace(CAMEL_RE, (_, c) => `_${c.toLowerCase()}`);
68
68
 
69
69
  // single-word ALL-CAPS functions (UNNEST) keep their casing
70
70
  const ALL_CAPS_RE = /^[A-Z]+$/;
71
- const toFuncName = (name) => ALL_CAPS_RE.test(name) ? name : toSnake(name);
71
+
72
+ const toFuncName = (name) => {
73
+ if (ALL_CAPS_RE.test(name))
74
+ return name;
75
+
76
+ return toSnake(name);
77
+ };
72
78
 
73
79
  const isTableFunc = (node) => isCallExpression(node) && !TABLE_ARG_NAMES.includes(node.callee.name);
74
80
 
@@ -104,8 +110,15 @@ const printArg = (path, printer) => {
104
110
 
105
111
  export const from = (path, {write, traverse}) => {
106
112
  const args = path.get('arguments');
107
- const tableArgs = args.filter((arg) => !isClause(arg));
108
- const clauseArgs = args.filter(isClause);
113
+ const tableArgs = [];
114
+ const clauseArgs = [];
115
+
116
+ for (const arg of args) {
117
+ if (isClause(arg))
118
+ clauseArgs.push(arg);
119
+ else
120
+ tableArgs.push(arg);
121
+ }
109
122
 
110
123
  write('FROM ');
111
124
  printArg(tableArgs[0], {
@@ -1,6 +1,9 @@
1
1
  import {types} from '@putout/babel';
2
2
 
3
- const {isTSAsExpression} = types;
3
+ const {
4
+ isTSAsExpression,
5
+ isArrayExpression,
6
+ } = types;
4
7
 
5
8
  const printTable = (table, {write, traverse}) => {
6
9
  if (isTSAsExpression(table.node)) {
@@ -36,7 +39,7 @@ export const into = (path, {write, traverse}) => {
36
39
 
37
40
  write(' (');
38
41
 
39
- if (columns.node.type === 'ArrayExpression') {
42
+ if (isArrayExpression(columns.node)) {
40
43
  const colArgs = columns.get('elements');
41
44
 
42
45
  for (let i = 0; i < colArgs.length; i++) {
@@ -1,3 +1,10 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {
4
+ isStringLiteral,
5
+ isArrayExpression,
6
+ } = types;
7
+
1
8
  const JSON_OPS = new Set([
2
9
  '->',
3
10
  '->>',
@@ -11,7 +18,7 @@ const JSON_OPS = new Set([
11
18
  '<@',
12
19
  ]);
13
20
 
14
- export const isJsonTuple = (node) => node.type === 'ArrayExpression' && node.elements.length === 3 && node.elements[1].type === 'StringLiteral' && JSON_OPS.has(node.elements[1].value);
21
+ export const isJsonTuple = (node) => isArrayExpression(node) && node.elements.length === 3 && isStringLiteral(node.elements[1]) && JSON_OPS.has(node.elements[1].value);
15
22
 
16
23
  export const printJsonTuple = (path, {write, traverse}) => {
17
24
  const elems = path.get('elements');
@@ -1,4 +1,11 @@
1
- const isWhereArg = ({node}) => node.type === 'CallExpression' && node.callee.name === 'where';
1
+ import {types} from '@putout/babel';
2
+
3
+ const {
4
+ isIdentifier,
5
+ isCallExpression,
6
+ } = types;
7
+
8
+ const isWhereArg = ({node}) => isCallExpression(node) && node.callee.name === 'where';
2
9
 
3
10
  export const onConflict = (path, {write, traverse}) => {
4
11
  const args = path.get('arguments');
@@ -34,7 +41,7 @@ export const onConflict = (path, {write, traverse}) => {
34
41
 
35
42
  write(' DO ');
36
43
 
37
- if (action.node.type === 'Identifier' && action.node.name === 'nothing') {
44
+ if (isIdentifier(action.node) && action.node.name === 'nothing') {
38
45
  write('NOTHING');
39
46
  return;
40
47
  }
@@ -1,27 +1,39 @@
1
+ const getCalleeName = (node) => {
2
+ if (!node || !node.callee)
3
+ return null;
4
+
5
+ return node.callee.name;
6
+ };
7
+
1
8
  export const references = (path, {write, traverse}) => {
2
9
  const [first, ...other] = path.get('arguments');
10
+ const cols = [];
11
+ const actions = [];
12
+
13
+ for (const arg of other) {
14
+ const name = getCalleeName(arg.node);
15
+
16
+ if (name === 'onDelete' || name === 'onUpdate')
17
+ actions.push(arg);
18
+ else
19
+ cols.push(arg);
20
+ }
3
21
 
4
22
  write('REFERENCES ');
5
23
  traverse(first);
6
24
  write('(');
7
25
 
8
- let wroteCol = false;
9
-
10
- for (const arg of other) {
11
- const name = arg.node?.callee?.name;
12
-
13
- if (name === 'onDelete' || name === 'onUpdate') {
14
- write(' ');
15
- traverse(arg);
16
- continue;
17
- }
18
-
19
- if (wroteCol)
26
+ for (const [i, col] of cols.entries()) {
27
+ if (i > 0)
20
28
  write(', ');
21
29
 
22
- traverse(arg);
23
- wroteCol = true;
30
+ traverse(col);
24
31
  }
25
32
 
26
33
  write(')');
34
+
35
+ for (const action of actions) {
36
+ write(' ');
37
+ traverse(action);
38
+ }
27
39
  };
@@ -1,8 +1,12 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {isArrayExpression} = types;
4
+
1
5
  export const section = (path, {write, traverse}) => {
2
6
  const [name, stmt] = path.get('arguments');
3
7
  write(`-- ${name.node.value}\n`);
4
8
 
5
- if (stmt.node.type === 'ArrayExpression') {
9
+ if (isArrayExpression(stmt.node)) {
6
10
  const elements = stmt.get('elements');
7
11
  const lastIndex = elements.length - 1;
8
12
 
@@ -10,7 +10,7 @@ export const rowsBetween = (path, {write, traverse}) => {
10
10
  export const preceding = (path, {write, traverse}) => {
11
11
  const [expr] = path.get('arguments');
12
12
 
13
- if (expr?.node) {
13
+ if (expr && expr.node) {
14
14
  traverse(expr);
15
15
  write(' PRECEDING');
16
16
 
@@ -23,7 +23,7 @@ export const preceding = (path, {write, traverse}) => {
23
23
  export const following = (path, {write, traverse}) => {
24
24
  const [expr] = path.get('arguments');
25
25
 
26
- if (expr?.node) {
26
+ if (expr && expr.node) {
27
27
  traverse(expr);
28
28
  write(' FOLLOWING');
29
29
 
@@ -1,4 +1,7 @@
1
- const needsParensIn = (node, op) => op === '&&' && node.type === 'LogicalExpression' && node.operator === '||';
1
+ import {types} from '@putout/babel';
2
+
3
+ const {isLogicalExpression} = types;
4
+ const needsParensIn = (node, op) => op === '&&' && isLogicalExpression(node) && node.operator === '||';
2
5
 
3
6
  const printNestedOr = (node, printer) => {
4
7
  const {write, traverse} = printer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-sql",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "description": "sql to js ast parser and printer",
6
6
  "exports": {
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "homepage": "https://github.com/coderaiser/happy-sql",
85
85
  "devDependencies": {
86
- "@putout/plugin-printer": "^8.22.0",
86
+ "@putout/plugin-printer": "^9.0.0",
87
87
  "@putout/test": "^15.8.2",
88
88
  "eslint": "^10.8.1",
89
89
  "eslint-plugin-node": "^11.1.0",
@@ -103,7 +103,7 @@
103
103
  "@putout/printer": "^18.38.1",
104
104
  "just-camel-case": "^6.2.0",
105
105
  "redstd": "^1.0.2",
106
- "sql-parser-cst": "^0.42.1",
106
+ "sql-parser-cst": "^0.44.1",
107
107
  "try-catch": "^4.0.9"
108
108
  }
109
109
  }