sql-love 0.9.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.
@@ -0,0 +1,37 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: build
5
+
6
+ on:
7
+ push:
8
+ branches: ['main']
9
+ pull_request:
10
+ branches: ['main']
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+
16
+ strategy:
17
+ matrix:
18
+ node-version: [18.x, 20.x]
19
+ # See supported Node.js release schedule at https://nodejs.org/en/about/previous-releases
20
+
21
+ steps:
22
+ - uses: actions/checkout@v3
23
+ - name: Use Node.js ${{ matrix.node-version }}
24
+ uses: actions/setup-node@v3
25
+ with:
26
+ node-version: ${{ matrix.node-version }}
27
+ cache: 'npm'
28
+ - run: |
29
+ npm ci
30
+ npm run build
31
+ npm test
32
+ npm run coverage
33
+ - name: Upload coverage to Codecov
34
+ uses: codecov/codecov-action@v3
35
+ with:
36
+ token: ${{ secrets.CODECOV_TOKEN }}
37
+ directory: ./coverage/
@@ -0,0 +1,13 @@
1
+ {
2
+ "plugins": ["prettier-plugin-organize-imports"],
3
+ "trailingComma": "es5",
4
+ "printWidth": 80,
5
+ "tabWidth": 2,
6
+ "useTabs": false,
7
+ "semi": true,
8
+ "singleQuote": true,
9
+ "bracketSpacing": true,
10
+ "jsxBracketSameLine": false,
11
+ "arrowParens": "avoid",
12
+ "endOfLine": "lf"
13
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ ## Change Log
2
+
3
+ ### v0.9.1
4
+
5
+ - More documentation
6
+ - GitHub Actions CI
7
+ - Support for parsing "OFFSET # ROWS"
8
+ - Support for parsing "FETCH NEXT # ROWS ONLY"
9
+
10
+ ### v0.9.0
11
+
12
+ - Initial release with `SelectBuilder`
@@ -0,0 +1,85 @@
1
+ # Contributing
2
+
3
+ When contributing to this repository, please note we have a code of conduct;
4
+ please follow it in all your interactions with the project.
5
+
6
+ ## Pull Request Process
7
+
8
+ 1. Ensure all unit tests pass. Add more unit tests if applicable.
9
+ 2. Update the README.md with details of change if applicable.
10
+ 3. Give maintainers some time to review the changes.
11
+
12
+ ## Code of Conduct
13
+
14
+ ### Our Pledge
15
+
16
+ In the interest of fostering an open and welcoming environment, we as
17
+ contributors and maintainers pledge to making participation in our project and
18
+ our community a harassment-free experience for everyone, regardless of age, body
19
+ size, disability, ethnicity, gender identity and expression, level of experience,
20
+ nationality, personal appearance, race, religion, or sexual identity and
21
+ orientation.
22
+
23
+ ### Our Standards
24
+
25
+ Examples of behavior that contributes to creating a positive environment
26
+ include:
27
+
28
+ * Using welcoming and inclusive language
29
+ * Being respectful of differing viewpoints and experiences
30
+ * Gracefully accepting constructive criticism
31
+ * Focusing on what is best for the community
32
+ * Showing empathy towards other community members
33
+
34
+ Examples of unacceptable behavior by participants include:
35
+
36
+ * The use of sexualized language or imagery and unwelcome sexual attention or
37
+ advances
38
+ * Trolling, insulting/derogatory comments, and personal or political attacks
39
+ * Public or private harassment
40
+ * Publishing others' private information, such as a physical or electronic
41
+ address, without explicit permission
42
+ * Other conduct which could reasonably be considered inappropriate in a
43
+ professional setting
44
+
45
+ ### Our Responsibilities
46
+
47
+ Project maintainers are responsible for clarifying the standards of acceptable
48
+ behavior and are expected to take appropriate and fair corrective action in
49
+ response to any instances of unacceptable behavior.
50
+
51
+ Project maintainers have the right and responsibility to remove, edit, or
52
+ reject comments, commits, code, wiki edits, issues, and other contributions
53
+ that are not aligned to this Code of Conduct, or to ban temporarily or
54
+ permanently any contributor for other behaviors that they deem inappropriate,
55
+ threatening, offensive, or harmful.
56
+
57
+ ### Scope
58
+
59
+ This Code of Conduct applies both within project spaces and in public spaces
60
+ when an individual is representing the project or its community. Examples of
61
+ representing a project or community include using an official project e-mail
62
+ address, posting via an official social media account, or acting as an appointed
63
+ representative at an online or offline event. Representation of a project may be
64
+ further defined and clarified by project maintainers.
65
+
66
+ ### Enforcement
67
+
68
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
69
+ reported by opening a GitHub ticket. All complaints will be reviewed and
70
+ investigated and will result in a response that is deemed necessary and
71
+ appropriate to the circumstances. The project team is obligated to maintain
72
+ confidentiality with regard to the reporter of an incident. Further details of
73
+ specific enforcement policies may be posted separately.
74
+
75
+ Project maintainers who do not follow or enforce the Code of Conduct in good
76
+ faith may face temporary or permanent repercussions as determined by other
77
+ members of the project's leadership.
78
+
79
+ ### Attribution
80
+
81
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
82
+ available at [http://contributor-covenant.org/version/1/4][version]
83
+
84
+ [homepage]: http://contributor-covenant.org
85
+ [version]: http://contributor-covenant.org/version/1/4/
package/LICENSE.md ADDED
@@ -0,0 +1,6 @@
1
+ ISC License (ISC)
2
+ Copyright 2021-2023 Ken Snyder
3
+
4
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # sql-love
2
+
3
+ [![NPM Link](https://badgen.net/npm/v/sql-love?v=0.9.1)](https://npm.com/package/any-date-parser)
4
+ [![Dependencies](https://badgen.net/static/dependencies/0/green?v=0.9.1)](https://npm.com/package/any-date-parser)
5
+ [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=0.9.1)](https://github.com/kensnyder/sql-love/actions)
6
+ [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=0.9.1)](https://codecov.io/gh/kensnyder/sql-love)
7
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=0.9.1)](https://opensource.org/licenses/ISC)
8
+
9
+ Classes for parsing and building SQL select queries in Node
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install sql-love
15
+ ```
16
+
17
+ ## Table of Contents
18
+
19
+ - [SelectBuilder](#selectbuilder)
20
+ - [Parsing base SQL](#parsing-base-sql)
21
+ - [Building the Query](#building-the-query)
22
+ - [Fetching Data](#fetching-data)
23
+ - [Counting Results](#counting-results)
24
+ - [Other Methods](#other-methods)
25
+ - [new SelectBuilder() Limitations](#selectparse-limitations)
26
+ - [How to Contribute](./CONTRIBUTING.md)
27
+ - [ISC License](./LICENSE.md)
28
+
29
+ ## SelectBuilder
30
+
31
+ A Select object represents a SQL SELECT query and allows dynamically adding
32
+ clauses including JOIN, WHERE, ORDER BY, LIMIT, OFFSET.
33
+
34
+ ### Parsing base SQL
35
+
36
+ You can define a base query and pass it to the SelectBuilder constructor.
37
+
38
+ ```js
39
+ const { SelectBuilder } = require('sql-love');
40
+ const query = new SelectBuilder(`
41
+ SELECT u.id, u.fname, u.lname, u.email, p.phone
42
+ FROM users
43
+ LEFT JOIN phone_numbers p ON p.user_id = u.id
44
+ AND p.type = 'main'
45
+ WHERE u.is_active = 1
46
+ `);
47
+ if (email) {
48
+ query.where('u.email', email);
49
+ }
50
+ if (areaCode) {
51
+ query.where('p.phone', 'LIKE ?%', areaCode);
52
+ }
53
+ query.sort(sortField);
54
+ query.limit(limitTo);
55
+ const { sql, bindings } = query.compile();
56
+
57
+ // then execute the SQL in your preferred client:
58
+ // like mysql2
59
+ mysql.query(sql, bindings);
60
+ // or Prisma
61
+ const result = await prisma.$executeRaw(sql, bindings);
62
+ ```
63
+
64
+ It is possible to add placeholders to the base query.
65
+
66
+ ```js
67
+ const query = new SelectBuilder(
68
+ `
69
+ SELECT u.id, u.fname, u.lname, u.email, a.city, a.zip
70
+ FROM users
71
+ LEFT JOIN addresses a ON a.user_id = u.id
72
+ WHERE a.state = :state
73
+ AND a.city IN (:city)
74
+ `,
75
+ { state: 'CA', city: ['San Francisco', 'Los Angeles'] }
76
+ );
77
+
78
+ const { sql, bindings } = query.compile();
79
+ /*
80
+ Then "sql" equals:
81
+
82
+ SELECT u.id, u.fname, u.lname, u.email, a.city, a.zip
83
+ FROM users
84
+ LEFT JOIN addresses a ON a.user_id = u.id
85
+ WHERE a.state = ?
86
+ AND a.city IN (?, ?)
87
+
88
+ And "bindings" equals:
89
+
90
+ ['CA', 'San Francisco', 'Los Angeles']
91
+ */
92
+ ```
93
+
94
+ You may specify a compiler engine to use. The default is `"mysql"`.
95
+
96
+ ```js
97
+ const query = new SelectBuilder('SELECT * FROM users')
98
+ .limit(10)
99
+ .page(3)
100
+ .where('id', 1);
101
+
102
+ query.compile('mysql').sql;
103
+ // SELECT * FROM users WHERE id = ? OFFSET 20 LIMIT 10
104
+
105
+ query.compile('pg').sql;
106
+ // SELECT * FROM users WHERE id = $1 OFFSET 20 LIMIT 10
107
+
108
+ query.compile('mssql').sql;
109
+ // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
110
+
111
+ query.compile('oracle').sql;
112
+ // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
113
+ ```
114
+
115
+ ### Building the Query
116
+
117
+ Use the following methods to build queries.
118
+
119
+ - `query.where(column, operator, value)` - Require column satisfy operator
120
+ - `query.where(column, value)` - Require column equal a value
121
+ - `query.where(expression)` - Add an arbitrary WHERE expression
122
+ - `query.where(columnValueRecord)` - Add multiple conditions
123
+ - `query.whereBetween(column, [min, max])` - Require value BETWEEN, < or >
124
+ - `query.orWhere(conditions)` - Specify multiple `where()`s joined by `OR`
125
+ - `query.having(column, operator, value)` - Having column satisfy operator
126
+ - `query.having(column, value)` - Having column equal value
127
+ - `query.having(expression)` - Having an arbitrary expression
128
+ - `query.having(columnValueRecord)` - Add multiple conditions
129
+ - `query.orHaving(expressions)` - Multiple `having()`s joined by OR
130
+ - `query.orderBy(column)` - Add ORDER BY clause
131
+ - `query.sortField(column, mapNames)` - Add ORDER BY clause with mapNames
132
+ - `query.limit(num)` - Limit by the given number
133
+ - `query.offset(num)` - Specify an offset
134
+ - `query.page(num)` - Automatically calculate offset based on limit and page
135
+ - `query.columns(columnNames)` - Add column names to fetch (alias `select()`)
136
+ - `query.column(columnName)` - Add a column name to fetch
137
+ - `query.table(tableName)` - Specify the table in the FROM clause
138
+ - `query.from(tableName)` - Same as above
139
+ - `query.join(expression)` - Add a JOIN expression
140
+ - `query.innerJoin(expression)` - Add an INNER JOIN expression
141
+ - `query.leftJoin(expression)` - Add a LEFT JOIN expression
142
+ - `query.fullJoin(expression)` - Add a FULL JOIN expression
143
+ - `query.rightJoin(expression)` - Add a RIGHT JOIN expression
144
+ - `query.crossJoin(expression)` - Add a CROSS JOIN expression
145
+ - `query.leftOuterJoin(expression)` - Add a LEFT OUTER JOIN expression
146
+ - `query.fullOuterJoin(expression)` - Add a FULL OUTER JOIN expression
147
+ - `query.rightOuterJoin(expression)` - Add a RIGHT OUTER JOIN expression
148
+ - `query.groupBy(column)` - Group by a column or expression
149
+
150
+ ### Counting Results
151
+
152
+ One powerful feature of SelectBuilder is that it can construct a count query
153
+ to determine the number of results that would have been returned if there were
154
+ no LIMIT.
155
+
156
+ ```js
157
+ const query = new SelectBuilder('SELECT id, name FROM users LIMIT 5');
158
+
159
+ const { sql } = query.compileCount();
160
+ // SELECT COUNT(*) AS found_rows FROM users
161
+ ```
162
+
163
+ With queries that have a "HAVING" clause, the main query will be wrapped in a
164
+ count query.
165
+
166
+ ```js
167
+ const query = new SelectBuilder(`
168
+ SELECT category, COUNT(*)
169
+ FROM posts
170
+ GROUP BY category
171
+ HAVING COUNT(*) > 1
172
+ `);
173
+
174
+ const { sql } = query.compileCount();
175
+ /*
176
+ SELECT COUNT(*) AS found_rows FROM (
177
+ SELECT category, COUNT(*)
178
+ FROM posts
179
+ GROUP BY category
180
+ HAVING COUNT(*) > 1
181
+ ) AS subquery_results
182
+ */
183
+ ```
184
+
185
+ ### Other methods
186
+
187
+ SelectBuilder has a few other useful methods.
188
+
189
+ - `query.getClone()` - Get an exact copy of this query object
190
+ - `query.unjoin(tableName)` - Remove a join expression
191
+ - `query.reset(field)` - Reset a single aspect of the query (e.g. 'where' or 'having')
192
+ - `query.reset(fields)` - Reset a few particular aspects of the query (e.g. \['where', 'having'\])
193
+ - `query.reset()` - Reset query to an empty state
194
+
195
+ ### Parser Limitations
196
+
197
+ `new SelectBuilder(sql)` uses regular expressions and is not a true parser.
198
+ The goal is to be fast and useful for 99% of situations.
199
+
200
+ Below are some limitations illustrated by example.
201
+
202
+ #### Nested Subqueries
203
+
204
+ Most subqueries can be parsed but sub-subqueries don't work.
205
+
206
+ ```js
207
+ // ❌ WILL NOT WORK
208
+ const query = new SelectBuilder(`
209
+ SELECT * FROM categories_posts WHERE category_id IN(
210
+ SELECT id FROM categories WHERE client_id IN(
211
+ SELECT client_id FROM affiliations WHERE name LIKE 'test'
212
+ )
213
+ )`);
214
+
215
+ // ✅ WORKING EQUIVALENT
216
+ const query = new SelectBuilder(`SELECT * FROM categories_posts`);
217
+ const subquery =
218
+ new SelectBuilder(`SELECT id FROM categories WHERE client_id IN(
219
+ SELECT client_id FROM affiliations WHERE name LIKE 'test'
220
+ )`);
221
+ query.where(`category_id IN(${subquery})`);
222
+ ```
223
+
224
+ #### Keywords in Strings
225
+
226
+ If you need to use SQL keywords in strings, use bindings.
227
+
228
+ ```js
229
+ // ❌ WILL NOT WORK
230
+ new SelectBuilder(`SELECT id, CONCAT('where ', expr) FROM users`);
231
+
232
+ // ✅ WORKING EQUIVALENT
233
+ new SelectBuilder(`SELECT id, CONCAT(:prefix, expr) FROM users`, {
234
+ prefix: 'where ',
235
+ });
236
+ ```
237
+
238
+ #### Nested OR and AND Clauses
239
+
240
+ Nested logic can't be parsed properly.
241
+
242
+ ```js
243
+ // ❌ WILL NOT WORK
244
+ new SelectBuilder(`
245
+ SELECT * FROM users
246
+ WHERE (
247
+ fname = 'Matthew' AND (
248
+ lname LIKE '%john' OR lname LIKE 'john%'
249
+ ) OR (
250
+ id > 0 AND is_active IS NOT NULL
251
+ )
252
+ )
253
+ `);
254
+
255
+ // ✅ WORKING EQUIVALENT
256
+ const query = new SelectBuilder(`SELECT * FROM users`);
257
+ query.orWhere([
258
+ "fname = 'Matthew' AND (lname LIKE '%john' OR lname LIKE 'john%')",
259
+ 'id > 0 AND is_active IS NOT NULL',
260
+ ]);
261
+ ```
package/codecov.yml ADDED
@@ -0,0 +1,7 @@
1
+ codecov:
2
+ branch: main
3
+
4
+ coverage:
5
+ precision: 1
6
+ round: down
7
+ range: '70...100'
package/dist/.gitkeep ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ export { default as Parser } from './src/Parser/Parser';
2
+ export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as Parser } from './src/Parser/Parser';
2
+ export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
@@ -0,0 +1,157 @@
1
+ import type Select from '../SelectBuilder/SelectBuilder';
2
+ /**
3
+ * Parse SQL and populate onto a Select query object
4
+ */
5
+ export default class Parser {
6
+ query: Select;
7
+ /**
8
+ * Create a new instance
9
+ * @param query A Select object on which to build parsed conditions
10
+ */
11
+ constructor(query: Select);
12
+ /**
13
+ * Strip single-line and multi-line comment blocks
14
+ * @param sql The SQL string
15
+ * @return The SQL string without comments
16
+ * @private
17
+ */
18
+ _stripComments(sql: string): string;
19
+ /**
20
+ * Before splitting into SQL clauses, extract some regex-able subQueries
21
+ * @param sql The unparsed sql string
22
+ * @return An array with new sql and subQueries
23
+ * @private
24
+ */
25
+ _extractSubQueries(sql: string): {
26
+ sql: string;
27
+ subQueries: {};
28
+ };
29
+ /**
30
+ * Inject column subQueries back into this object
31
+ * @param subQueries The lookup of extracted subQueries
32
+ * @private
33
+ */
34
+ _injectSubQueries(subQueries: Record<string, string>): void;
35
+ /**
36
+ * Split SQL into clauses (used by parse())
37
+ * @param sql The SQL to split
38
+ * @return
39
+ * @private
40
+ */
41
+ _split(sql: string): string[];
42
+ /**
43
+ * Get a QuickSelect object representing the given SQL SELECT statement
44
+ * @param rawSql The raw SQL for the SELECT statement
45
+ * @return True if the SQL was parsed successfully
46
+ */
47
+ parse(rawSql: string): boolean;
48
+ /**
49
+ * Handle SQL_CALC_FOUND_ROWS and column names
50
+ * @param clause The clause after the SELECT
51
+ * @private
52
+ */
53
+ _handleSELECT(clause: string): void;
54
+ /**
55
+ * Handle table names
56
+ * @param clause The clause after the FROM
57
+ * @private
58
+ */
59
+ _handleFROM(clause: string): void;
60
+ /**
61
+ * Handle INNER JOIN statements
62
+ * @param clause The clause after the INNER JOIN
63
+ * @private
64
+ */
65
+ _handleINNER_JOIN(clause: string): void;
66
+ /**
67
+ * Handle LEFT JOIN statements
68
+ * @param clause The clause after the LEFT JOIN
69
+ * @private
70
+ */
71
+ _handleLEFT_JOIN(clause: string): void;
72
+ /**
73
+ * Handle LEFT OUTER JOIN statements
74
+ * @param clause The clause after the LEFT OUTER JOIN
75
+ * @private
76
+ */
77
+ _handleLEFT_OUTER_JOIN(clause: string): void;
78
+ /**
79
+ * Handle RIGHT JOIN statements
80
+ * @param clause The clause after the RIGHT JOIN
81
+ * @private
82
+ */
83
+ _handleRIGHT_JOIN(clause: string): void;
84
+ /**
85
+ * Handle RIGHT OUTER JOIN statements
86
+ * @param clause The clause after the RIGHT OUTER JOIN
87
+ * @private
88
+ */
89
+ _handleRIGHT_OUTER_JOIN(clause: string): void;
90
+ /**
91
+ * Handle CROSS JOIN statements
92
+ * @param clause The clause after the CROSS JOIN
93
+ * @private
94
+ */
95
+ _handleCROSS_JOIN(clause: string): void;
96
+ /**
97
+ * Handle FULL JOIN statements
98
+ * @param clause The clause after the FULL JOIN
99
+ * @private
100
+ */
101
+ _handleFULL_JOIN(clause: string): void;
102
+ /**
103
+ * Handle FULL OUTER JOIN statements
104
+ * @param clause The clause after the FULL OUTER JOIN
105
+ * @private
106
+ */
107
+ _handleFULL_OUTER_JOIN(clause: string): void;
108
+ /**
109
+ * Handle WHERE conditions
110
+ * @param clause All the conditions after WHERE
111
+ * @private
112
+ */
113
+ _handleWHERE(clause: string): void;
114
+ /**
115
+ * Handle HAVING statements
116
+ * @param clause All the conditions after HAVING
117
+ * @private
118
+ */
119
+ _handleHAVING(clause: string): void;
120
+ /**
121
+ * Build a conditions list
122
+ * @param type Either where or having
123
+ * @param clause The expressions following the type keyword
124
+ * @private
125
+ */
126
+ _handleConditions(type: 'where' | 'having', clause: string): void;
127
+ /**
128
+ * Handle GROUP BY statements
129
+ * @param clause The clauses after the GROUP BY
130
+ * @private
131
+ */
132
+ _handleGROUP_BY(clause: string): void;
133
+ /**
134
+ * Handle ORDER BY statements
135
+ * @param clause The clause after the ORDER BY
136
+ * @private
137
+ */
138
+ _handleORDER_BY(clause: string): void;
139
+ /**
140
+ * Handle LIMIT statements including "LIMIT #" and "LIMIT #, #"
141
+ * @param clause The clause after the LIMIT
142
+ * @private
143
+ */
144
+ _handleLIMIT(clause: string): void;
145
+ /**
146
+ * Handle OFFSET statements
147
+ * @param clause The number after the OFFSET
148
+ * @private
149
+ */
150
+ _handleOFFSET(clause: string): void;
151
+ /**
152
+ * Handle OFFSET statements
153
+ * @param clause The number after the OFFSET
154
+ * @private
155
+ */
156
+ _handleFETCH_NEXT(clause: string): void;
157
+ }