@possumtech/sqlrite 0.2.4 → 1.0.2
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/.github/ISSUE_TEMPLATE/build.md +9 -0
- package/.github/ISSUE_TEMPLATE/chore.md +9 -0
- package/.github/ISSUE_TEMPLATE/ci.md +9 -0
- package/.github/ISSUE_TEMPLATE/docs.md +9 -0
- package/.github/ISSUE_TEMPLATE/feat.md +12 -0
- package/.github/ISSUE_TEMPLATE/fix.md +12 -0
- package/.github/ISSUE_TEMPLATE/perf.md +9 -0
- package/.github/ISSUE_TEMPLATE/refactor.md +9 -0
- package/.github/ISSUE_TEMPLATE/revert.md +9 -0
- package/.github/ISSUE_TEMPLATE/style.md +9 -0
- package/.github/ISSUE_TEMPLATE/test.md +9 -0
- package/README.md +100 -138
- package/SqlRite.d.ts +22 -10
- package/SqlRite.js +84 -72
- package/SqlRiteCore.js +99 -0
- package/SqlRiteSync.js +56 -0
- package/SqlWorker.js +68 -0
- package/biome.json +16 -0
- package/package.json +12 -4
- package/test/test.js +121 -32
- package/sql/test.sql +0 -35
package/README.md
CHANGED
|
@@ -1,187 +1,149 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🪨 SqlRite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@possumtech/sqlrite)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**SQL Done Right.** A high-performance, opinionated, and LLM-ready wrapper for Node.js native SQLite.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
alternative to ORMs. It is a thin wrapper around the
|
|
9
|
-
[native sqlite module](https://nodejs.org/api/sqlite.html), which
|
|
10
|
-
enables one to separate SQL code from Javascript code.
|
|
9
|
+
---
|
|
11
10
|
|
|
12
|
-
##
|
|
11
|
+
## 📖 About
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
SqlRite is a thin, zero-dependency wrapper around the [native Node.js `sqlite` module](https://nodejs.org/api/sqlite.html). It enforces a clean separation of concerns by treating SQL as a first-class citizen, enabling a development workflow that is faster, more secure, and optimized for modern AI coding assistants.
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
contains a native sqlite module. Sqlite is the standard for SQL.
|
|
15
|
+
### Why SqlRite?
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
1. **⚡ Zero-Config Prepared Statements**: Define SQL in `.sql` files; call them as native JS methods.
|
|
18
|
+
2. **🧵 True Non-Blocking I/O**: The default async model offloads all DB operations to a dedicated Worker Thread.
|
|
19
|
+
3. **📦 LLM-Ready Architecture**: By isolating SQL from JS boilerplate, you provide AI agents with a clean, high-signal "Source of Truth" for your data layer.
|
|
20
|
+
4. **🧩 Locality of Behavior**: Keep your SQL files right next to the JS logic that uses them.
|
|
21
|
+
5. **🚀 Modern Standards**: Built for Node 25+, ESM-native, and uses the latest `node:sqlite` primitives.
|
|
22
|
+
6. **🛡️ Production-Ready Defaults**: Automatically enables WAL mode, Foreign Key enforcement, and DML Strictness.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
---
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
your queries are compiled and cached by the sqlite engine.
|
|
26
|
+
## 🛠 Installation
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
7. **Size**: By relying on the native sqlite module, and nothing else, sqlrite
|
|
32
|
-
won't bloat your project with unnecessary dependencies. This thing is *tiny*.
|
|
33
|
-
|
|
34
|
-
## Usage
|
|
35
|
-
|
|
36
|
-
**SQL**
|
|
37
|
-
|
|
38
|
-
Add a `sql` folder to your project and include as many `.sql` files as you
|
|
39
|
-
wish, with whatever folder structure you like. Sqlrite will automatically load
|
|
40
|
-
them all.
|
|
41
|
-
|
|
42
|
-
| Syntax | Name | Description |
|
|
43
|
-
|---------------------|------------------------|------------------------|
|
|
44
|
-
| `-- INIT: txName` | Executed Transaction | Executed transaction |
|
|
45
|
-
| `-- EXEC: txName` | Executable Transaction | Executable transaction |
|
|
46
|
-
| `-- PREP: stmtName` | Prepared Statements | Prepared statement |
|
|
47
|
-
|
|
48
|
-
There are three types of "chunk" one can add to a `.sql` file:
|
|
49
|
-
|
|
50
|
-
1. **INIT**: A transaction that is executed when the module is instantiated.
|
|
51
|
-
This is where you should create your tables, for example.
|
|
52
|
-
|
|
53
|
-
2. **EXEC**: A transaction that can be executed at any time. For example,
|
|
54
|
-
dropping a table. This is where you should put your transactions that are
|
|
55
|
-
not prepared statements, like maintaining your database.
|
|
56
|
-
|
|
57
|
-
3. **PREP**: A prepared statement that can be executed at any time. This is
|
|
58
|
-
where you should put your queries. After declaring a prepared statement, you can
|
|
59
|
-
then run it with either the `.all({})`, `.get({})` or `.run({})` methods, as per
|
|
60
|
-
the native sqlite API.
|
|
61
|
-
|
|
62
|
-
| Method | Description |
|
|
63
|
-
|------------|-------------------------------------------------------|
|
|
64
|
-
| `.all({})` | Returns all rows that match the query. |
|
|
65
|
-
| `.get({})` | Returns the first row that matches the query. |
|
|
66
|
-
| `.run({})` | Executes the query and returns the (optional) result. |
|
|
67
|
-
|
|
68
|
-
### Synchronous/Asynchronous
|
|
69
|
-
|
|
70
|
-
The native sqlite module currently only supports synchronous operations. This
|
|
71
|
-
can pose a performance issue in some common cases. Sqlrite addresses this by
|
|
72
|
-
allowing one to run one's queries asynchronously by appending `.async`:
|
|
73
|
-
|
|
74
|
-
For example, instead of:
|
|
75
|
-
|
|
76
|
-
**Synchronous**
|
|
77
|
-
|
|
78
|
-
```js
|
|
79
|
-
console.log(sql.getPositions.all());
|
|
28
|
+
```bash
|
|
29
|
+
npm install @possumtech/sqlrite
|
|
80
30
|
```
|
|
81
31
|
|
|
82
|
-
|
|
32
|
+
---
|
|
83
33
|
|
|
84
|
-
|
|
85
|
-
sql.async.getPositions.all().then((positions) => console.log(positions));
|
|
86
|
-
```
|
|
34
|
+
## 🚀 Quick Start
|
|
87
35
|
|
|
36
|
+
### 1. Define your SQL (`src/users.sql`)
|
|
88
37
|
|
|
89
|
-
|
|
38
|
+
SqlRite uses simple metadata headers to turn SQL chunks into JS methods. We recommend using `STRICT` tables for maximum type safety.
|
|
90
39
|
|
|
91
40
|
```sql
|
|
92
|
-
-- INIT:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
41
|
+
-- INIT: createUsers
|
|
42
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
name TEXT NOT NULL,
|
|
45
|
+
meta TEXT
|
|
46
|
+
) STRICT;
|
|
47
|
+
|
|
48
|
+
-- PREP: addUser
|
|
49
|
+
INSERT INTO users (name, meta) VALUES ($name, $meta);
|
|
50
|
+
|
|
51
|
+
-- PREP: getUserByName
|
|
52
|
+
SELECT * FROM users WHERE name = $name;
|
|
53
|
+
```
|
|
101
54
|
|
|
102
|
-
|
|
55
|
+
### 2. Use it in Javascript
|
|
103
56
|
|
|
104
|
-
|
|
105
|
-
|
|
57
|
+
#### Asynchronous (Default - Recommended)
|
|
58
|
+
Uses Worker Threads to keep your main event loop free.
|
|
106
59
|
|
|
107
|
-
|
|
60
|
+
```javascript
|
|
61
|
+
import SqlRite from "@possumtech/sqlrite";
|
|
108
62
|
|
|
109
|
-
|
|
63
|
+
const sql = new SqlRite({
|
|
64
|
+
path: "data.db",
|
|
65
|
+
dir: "src"
|
|
66
|
+
});
|
|
110
67
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
68
|
+
// PREP chunks expose .all(), .get(), and .run()
|
|
69
|
+
// Objects/Arrays are automatically stringified for you!
|
|
70
|
+
await sql.addUser.run({
|
|
71
|
+
name: "Alice",
|
|
72
|
+
meta: { theme: "dark", preferences: [1, 2, 3] }
|
|
73
|
+
});
|
|
114
74
|
|
|
115
|
-
|
|
116
|
-
|
|
75
|
+
const user = await sql.getUserByName.get({ name: "Alice" });
|
|
76
|
+
console.log(JSON.parse(user.meta).theme); // Manual parse required for output
|
|
117
77
|
|
|
118
|
-
|
|
119
|
-
SELECT name FROM employees ORDER BY salary DESC LIMIT 1;
|
|
78
|
+
await sql.close();
|
|
120
79
|
```
|
|
121
80
|
|
|
122
|
-
|
|
81
|
+
#### Synchronous
|
|
82
|
+
Ideal for CLI tools, migrations, or scripts.
|
|
123
83
|
|
|
124
|
-
```
|
|
125
|
-
import
|
|
84
|
+
```javascript
|
|
85
|
+
import { SqlRiteSync } from "@possumtech/sqlrite";
|
|
126
86
|
|
|
127
|
-
const sql = new
|
|
87
|
+
const sql = new SqlRiteSync({ dir: ["src", "migrations"] });
|
|
88
|
+
const users = sql.getUserByName.all({ name: "Alice" });
|
|
89
|
+
sql.close();
|
|
90
|
+
```
|
|
128
91
|
|
|
129
|
-
|
|
130
|
-
sql.addEmployee.run({ name: "Jane", position: "COO", salary: 49998 });
|
|
131
|
-
sql.addEmployee.run({ name: "Jack", position: "CFO", salary: 49997 });
|
|
132
|
-
sql.addEmployee.run({ name: "Jill", position: "CIO", salary: 49996 });
|
|
92
|
+
---
|
|
133
93
|
|
|
134
|
-
|
|
94
|
+
## 🤖 LLM-Ready Architecture
|
|
135
95
|
|
|
136
|
-
|
|
96
|
+
In the era of AI-assisted engineering, **Context is King**.
|
|
137
97
|
|
|
138
|
-
|
|
98
|
+
SqlRite's "SQL-First" approach is specifically designed to maximize the effectiveness of LLMs (like Gemini, Claude, and GPT):
|
|
139
99
|
|
|
140
|
-
|
|
100
|
+
* **High Signal-to-Noise**: When you feed a `.sql` file to an LLM, it sees 100% schema and logic, 0% Javascript boilerplate. This prevents "context contamination" and hallucination.
|
|
101
|
+
* **Schema Awareness**: Agents can instantly "understand" your entire database contract by reading the isolated SQL files, making them significantly better at generating correct queries.
|
|
102
|
+
* **Clean Diffs**: AI-generated refactors of your data layer stay within `.sql` files, keeping your JS history clean and your logic easier to audit.
|
|
141
103
|
|
|
142
|
-
|
|
143
|
-
```
|
|
104
|
+
---
|
|
144
105
|
|
|
145
|
-
##
|
|
106
|
+
## 💎 Features & Syntax
|
|
146
107
|
|
|
147
|
-
|
|
108
|
+
### Modern Defaults
|
|
148
109
|
|
|
149
|
-
|
|
150
|
-
npm install @possumtech/sqlrite
|
|
151
|
-
```
|
|
110
|
+
SqlRite automatically executes these PRAGMAs on every connection to ensure high performance and data integrity:
|
|
152
111
|
|
|
153
|
-
|
|
154
|
-
|
|
112
|
+
* **WAL Mode**: `PRAGMA journal_mode = WAL` enables concurrent readers and writers.
|
|
113
|
+
* **Foreign Keys**: `PRAGMA foreign_keys = ON` enforces relational constraints.
|
|
114
|
+
* **DML Strict Mode**: `PRAGMA dml_strict = ON` catches common SQL errors (like using double quotes for strings).
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
mkdir sql
|
|
158
|
-
cd sql
|
|
159
|
-
touch exampleFile.sql
|
|
160
|
-
```
|
|
116
|
+
### Metadata Headers
|
|
161
117
|
|
|
162
|
-
|
|
118
|
+
| Syntax | Name | Behavior |
|
|
119
|
+
| :--- | :--- | :--- |
|
|
120
|
+
| `-- INIT: name` | **Initializer** | Runs once automatically when `SqlRite` is instantiated. |
|
|
121
|
+
| `-- EXEC: name` | **Transaction** | Exposes a method `sql.name()` for one-off SQL execution. |
|
|
122
|
+
| `-- PREP: name` | **Statement** | Compiles a Prepared Statement; exposes `.all()`, `.get()`, and `.run()`. |
|
|
163
123
|
|
|
164
|
-
|
|
165
|
-
import SqlRite from "@possumtech/sqlrite";
|
|
124
|
+
### Locality & Multi-Directory Support
|
|
166
125
|
|
|
167
|
-
|
|
168
|
-
// SQLite database file path.
|
|
169
|
-
path: ":memory:",
|
|
126
|
+
You don't have to put all your SQL in one folder. SqlRite encourages placing SQL files exactly where they are needed:
|
|
170
127
|
|
|
171
|
-
|
|
172
|
-
|
|
128
|
+
```javascript
|
|
129
|
+
const sql = new SqlRite({
|
|
130
|
+
dir: ["src/auth", "src/billing", "src/shared/sql"]
|
|
173
131
|
});
|
|
174
132
|
```
|
|
175
133
|
|
|
176
|
-
|
|
177
|
-
database file. Otherwise, the database will be created in memory and lost when
|
|
178
|
-
the process ends.
|
|
134
|
+
Files are sorted **numerically by filename prefix** across all directories (e.g., `001-setup.sql` will always run before `002-seed.sql`), ensuring deterministic migrations.
|
|
179
135
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## ⚙️ Configuration
|
|
139
|
+
|
|
140
|
+
| Option | Type | Default | Description |
|
|
141
|
+
| :--- | :--- | :--- | :--- |
|
|
142
|
+
| `path` | `string` | `":memory:"` | Path to the SQLite database file. |
|
|
143
|
+
| `dir` | `string\|string[]` | `"sql"` | Directory or directories to scan for `.sql` files. |
|
|
144
|
+
|
|
145
|
+
---
|
|
183
146
|
|
|
184
|
-
|
|
185
|
-
module.
|
|
147
|
+
## 📄 License
|
|
186
148
|
|
|
187
|
-
|
|
149
|
+
MIT © [@wikitopian](https://github.com/wikitopian)
|
package/SqlRite.d.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface SqlRiteOptions {
|
|
1
|
+
export interface SqlRiteOptions {
|
|
4
2
|
path?: string;
|
|
5
|
-
dir?: string;
|
|
3
|
+
dir?: string | string[];
|
|
6
4
|
}
|
|
7
5
|
|
|
8
|
-
interface
|
|
6
|
+
export interface SqlRitePreparedStatements {
|
|
9
7
|
all: (params?: Record<string, any>) => Promise<any[]>;
|
|
10
8
|
get: (params?: Record<string, any>) => Promise<any>;
|
|
11
|
-
run: (params?: Record<string, any>) => Promise<
|
|
9
|
+
run: (params?: Record<string, any>) => Promise<any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SqlRiteSyncPreparedStatements {
|
|
13
|
+
all: (params?: Record<string, any>) => any[];
|
|
14
|
+
get: (params?: Record<string, any>) => any;
|
|
15
|
+
run: (params?: Record<string, any>) => any;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
export class SqlRiteSync {
|
|
19
|
+
constructor(options?: SqlRiteOptions);
|
|
20
|
+
close(): void;
|
|
21
|
+
[key: string]:
|
|
22
|
+
| ((params?: Record<string, any>) => void)
|
|
23
|
+
| SqlRiteSyncPreparedStatements
|
|
24
|
+
| any;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export default class SqlRite {
|
|
19
28
|
constructor(options?: SqlRiteOptions);
|
|
20
|
-
|
|
21
|
-
[key: string]:
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
[key: string]:
|
|
31
|
+
| ((params?: Record<string, any>) => Promise<void>)
|
|
32
|
+
| SqlRitePreparedStatements
|
|
33
|
+
| any;
|
|
22
34
|
}
|
package/SqlRite.js
CHANGED
|
@@ -1,95 +1,107 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
import SqlRiteSync from "./SqlRiteSync.js";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export { SqlRiteSync };
|
|
3
9
|
|
|
4
10
|
export default class SqlRite {
|
|
11
|
+
#worker = null;
|
|
12
|
+
#id = 0;
|
|
13
|
+
#promises = new Map();
|
|
14
|
+
#readyPromise = Promise.resolve();
|
|
15
|
+
#protected = new Set(["exec", "close", "constructor"]);
|
|
16
|
+
|
|
5
17
|
constructor(options = {}) {
|
|
6
18
|
const defaults = {
|
|
7
19
|
path: ":memory:",
|
|
8
20
|
dir: "sql",
|
|
9
21
|
};
|
|
10
|
-
|
|
11
22
|
const merged = { ...defaults, ...options };
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.close = () => db.close();
|
|
16
|
-
|
|
17
|
-
// allow multiple directories
|
|
18
|
-
if (!Array.isArray(merged.dir)) merged.dir = [merged.dir];
|
|
19
|
-
const files = merged.dir.flatMap((d) => this.getFiles(d));
|
|
20
|
-
|
|
21
|
-
const code = files.map((f) => fs.readFileSync(f, "utf8")).join("");
|
|
22
|
-
|
|
23
|
-
this.async = {};
|
|
24
|
-
|
|
25
|
-
const chunks =
|
|
26
|
-
/-- (?<chunk>(?<type>INIT|EXEC|PREP): (?<name>\w+)\n(?<sql>.*?))($|(?=-- (INIT|EXEC|PREP):))/gs;
|
|
27
|
-
|
|
28
|
-
const initChunks = [];
|
|
29
|
-
const execChunks = [];
|
|
30
|
-
const prepChunks = [];
|
|
31
|
-
|
|
32
|
-
for (const chunk of code.matchAll(chunks)) {
|
|
33
|
-
const { type } = chunk.groups;
|
|
34
|
-
|
|
35
|
-
if (type === "INIT") initChunks.push(chunk.groups);
|
|
36
|
-
if (type === "EXEC") execChunks.push(chunk.groups);
|
|
37
|
-
if (type === "PREP") prepChunks.push(chunk.groups);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
initChunks.forEach((init) => {
|
|
41
|
-
db.exec(init.sql);
|
|
24
|
+
this.#worker = new Worker(path.join(__dirname, "SqlWorker.js"), {
|
|
25
|
+
workerData: { options: merged },
|
|
42
26
|
});
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
this
|
|
46
|
-
|
|
28
|
+
this.#readyPromise = new Promise((resolve) => {
|
|
29
|
+
this.#worker.on("message", (msg) => {
|
|
30
|
+
if (msg.type === "READY") {
|
|
31
|
+
this.#setupMethods(msg.names);
|
|
32
|
+
resolve();
|
|
33
|
+
} else if (msg.id !== undefined) {
|
|
34
|
+
const promise = this.#promises.get(msg.id);
|
|
35
|
+
if (promise) {
|
|
36
|
+
this.#promises.delete(msg.id);
|
|
37
|
+
if (msg.error) promise.reject(new Error(msg.error));
|
|
38
|
+
else promise.resolve(msg.result);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
47
42
|
});
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
// Fallback for methods not yet defined or dynamic ones
|
|
45
|
+
return new Proxy(this, {
|
|
46
|
+
get: (target, prop, _receiver) => {
|
|
47
|
+
if (prop in target) {
|
|
48
|
+
const val = target[prop];
|
|
49
|
+
if (typeof val === "function") return val.bind(target);
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
if (typeof prop === "symbol" || prop === "then") {
|
|
53
|
+
return target[prop];
|
|
54
|
+
}
|
|
55
|
+
// Return a proxy that can handle .all(), .get(), .run() or direct calls
|
|
56
|
+
return new Proxy(() => {}, {
|
|
57
|
+
apply: (_t, _thisArg, args) => {
|
|
58
|
+
return target.#callWorker("EXEC", prop, null, args[0]);
|
|
59
|
+
},
|
|
60
|
+
get: (_t, method) => {
|
|
61
|
+
if (["all", "get", "run"].includes(method)) {
|
|
62
|
+
return (params) =>
|
|
63
|
+
target.#callWorker(
|
|
64
|
+
`PREP_${method.toUpperCase()}`,
|
|
65
|
+
prop,
|
|
66
|
+
null,
|
|
67
|
+
params,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
},
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
|
|
76
|
+
#setupMethods(names) {
|
|
77
|
+
for (const name of names.EXEC) {
|
|
78
|
+
if (this.#protected.has(name)) continue;
|
|
79
|
+
this[name] = (params) => this.#callWorker("EXEC", name, null, params);
|
|
80
|
+
}
|
|
81
|
+
for (const name of names.PREP) {
|
|
82
|
+
if (this.#protected.has(name)) continue;
|
|
83
|
+
this[name] = {
|
|
84
|
+
all: (params) => this.#callWorker("PREP_ALL", name, null, params),
|
|
85
|
+
get: (params) => this.#callWorker("PREP_GET", name, null, params),
|
|
86
|
+
run: (params) => this.#callWorker("PREP_RUN", name, null, params),
|
|
87
|
+
};
|
|
81
88
|
}
|
|
89
|
+
}
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
async #callWorker(type, name, sql, params) {
|
|
92
|
+
await this.#readyPromise;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const id = this.#id++;
|
|
95
|
+
this.#promises.set(id, { resolve, reject });
|
|
96
|
+
this.#worker.postMessage({ id, type, name, sql, params });
|
|
97
|
+
});
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
params[param] = JSON.stringify(params[param]);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
100
|
+
async exec(sql) {
|
|
101
|
+
return this.#callWorker("EXEC", null, sql, null);
|
|
102
|
+
}
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
async close() {
|
|
105
|
+
return this.#callWorker("CLOSE");
|
|
94
106
|
}
|
|
95
107
|
}
|
package/SqlRiteCore.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default class SqlRiteCore {
|
|
5
|
+
static #CHUNK_REGEX = /^-- (INIT|EXEC|PREP): (\w+)/;
|
|
6
|
+
|
|
7
|
+
static getFiles(dir) {
|
|
8
|
+
const files = [];
|
|
9
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
const fullPath = path.join(dir, item.name);
|
|
13
|
+
if (item.isDirectory()) {
|
|
14
|
+
files.push(...SqlRiteCore.getFiles(fullPath));
|
|
15
|
+
} else if (item.name.endsWith(".sql")) {
|
|
16
|
+
files.push(fullPath);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return SqlRiteCore.#sortFiles(files);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static #sortFiles(files) {
|
|
24
|
+
return [...files].sort((a, b) => {
|
|
25
|
+
const aBase = path.basename(a);
|
|
26
|
+
const bBase = path.basename(b);
|
|
27
|
+
const aMatch = aBase.match(/^(\d+)/);
|
|
28
|
+
const bMatch = bBase.match(/^(\d+)/);
|
|
29
|
+
|
|
30
|
+
if (aMatch && bMatch) {
|
|
31
|
+
const aNum = Number.parseInt(aMatch[1], 10);
|
|
32
|
+
const bNum = Number.parseInt(bMatch[1], 10);
|
|
33
|
+
if (aNum !== bNum) return aNum - bNum;
|
|
34
|
+
} else if (aMatch) {
|
|
35
|
+
return -1;
|
|
36
|
+
} else if (bMatch) {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return a.localeCompare(b);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static parseSql(files) {
|
|
45
|
+
const chunks = { INIT: [], EXEC: [], PREP: [] };
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const content = fs.readFileSync(file, "utf8");
|
|
49
|
+
SqlRiteCore.#processContent(content, chunks);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return SqlRiteCore.#trimChunks(chunks);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static #processContent(content, chunks) {
|
|
56
|
+
const lines = content.split(/\r?\n/);
|
|
57
|
+
let currentChunk = null;
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const match = line.match(SqlRiteCore.#CHUNK_REGEX);
|
|
61
|
+
if (match) {
|
|
62
|
+
const [_, type, name] = match;
|
|
63
|
+
currentChunk = { type, name, sql: "" };
|
|
64
|
+
chunks[type].push(currentChunk);
|
|
65
|
+
} else if (currentChunk) {
|
|
66
|
+
currentChunk.sql += `${line}\n`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static #trimChunks(chunks) {
|
|
72
|
+
for (const [type, list] of Object.entries(chunks)) {
|
|
73
|
+
chunks[type] = list
|
|
74
|
+
.map((c) => ({
|
|
75
|
+
...c,
|
|
76
|
+
sql: c.sql.trim(),
|
|
77
|
+
}))
|
|
78
|
+
.filter((c) => c.sql.length > 0);
|
|
79
|
+
}
|
|
80
|
+
return chunks;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static jsonify(params) {
|
|
84
|
+
if (!params) return {};
|
|
85
|
+
const result = { ...params };
|
|
86
|
+
|
|
87
|
+
for (const [key, value] of Object.entries(result)) {
|
|
88
|
+
if (
|
|
89
|
+
Array.isArray(value) ||
|
|
90
|
+
(value !== null &&
|
|
91
|
+
typeof value === "object" &&
|
|
92
|
+
value.constructor?.name === "Object")
|
|
93
|
+
) {
|
|
94
|
+
result[key] = JSON.stringify(value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
}
|
package/SqlRiteSync.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import SqlRiteCore from "./SqlRiteCore.js";
|
|
3
|
+
|
|
4
|
+
export default class SqlRiteSync {
|
|
5
|
+
#db = null;
|
|
6
|
+
#stmts = new Map();
|
|
7
|
+
#protected = new Set(["exec", "close", "constructor"]);
|
|
8
|
+
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
const defaults = {
|
|
11
|
+
path: ":memory:",
|
|
12
|
+
dir: "sql",
|
|
13
|
+
};
|
|
14
|
+
const merged = { ...defaults, ...options };
|
|
15
|
+
this.#db = new DatabaseSync(merged.path, merged);
|
|
16
|
+
|
|
17
|
+
// Performance and Safety Defaults
|
|
18
|
+
this.#db.exec("PRAGMA journal_mode = WAL;");
|
|
19
|
+
this.#db.exec("PRAGMA synchronous = NORMAL;");
|
|
20
|
+
this.#db.exec("PRAGMA foreign_keys = ON;");
|
|
21
|
+
this.#db.exec("PRAGMA dml_strict = ON;");
|
|
22
|
+
|
|
23
|
+
const dirs = Array.isArray(merged.dir) ? merged.dir : [merged.dir];
|
|
24
|
+
const files = dirs.flatMap((d) => SqlRiteCore.getFiles(d));
|
|
25
|
+
const chunks = SqlRiteCore.parseSql(files);
|
|
26
|
+
|
|
27
|
+
for (const init of chunks.INIT) {
|
|
28
|
+
this.#db.exec(init.sql);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const exec of chunks.EXEC) {
|
|
32
|
+
if (this.#protected.has(exec.name)) continue;
|
|
33
|
+
this[exec.name] = () => this.#db.exec(exec.sql);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const prep of chunks.PREP) {
|
|
37
|
+
if (this.#protected.has(prep.name)) continue;
|
|
38
|
+
const stmt = this.#db.prepare(prep.sql);
|
|
39
|
+
this.#stmts.set(prep.name, stmt);
|
|
40
|
+
|
|
41
|
+
this[prep.name] = {
|
|
42
|
+
all: (params = {}) => stmt.all(SqlRiteCore.jsonify(params)),
|
|
43
|
+
get: (params = {}) => stmt.get(SqlRiteCore.jsonify(params)),
|
|
44
|
+
run: (params = {}) => stmt.run(SqlRiteCore.jsonify(params)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exec(sql) {
|
|
50
|
+
this.#db.exec(sql);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
close() {
|
|
54
|
+
this.#db.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
package/SqlWorker.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
import SqlRiteCore from "./SqlRiteCore.js";
|
|
4
|
+
|
|
5
|
+
const { options } = workerData;
|
|
6
|
+
const db = new DatabaseSync(options.path, options);
|
|
7
|
+
|
|
8
|
+
// Performance and Safety Defaults
|
|
9
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
10
|
+
db.exec("PRAGMA synchronous = NORMAL;");
|
|
11
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
12
|
+
db.exec("PRAGMA dml_strict = ON;");
|
|
13
|
+
|
|
14
|
+
const stmts = new Map();
|
|
15
|
+
|
|
16
|
+
// Initialize
|
|
17
|
+
const dirs = Array.isArray(options.dir) ? options.dir : [options.dir];
|
|
18
|
+
const files = dirs.flatMap((d) => SqlRiteCore.getFiles(d));
|
|
19
|
+
const chunks = SqlRiteCore.parseSql(files);
|
|
20
|
+
|
|
21
|
+
for (const init of chunks.INIT) {
|
|
22
|
+
db.exec(init.sql);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const prep of chunks.PREP) {
|
|
26
|
+
const stmt = db.prepare(prep.sql);
|
|
27
|
+
stmts.set(prep.name, stmt);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
parentPort.on("message", (msg) => {
|
|
31
|
+
const { id, type, name, sql, params } = msg;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
let result;
|
|
35
|
+
if (type === "EXEC") {
|
|
36
|
+
const chunk = chunks.EXEC.find((e) => e.name === name);
|
|
37
|
+
if (chunk) {
|
|
38
|
+
db.exec(chunk.sql);
|
|
39
|
+
} else if (sql) {
|
|
40
|
+
db.exec(sql);
|
|
41
|
+
}
|
|
42
|
+
result = null;
|
|
43
|
+
} else if (type === "PREP_ALL") {
|
|
44
|
+
result = stmts.get(name).all(SqlRiteCore.jsonify(params));
|
|
45
|
+
} else if (type === "PREP_GET") {
|
|
46
|
+
result = stmts.get(name).get(SqlRiteCore.jsonify(params));
|
|
47
|
+
} else if (type === "PREP_RUN") {
|
|
48
|
+
result = stmts.get(name).run(SqlRiteCore.jsonify(params));
|
|
49
|
+
} else if (type === "CLOSE") {
|
|
50
|
+
db.close();
|
|
51
|
+
parentPort.postMessage({ id, result: null });
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
parentPort.postMessage({ id, result });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
parentPort.postMessage({ id, error: error.message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Signal ready
|
|
62
|
+
parentPort.postMessage({
|
|
63
|
+
type: "READY",
|
|
64
|
+
names: {
|
|
65
|
+
EXEC: chunks.EXEC.map((e) => e.name),
|
|
66
|
+
PREP: chunks.PREP.map((p) => p.name),
|
|
67
|
+
},
|
|
68
|
+
});
|
package/biome.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
|
|
3
|
+
"linter": {
|
|
4
|
+
"rules": {
|
|
5
|
+
"complexity": {
|
|
6
|
+
"noStaticOnlyClass": "off"
|
|
7
|
+
},
|
|
8
|
+
"correctness": {
|
|
9
|
+
"noConstructorReturn": "off"
|
|
10
|
+
},
|
|
11
|
+
"suspicious": {
|
|
12
|
+
"noExplicitAny": "off"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@possumtech/sqlrite",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "SQL Done Right",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -21,9 +21,17 @@
|
|
|
21
21
|
"author": "@wikitopian",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"main": "SqlRite.js",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=25.0.0",
|
|
26
|
+
"npm": ">=11.1.0"
|
|
27
|
+
},
|
|
24
28
|
"scripts": {
|
|
25
|
-
"lint": "
|
|
26
|
-
"test": "node --experimental-test-coverage --
|
|
27
|
-
"debug": "node --experimental-test-coverage --inspect-brk --test"
|
|
29
|
+
"lint": "biome check .",
|
|
30
|
+
"test": "node --experimental-test-coverage --test test/test.js",
|
|
31
|
+
"debug": "node --experimental-test-coverage --inspect-brk --test",
|
|
32
|
+
"check": "npm run lint && npm test"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "2.4.6"
|
|
28
36
|
}
|
|
29
37
|
}
|
package/test/test.js
CHANGED
|
@@ -1,58 +1,147 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import test from "node:test";
|
|
3
|
-
import SqlRite from "../SqlRite.js";
|
|
5
|
+
import SqlRite, { SqlRiteSync } from "../SqlRite.js";
|
|
6
|
+
import SqlRiteCore from "../SqlRiteCore.js";
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
// Setup test environment
|
|
9
|
+
if (!fs.existsSync("sql")) fs.mkdirSync("sql");
|
|
10
|
+
fs.writeFileSync(
|
|
11
|
+
"sql/001-init.sql",
|
|
12
|
+
"-- INIT: createEmployees\nCREATE TABLE IF NOT EXISTS employees (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, position TEXT NOT NULL, salary REAL NOT NULL);",
|
|
13
|
+
);
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
"sql/002-data.sql",
|
|
16
|
+
"-- PREP: addEmployee\nINSERT INTO employees (name, position, salary) VALUES ($name, $position, $salary);\n-- PREP: getPositions\nSELECT name, position FROM employees;\n-- PREP: getHighestPaidEmployee\nSELECT * FROM employees ORDER BY salary DESC LIMIT 1;\n-- EXEC: deleteTable\nDROP TABLE IF EXISTS sync_test;",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
test("SqlRiteCore", (t) => {
|
|
20
|
+
t.test("getFiles() should sort numerically", () => {
|
|
21
|
+
const files = SqlRiteCore.getFiles("sql");
|
|
22
|
+
const basenames = files.map((f) => path.basename(f));
|
|
23
|
+
assert.ok(
|
|
24
|
+
basenames.indexOf("001-init.sql") < basenames.indexOf("002-data.sql"),
|
|
25
|
+
"001 should come before 002",
|
|
26
|
+
);
|
|
9
27
|
});
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
t.test("getFiles() handles subdirectories", () => {
|
|
30
|
+
if (!fs.existsSync("sql/sub")) fs.mkdirSync("sql/sub");
|
|
31
|
+
fs.writeFileSync("sql/sub/999-last.sql", "-- INIT: subInit\nSELECT 1;");
|
|
32
|
+
const files = SqlRiteCore.getFiles("sql");
|
|
33
|
+
assert.ok(
|
|
34
|
+
files.some((f) => f.includes("999-last.sql")),
|
|
35
|
+
"Should find file in subdirectory",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
12
38
|
|
|
13
|
-
t.test("
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
sql.addEmployee.run({ name: "Jill", position: "CIO", salary: 49996 });
|
|
39
|
+
t.test("parseSql() filters empty and trims", () => {
|
|
40
|
+
fs.writeFileSync("sql/empty.sql", "-- PREP: empty\n \n ");
|
|
41
|
+
const chunks = SqlRiteCore.parseSql(["sql/empty.sql"]);
|
|
42
|
+
assert.strictEqual(chunks.PREP.length, 0, "Should filter empty chunks");
|
|
18
43
|
});
|
|
19
44
|
|
|
20
|
-
t.test("
|
|
21
|
-
const
|
|
22
|
-
|
|
45
|
+
t.test("jsonify() handles objects and arrays", () => {
|
|
46
|
+
const input = { arr: [1, 2], obj: { a: 1 }, str: "val", nil: null };
|
|
47
|
+
const output = SqlRiteCore.jsonify(input);
|
|
48
|
+
assert.strictEqual(output.arr, "[1,2]");
|
|
49
|
+
assert.strictEqual(output.obj, '{"a":1}');
|
|
50
|
+
assert.strictEqual(output.str, "val");
|
|
51
|
+
assert.strictEqual(output.nil, null);
|
|
52
|
+
assert.deepStrictEqual(
|
|
53
|
+
SqlRiteCore.jsonify(null),
|
|
54
|
+
{},
|
|
55
|
+
"Should return empty object for null params",
|
|
56
|
+
);
|
|
23
57
|
});
|
|
58
|
+
});
|
|
24
59
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
test("SqlRiteSync", (t) => {
|
|
61
|
+
const sql = new SqlRiteSync({ dir: "sql" });
|
|
62
|
+
|
|
63
|
+
t.test("initialization and INIT chunks", () => {
|
|
64
|
+
// Employees table should exist from 001-init.sql
|
|
65
|
+
const res = sql.getPositions.all();
|
|
66
|
+
assert.ok(Array.isArray(res));
|
|
28
67
|
});
|
|
29
68
|
|
|
30
|
-
t.test("
|
|
31
|
-
|
|
32
|
-
|
|
69
|
+
t.test("exec()", () => {
|
|
70
|
+
sql.exec("CREATE TABLE sync_test (id INTEGER)");
|
|
71
|
+
sql.exec("INSERT INTO sync_test VALUES (1)");
|
|
72
|
+
// No error means success
|
|
33
73
|
});
|
|
34
74
|
|
|
35
|
-
t.test("
|
|
36
|
-
sql.
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
t.test("PREP methods (all, get, run)", () => {
|
|
76
|
+
sql.addEmployee.run({
|
|
77
|
+
name: "Sync User",
|
|
78
|
+
position: "Dev",
|
|
79
|
+
salary: 50000,
|
|
80
|
+
});
|
|
81
|
+
const res = sql.getPositions.all();
|
|
82
|
+
assert.ok(res.some((e) => e.name === "Sync User"));
|
|
39
83
|
});
|
|
40
84
|
|
|
41
|
-
t.test("
|
|
85
|
+
t.test("EXEC methods", () => {
|
|
42
86
|
sql.deleteTable();
|
|
87
|
+
// No error means success
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t.test("close()", () => {
|
|
91
|
+
sql.close();
|
|
43
92
|
});
|
|
93
|
+
});
|
|
44
94
|
|
|
45
|
-
|
|
46
|
-
|
|
95
|
+
test("SqlRite (Async)", async (t) => {
|
|
96
|
+
const sql = new SqlRite({ dir: "sql" });
|
|
47
97
|
|
|
48
|
-
|
|
98
|
+
await t.test("READY signal and methods setup", async () => {
|
|
99
|
+
// Wait for ready via any method call or just a small timeout if needed
|
|
100
|
+
// but since every method awaits the readyPromise, we can just call one
|
|
101
|
+
assert.ok(typeof sql.addEmployee.run === "function");
|
|
49
102
|
});
|
|
50
103
|
|
|
51
|
-
t.test("
|
|
52
|
-
|
|
104
|
+
await t.test("PREP methods", async () => {
|
|
105
|
+
await sql.addEmployee.run({
|
|
106
|
+
name: "Async User",
|
|
107
|
+
position: "Lead",
|
|
108
|
+
salary: 90000,
|
|
109
|
+
});
|
|
110
|
+
const res = await sql.getPositions.all();
|
|
111
|
+
assert.ok(res.some((e) => e.name === "Async User"));
|
|
112
|
+
});
|
|
53
113
|
|
|
54
|
-
|
|
114
|
+
await t.test("Proxy fallback", async () => {
|
|
115
|
+
const res = await sql.getHighestPaidEmployee.get();
|
|
116
|
+
assert.strictEqual(res.name, "Async User");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await t.test("Raw SQL execution", async () => {
|
|
120
|
+
await sql.exec("CREATE TABLE async_test (id INTEGER)");
|
|
121
|
+
// Success if no throw
|
|
122
|
+
});
|
|
55
123
|
|
|
56
|
-
|
|
124
|
+
await t.test("Error handling", async () => {
|
|
125
|
+
try {
|
|
126
|
+
await sql.nonExistentMethod.all();
|
|
127
|
+
assert.fail("Should have thrown");
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Proxy fallback returns a function that calls #callWorker
|
|
130
|
+
// Since the name isn't found in EXEC or PREP, the worker will throw
|
|
131
|
+
assert.ok(err.message.includes("Cannot read properties of undefined"));
|
|
132
|
+
}
|
|
57
133
|
});
|
|
134
|
+
|
|
135
|
+
await t.test("close()", async () => {
|
|
136
|
+
await sql.close();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("Multi-directory support", () => {
|
|
141
|
+
if (!fs.existsSync("sql2")) fs.mkdirSync("sql2");
|
|
142
|
+
fs.writeFileSync("sql2/extra.sql", "-- PREP: extra\nSELECT 1 as val;");
|
|
143
|
+
const sql = new SqlRiteSync({ dir: ["sql", "sql2"] });
|
|
144
|
+
const res = sql.extra.get();
|
|
145
|
+
assert.strictEqual(res.val, 1);
|
|
146
|
+
sql.close();
|
|
58
147
|
});
|
package/sql/test.sql
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
-- INIT: createEmployeeTable
|
|
2
|
-
BEGIN TRANSACTION;
|
|
3
|
-
|
|
4
|
-
CREATE TABLE IF NOT EXISTS employees (
|
|
5
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
-
name TEXT NOT NULL,
|
|
7
|
-
position TEXT NOT NULL,
|
|
8
|
-
salary REAL NOT NULL
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
END TRANSACTION;
|
|
12
|
-
|
|
13
|
-
-- EXEC: deleteTable
|
|
14
|
-
BEGIN TRANSACTION;
|
|
15
|
-
|
|
16
|
-
DROP TABLE IF EXISTS employees;
|
|
17
|
-
|
|
18
|
-
END TRANSACTION;
|
|
19
|
-
|
|
20
|
-
-- PREP: addEmployee
|
|
21
|
-
INSERT INTO employees (name, position, salary)
|
|
22
|
-
VALUES ($name, $position, $salary);
|
|
23
|
-
|
|
24
|
-
-- PREP: getPositions
|
|
25
|
-
SELECT name, position FROM employees;
|
|
26
|
-
|
|
27
|
-
-- PREP: getHighestPaidEmployee
|
|
28
|
-
SELECT name FROM employees ORDER BY salary DESC LIMIT 1;
|
|
29
|
-
|
|
30
|
-
-- PREP: getMultiEmployees
|
|
31
|
-
SELECT name, position, salary FROM
|
|
32
|
-
employees WHERE name IN (SELECT value FROM json_each($names));
|
|
33
|
-
|
|
34
|
-
-- PREP: deleteEmployees
|
|
35
|
-
DELETE FROM employees WHERE name IN (SELECT value FROM json_each($names));
|