happy-sql 1.0.0 → 1.1.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.
Files changed (31) hide show
  1. package/ChangeLog +9 -0
  2. package/README.md +13 -0
  3. package/lib/parser/parser.js +33 -1
  4. package/lib/parser/visitors/clause/count/count.js +23 -0
  5. package/lib/parser/visitors/clause/count/fixture/count-fix.js +3 -0
  6. package/lib/parser/visitors/clause/count/fixture/count.sql +1 -0
  7. package/lib/parser/visitors/clause/delete/delete.js +3 -2
  8. package/lib/parser/visitors/clause/find-clause.js +3 -0
  9. package/lib/parser/visitors/clause/insert/fixture/insert-identifier-fix.js +3 -0
  10. package/lib/parser/visitors/clause/insert/fixture/insert-identifier.sql +1 -0
  11. package/lib/parser/visitors/clause/insert/insert.js +9 -30
  12. package/lib/parser/visitors/clause/operand.js +20 -0
  13. package/lib/parser/visitors/clause/select/fixture/select-alias-param-fix.js +3 -0
  14. package/lib/parser/visitors/clause/select/fixture/select-alias-param.sql +1 -0
  15. package/lib/parser/visitors/clause/select/select.js +9 -18
  16. package/lib/parser/visitors/clause/set.js +18 -0
  17. package/lib/parser/visitors/clause/update/fixture/update-identifier-fix.js +3 -0
  18. package/lib/parser/visitors/clause/update/fixture/update-identifier.sql +1 -0
  19. package/lib/parser/visitors/clause/update/fixture/update-number-fix.js +3 -0
  20. package/lib/parser/visitors/clause/update/fixture/update-number.sql +1 -0
  21. package/lib/parser/visitors/clause/update/update.js +5 -17
  22. package/lib/printer/visitors/call-expression/call-expression.js +3 -2
  23. package/lib/printer/visitors/call-expression/clauses/clauses.js +5 -1
  24. package/lib/printer/visitors/call-expression/clauses/count/count.js +7 -0
  25. package/lib/printer/visitors/call-expression/clauses/count/fixture/count.sql +2 -0
  26. package/lib/printer/visitors/call-expression/clauses/on-conflict/fixture/on-conflict-multi.sql +1 -0
  27. package/lib/printer/visitors/call-expression/clauses/section/fixture/section-fix.sql +3 -0
  28. package/lib/printer/visitors/call-expression/clauses/section/section.js +5 -0
  29. package/package.json +10 -4
  30. /package/lib/printer/visitors/call-expression/clauses/delete/fixture/{delete-fix.sql → delete.sql} +0 -0
  31. /package/lib/printer/visitors/call-expression/clauses/on-conflict/fixture/{on-conflict-fix.sql → on-conflict.sql} +0 -0
package/ChangeLog ADDED
@@ -0,0 +1,9 @@
1
+ 2026.07.27, v1.1.0
2
+
3
+ feature:
4
+ - f6446c3 happy-sql: COUNT
5
+
6
+ 2026.07.27, v1.0.1
7
+
8
+ feature:
9
+ - b609029 happy-sql: keywords
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  [LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat
8
8
  [LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License"
9
9
 
10
+ <img width="600" height="428" alt="image" src="https://github.com/user-attachments/assets/aefbd055-957e-4de3-aca5-c30e5e3ee3f9" />
11
+
10
12
  SQL to JS AST parser and printer.
11
13
 
12
14
  ## Install
@@ -15,6 +17,17 @@ SQL to JS AST parser and printer.
15
17
  npm i happy-sql --save
16
18
  ```
17
19
 
20
+ ## API
21
+
22
+ ```js
23
+ const source = `select(count('*'), from(users))`;
24
+
25
+ convertJsToSql(source);
26
+ // returns
27
+ 'SELECT COUNT(*)\nFROM users\n');
28
+
29
+ ```
30
+
18
31
  ## License
19
32
 
20
33
  MIT
@@ -3,20 +3,52 @@ import {types} from '@putout/babel';
3
3
  import {convertClause} from '#parser/clause';
4
4
 
5
5
  const {
6
+ identifier,
7
+ stringLiteral,
8
+ callExpression,
6
9
  arrayExpression,
7
10
  expressionStatement,
8
11
  file,
9
12
  program,
10
13
  } = types;
11
14
 
15
+ const getSectionName = (leading) => {
16
+ const comment = leading?.find((c) => c.text.startsWith('-- @'));
17
+
18
+ return comment ? comment.text
19
+ .slice(2)
20
+ .trim() : null;
21
+ };
22
+
23
+ const wrapSection = (stmtNode, sectionName) => {
24
+ if (!sectionName)
25
+ return stmtNode;
26
+
27
+ return callExpression(identifier('section'), [
28
+ stringLiteral(sectionName),
29
+ stmtNode,
30
+ ]);
31
+ };
32
+
12
33
  export const parseSqlNode = (source) => {
13
34
  const cst = parse(source, {
14
35
  dialect: 'sqlite',
15
36
  paramTypes: [':name'],
16
37
  includeRange: true,
38
+ includeComments: true,
17
39
  });
18
40
 
19
- const elements = cst.statements.map(convertClause);
41
+ const elements = [];
42
+
43
+ for (const [i, statement] of cst.statements.entries()) {
44
+ if (statement.type === 'empty')
45
+ continue;
46
+
47
+ const stmtNode = convertClause(statement);
48
+ const leading = i ? statement.leading : cst.leading;
49
+
50
+ elements.push(wrapSection(stmtNode, getSectionName(leading)));
51
+ }
20
52
 
21
53
  return file(program([
22
54
  expressionStatement(arrayExpression(elements)),
@@ -0,0 +1,23 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {
4
+ stringLiteral,
5
+ callExpression,
6
+ identifier,
7
+ } = types;
8
+
9
+ export const isCount = (column) => {
10
+ return column.type === 'func_call' && /count/i.test(column.name.name);
11
+ };
12
+
13
+ export const convertCount = (column) => {
14
+ const {items} = column.args.expr.args;
15
+ const args = [];
16
+
17
+ for (const item of items) {
18
+ if (item.type === 'all_columns')
19
+ args.push(stringLiteral('*'));
20
+ }
21
+
22
+ return callExpression(identifier('count'), args);
23
+ };
@@ -0,0 +1,3 @@
1
+ [
2
+ select(count('*'), from(users)),
3
+ ];
@@ -0,0 +1 @@
1
+ SELECT COUNT(*) FROM users
@@ -1,11 +1,12 @@
1
1
  import {types} from '@putout/babel';
2
2
  import {convertWhere} from '#parser/clause/where';
3
+ import {findClause} from '#parser/clause/find';
3
4
 
4
5
  const {identifier, callExpression} = types;
5
6
 
6
7
  export const convertDelete = ({clauses}) => {
7
- const deleteClause = clauses.find((c) => c.type === 'delete_clause');
8
- const whereClause = clauses.find((c) => c.type === 'where_clause');
8
+ const deleteClause = findClause(clauses, 'delete_clause');
9
+ const whereClause = findClause(clauses, 'where_clause');
9
10
 
10
11
  const table = identifier(deleteClause.tables.items[0].name);
11
12
 
@@ -0,0 +1,3 @@
1
+ const equalType = (a) => ({type}) => type === a;
2
+
3
+ export const findClause = (clauses, type) => clauses.find(equalType(type));
@@ -0,0 +1,3 @@
1
+ [
2
+ insert(into(t, x, values(':x')), onConflict(x, set(y === x))),
3
+ ];
@@ -0,0 +1 @@
1
+ INSERT INTO t (x) VALUES (:x) ON CONFLICT (x) DO UPDATE SET y = x
@@ -1,41 +1,22 @@
1
1
  import {types} from '@putout/babel';
2
+ import {convertSet} from '#parser/clause/set';
3
+ import {findClause} from '#parser/clause/find';
2
4
 
3
5
  const {
4
6
  identifier,
5
- stringLiteral,
6
- binaryExpression,
7
- arrayExpression,
8
7
  callExpression,
8
+ arrayExpression,
9
+ stringLiteral,
9
10
  } = types;
10
11
 
11
- const convertOperand = (node) => {
12
- if (node.type === 'parameter')
13
- return stringLiteral(node.text);
14
-
15
- if (node.type === 'number_literal')
16
- return types.numericLiteral(node.value);
17
-
18
- if (node.type === 'string_literal')
19
- return stringLiteral(node.value);
20
-
21
- return identifier(node.name);
22
- };
23
-
24
- const convertSet = (setClause) => {
25
- const args = [];
26
-
27
- for (const a of setClause.assignments.items) {
28
- args.push(binaryExpression('===', identifier(a.column.name), convertOperand(a.expr)));
29
- }
30
-
31
- return callExpression(identifier('set'), args);
32
- };
12
+ const convertExprToIdentifier = ({expr}) => identifier(expr.name);
13
+ const convertColToIdentifier = ({name}) => identifier(name);
33
14
 
34
15
  const convertUpsert = (upsertClause) => {
35
16
  const target = upsertClause.conflictTarget;
36
17
  const {action} = upsertClause;
37
18
 
38
- const targetArgs = target ? target.expr.items.map((i) => identifier(i.expr.name)) : [];
19
+ const targetArgs = target ? target.expr.items.map(convertExprToIdentifier) : [];
39
20
  const actionNode = action.type === 'upsert_action_nothing' ? identifier('nothing') : convertSet(action.set);
40
21
 
41
22
  return callExpression(identifier('onConflict'), [...targetArgs, actionNode]);
@@ -44,13 +25,11 @@ const convertUpsert = (upsertClause) => {
44
25
  export const convertInsert = ({clauses}) => {
45
26
  const [ic] = clauses;
46
27
  const [, vc] = clauses;
47
- const uc = clauses.find((c) => c.type === 'upsert_clause');
28
+ const uc = findClause(clauses, 'upsert_clause');
48
29
 
49
30
  const cols = ic.columns.expr.items;
50
31
  const vals = vc.values.items[0].expr.items;
51
-
52
- const colArg = cols.length === 1 ? identifier(cols[0].name) : arrayExpression(cols.map((c) => identifier(c.name)));
53
-
32
+ const colArg = cols.length === 1 ? identifier(cols[0].name) : arrayExpression(cols.map(convertColToIdentifier));
54
33
  const valArgs = [];
55
34
 
56
35
  for (const p of vals) {
@@ -0,0 +1,20 @@
1
+ import {types} from '@putout/babel';
2
+
3
+ const {
4
+ identifier,
5
+ stringLiteral,
6
+ numericLiteral,
7
+ } = types;
8
+
9
+ export const convertOperand = (node) => {
10
+ if (node.type === 'parameter')
11
+ return stringLiteral(node.text);
12
+
13
+ if (node.type === 'number_literal')
14
+ return numericLiteral(node.value);
15
+
16
+ if (node.type === 'string_literal')
17
+ return stringLiteral(node.value);
18
+
19
+ return identifier(node.name);
20
+ };
@@ -0,0 +1,3 @@
1
+ [
2
+ select(file = ':file', from(t)),
3
+ ];
@@ -0,0 +1 @@
1
+ SELECT :file AS file FROM t
@@ -1,6 +1,9 @@
1
1
  import {types} from '@putout/babel';
2
+ import {convertOperand} from '#parser/clause/operand';
2
3
  import {convertFrom} from '#parser/clause/from';
3
4
  import {convertWhere} from '#parser/clause/where';
5
+ import {findClause} from '#parser/clause/find';
6
+ import {convertCount, isCount} from '#parser/clause/count';
4
7
 
5
8
  const {
6
9
  identifier,
@@ -9,26 +12,14 @@ const {
9
12
  callExpression,
10
13
  } = types;
11
14
 
12
- const findClause = (clauses, type) => clauses.find((c) => c.type === type);
13
-
14
- const convertOperand = (node) => {
15
- if (node.type === 'parameter')
16
- return stringLiteral(node.text);
17
-
18
- if (node.type === 'number_literal')
19
- return types.numericLiteral(node.value);
20
-
21
- if (node.type === 'string_literal')
22
- return stringLiteral(node.value);
15
+ const convertColumn = (column) => {
16
+ if (isCount(column))
17
+ return convertCount(column);
23
18
 
24
- return identifier(node.name);
25
- };
26
-
27
- const convertColumn = (col) => {
28
- if (col.type === 'alias')
29
- return assignmentExpression('=', identifier(col.alias.name), convertOperand(col.expr));
19
+ if (column.type === 'alias')
20
+ return assignmentExpression('=', identifier(column.alias.name), convertOperand(column.expr));
30
21
 
31
- return identifier(col.name);
22
+ return identifier(column.name);
32
23
  };
33
24
 
34
25
  const convertColumns = (columns) => {
@@ -0,0 +1,18 @@
1
+ import {types} from '@putout/babel';
2
+ import {convertOperand} from '#parser/clause/operand';
3
+
4
+ const {
5
+ callExpression,
6
+ identifier,
7
+ binaryExpression,
8
+ } = types;
9
+
10
+ export const convertSet = (setClause) => {
11
+ const args = [];
12
+
13
+ for (const a of setClause.assignments.items) {
14
+ args.push(binaryExpression('===', identifier(a.column.name), convertOperand(a.expr)));
15
+ }
16
+
17
+ return callExpression(identifier('set'), args);
18
+ };
@@ -0,0 +1,3 @@
1
+ [
2
+ update(t, set(x === y), where(id === ':id')),
3
+ ];
@@ -0,0 +1 @@
1
+ UPDATE t SET x = y WHERE id = :id
@@ -0,0 +1,3 @@
1
+ [
2
+ update(t, set(x === 1), where(id === ':id')),
3
+ ];
@@ -0,0 +1 @@
1
+ UPDATE t SET x = 1 WHERE id = :id
@@ -1,30 +1,18 @@
1
1
  import {types} from '@putout/babel';
2
2
  import {convertWhere} from '#parser/clause/where';
3
+ import {convertOperand} from '#parser/clause/operand';
4
+ import {findClause} from '#parser/clause/find';
3
5
 
4
6
  const {
5
7
  identifier,
6
- stringLiteral,
7
8
  binaryExpression,
8
9
  callExpression,
9
10
  } = types;
10
11
 
11
- const convertOperand = (node) => {
12
- if (node.type === 'parameter')
13
- return stringLiteral(node.text);
14
-
15
- if (node.type === 'number_literal')
16
- return types.numericLiteral(node.value);
17
-
18
- if (node.type === 'string_literal')
19
- return stringLiteral(node.value);
20
-
21
- return identifier(node.name);
22
- };
23
-
24
12
  export const convertUpdate = ({clauses}) => {
25
- const updateClause = clauses.find((c) => c.type === 'update_clause');
26
- const setClause = clauses.find((c) => c.type === 'set_clause');
27
- const whereClause = clauses.find((c) => c.type === 'where_clause');
13
+ const updateClause = findClause(clauses, 'update_clause');
14
+ const setClause = findClause(clauses, 'set_clause');
15
+ const whereClause = findClause(clauses, 'where_clause');
28
16
 
29
17
  const table = identifier(updateClause.tables.items[0].name);
30
18
  const setArgs = [];
@@ -2,9 +2,10 @@ import {clauses} from './clauses/clauses.js';
2
2
 
3
3
  export const CallExpression = (path, printer) => {
4
4
  const {name} = path.node.callee;
5
+ const visit = clauses[name];
5
6
 
6
- if (!clauses[name])
7
+ if (!visit)
7
8
  throw Error(`${name} not supported yet`);
8
9
 
9
- clauses[name](path, printer);
10
+ visit(path, printer);
10
11
  };
@@ -8,10 +8,13 @@ import {onConflict} from './on-conflict/on-conflict.js';
8
8
  import {update} from './update/update.js';
9
9
  import {set} from './set/set.js';
10
10
  import {del} from './delete/delete.js';
11
+ import {section} from './section/section.js';
12
+ import {count} from './count/count.js';
11
13
 
12
14
  export const clauses = {
13
- select,
15
+ count,
14
16
  from,
17
+ select,
15
18
  where,
16
19
  insert,
17
20
  into,
@@ -20,4 +23,5 @@ export const clauses = {
20
23
  update,
21
24
  set,
22
25
  delete: del,
26
+ section,
23
27
  };
@@ -0,0 +1,7 @@
1
+ export const count = (path, {write, traverse}) => {
2
+ const [first] = path.get('arguments');
3
+ write('COUNT');
4
+ write('(');
5
+ traverse(first);
6
+ write(')');
7
+ };
@@ -0,0 +1,2 @@
1
+ SELECT COUNT(*)
2
+ FROM users
@@ -0,0 +1 @@
1
+ INSERT INTO t (x) VALUES (:x) ON CONFLICT (x, y) DO UPDATE SET x = :x
@@ -0,0 +1,3 @@
1
+ -- @select
2
+ SELECT *
3
+ FROM users
@@ -0,0 +1,5 @@
1
+ export const section = (path, {write, traverse}) => {
2
+ const [name, stmt] = path.get('arguments');
3
+ write(`-- ${name.node.value}\n`);
4
+ traverse(stmt);
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-sql",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "sql to js ast parser and printer",
6
6
  "exports": {
@@ -12,9 +12,13 @@
12
12
  "imports": {
13
13
  "#happy-sql": "./lib/happy-sql.js",
14
14
  "#parser": "./lib/parser/parser.js",
15
+ "#parser/clause/operand": "./lib/parser/visitors/clause/operand.js",
16
+ "#parser/clause/set": "./lib/parser/visitors/clause/set.js",
17
+ "#parser/clause/find": "./lib/parser/visitors/clause/find-clause.js",
15
18
  "#parser/test": "./test/create-parser-test.js",
16
19
  "#parser/clause": "./lib/parser/visitors/clause/index.js",
17
20
  "#parser/clause/select": "./lib/parser/visitors/clause/select/select.js",
21
+ "#parser/clause/count": "./lib/parser/visitors/clause/count/count.js",
18
22
  "#parser/clause/from": "./lib/parser/visitors/clause/from/from.js",
19
23
  "#parser/clause/where": "./lib/parser/visitors/clause/where/where.js",
20
24
  "#parser/clause/insert": "./lib/parser/visitors/clause/insert/insert.js",
@@ -42,9 +46,10 @@
42
46
  "url": "git+https://github.com/coderaiser/happy-sql.git"
43
47
  },
44
48
  "keywords": [
45
- "store",
46
- "variable",
47
- "functional"
49
+ "sql",
50
+ "js",
51
+ "print",
52
+ "parse"
48
53
  ],
49
54
  "author": "coderaiser <mnemonic.enemy@gmail.com> (http://coderaiser.github.io/)",
50
55
  "license": "MIT",
@@ -75,6 +80,7 @@
75
80
  "@putout/babel": "^5.7.3",
76
81
  "@putout/operator-json": "^3.4.0",
77
82
  "@putout/printer": "^18.17.0",
83
+ "just-camel-case": "^6.2.0",
78
84
  "redstd": "^1.0.2",
79
85
  "sql-parser-cst": "^0.42.1"
80
86
  }