Skip to content

Commit 41642b2

Browse files
committed
Add support for Node v22.5's experimental SQLite.
1 parent b266960 commit 41642b2

9 files changed

Lines changed: 181 additions & 18 deletions

File tree

.github/workflows/node.yaml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,29 @@ jobs:
1313
}}
1414
1515
runs-on: ubuntu-latest
16+
continue-on-error: ${{!!matrix.versions.flaky}}
1617

1718
strategy:
1819
matrix:
1920
# Mapbox SQLite3:
20-
# v4 works even on v20.
21+
# v4 works even on v22.
2122
# v5 depends on Node >= v10, but works on v8, too.
2223
#
2324
# Better SQLite3:
2425
# v6 depends on Node >= v8 and <= 15.
2526
# v7 depends on Node >= 10, <= 18.
26-
# v8 depends on Node >= 14.
27+
# v8 depends on Node >= 14, <= 21.
28+
# v9 depends on Node >= 14.
29+
# v10 depends on Node >= 14.
30+
# v11 depends on Node >= 14.
2731
versions:
2832
- node: "8"
2933
mapbox-sqlite3: "4"
3034
better-sqlite3: "6"
3135

3236
- node: "8"
3337
better-sqlite3: "6"
38+
flaky: true
3439

3540
# Better SQLite v6 on Node v9 requires Python 2, which isn't
3641
# available on GitHub Actions.
@@ -49,28 +54,32 @@ jobs:
4954

5055
- node: "14"
5156

52-
- node: "15"
53-
5457
- node: "15"
5558
better-sqlite3: "6"
5659

60+
- node: "15"
5761
- node: "16"
58-
5962
- node: "17"
6063

6164
- node: "18"
6265
better-sqlite3: "7"
6366

6467
- node: "18"
65-
6668
- node: "19"
67-
6869
- node: "20"
69-
mapbox-sqlite3: "4"
70+
- node: "21"
7071

71-
- node: "20"
7272
- node: "21"
73+
better-sqlite3: "8"
74+
75+
- node: "22"
76+
7377
- node: "22"
78+
better-sqlite3: "10"
79+
80+
- node: "22"
81+
better-sqlite3: "9"
82+
mapbox-sqlite3: "4"
7483

7584
steps:
7685
- uses: actions/checkout@v3

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
- Refactors SQLite version detection for [Better SQLite3][better-sqlite3] to be per-SQLite connection, permitting parallel use of different versions.
33
This also permits using the Better SQLite3 Heaven adapter with different SQLite modules.
44

5+
- Adds support for [Node v22.5's experimental SQLite module][node-sqlite3].
6+
7+
```js
8+
var SqliteHeaven = require("heaven-sqlite")
9+
var Sqlite = require("node:sqlite").DatabaseSync
10+
var sqlite = new Sqlite(":memory:")
11+
var heaven = new SqliteHeaven(sqlite, "models")
12+
heaven.read(42)
13+
```
14+
15+
Note that Node v22.5 itself is buggy due to its [SQLite bindings returning empty rows if no rows were actually returned](https://github.com/nodejs/node/pull/53981). This was [fixed on Jul 23, 2024](https://github.com/nodejs/node/commit/db594d042bbde2cb37a2db11f5c284772a26a8e4) and released as [Node v22.6](https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V22.md#2024-08-06-version-2260-current-rafaelgss).
16+
17+
[node-sqlite3]: https://nodejs.org/api/sqlite.html
18+
519
## 2.0.0 (Jul 19, 2023)
620
- Adds support for [Mapbox's/Ghost's SQLite3][mapbox-sqlite3] v5.
721
- Adds support for [Joshua Wise's Better SQLite3][better-sqlite3] v8.

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ MOCHA = ./node_modules/.bin/_mocha
55
TEST = test/**/*_test.js
66
NPM_REBUILD = $(NPM) --ignore-scripts false rebuild --build-from-source
77

8+
ifneq ($(filter v22.%, $(shell node -v)),)
9+
NODE_OPTS += --experimental-sqlite
10+
endif
11+
812
love:
913
@echo "Feel like makin' love."
1014

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Heaven.js for SQLite
33
[![NPM version][npm-badge]](https://www.npmjs.com/package/heaven-sqlite)
44
[![Build status][build-badge]](https://github.com/moll/node-heaven-sqlite/actions/workflows/node.yaml)
55

6-
**Heaven.js for SQLite** is a JavaScript library for Node.js that gives you a [CRUD][crud] API for your SQLite database by implementing a [Data Mapper][data-mapper] or [Table Data Gateway][table-data-gateway] object. It's built on [Heaven.js][heaven] and comes with adapters for [Mapbox's SQLite3][mapbox-sqlite3] and [Joshua Wise's Better SQLite3][better-sqlite3]. It'll also work with other SQLite libraries that have compatible APIs. Along with [Sqlate.js][sqlate]'s tagged template strings, this permits convenient SQL queries that get parsed to your models and equally easy creation and updating.
6+
**Heaven.js for SQLite** is a JavaScript library for Node.js that gives you a [CRUD][crud] API for your SQLite database by implementing a [Data Mapper][data-mapper] or [Table Data Gateway][table-data-gateway] object. It's built on [Heaven.js][heaven] and comes with adapters for [Mapbox's SQLite3][mapbox-sqlite3], [Joshua Wise's Better SQLite3][better-sqlite3] and [Node v22.5's experimental SQLite module][node-sqlite3]. It'll also work with other SQLite libraries that have compatible APIs. Along with [Sqlate.js][sqlate]'s tagged template strings, this permits convenient SQL queries that get parsed to your models and equally easy creation and updating.
77

88
[npm-badge]: https://img.shields.io/npm/v/heaven-sqlite.svg
99
[build-badge]: https://github.com/moll/node-heaven-sqlite/actions/workflows/node.yaml/badge.svg
@@ -12,6 +12,7 @@ Heaven.js for SQLite
1212
[sqlate]: https://github.com/moll/js-sqlate
1313
[mapbox-sqlite3]: https://github.com/mapbox/node-sqlite3
1414
[better-sqlite3]: https://github.com/JoshuaWise/better-sqlite3
15+
[node-sqlite3]: https://nodejs.org/api/sqlite.html
1516
[heaven]: https://github.com/moll/js-heaven
1617
[crud]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete
1718

@@ -70,7 +71,7 @@ Instantiate `SqliteHeaven` by passing it the constructor for your model, an SQLi
7071
```javascript
7172
var SqliteHeaven = require("heaven-sqlite/better")
7273
var Sqlite3 = require("better-sqlite3")
73-
var sqlite = new Sqlite3.Database(":memory:", {memory: true})
74+
var sqlite = new Sqlite3.Database(":memory:")
7475

7576
function Model(attrs) { Object.assign(this, attrs)}
7677
var modelsDb = new SqliteHeaven(Model, sqlite, "models")
@@ -100,6 +101,43 @@ modelsDb.delete(model)
100101
```
101102

102103

104+
Using with Node v22's experimental SQLite
105+
-----------------------------------------
106+
Instantiate `SqliteHeaven` by passing it the constructor for your model, an SQLite connection and a table name:
107+
108+
```javascript
109+
var SqliteHeaven = require("heaven-sqlite")
110+
var Sqlite = require("node:sqlite").DatabaseSync
111+
var sqlite = new Sqlite(":memory:")
112+
113+
function Model(attrs) { Object.assign(this, attrs)}
114+
var modelsDb = new SqliteHeaven(Model, sqlite, "models")
115+
```
116+
117+
Suppose the "models" table looks like this:
118+
```sql
119+
CREATE TABLE "models" (
120+
"id" INTEGER PRIMARY KEY NOT NULL,
121+
"name" TEXT DEFAULT '',
122+
"age" INTEGER DEFAULT 0
123+
)
124+
```
125+
126+
You can then call the five [CRUD][crud] methods like described in [Heaven.js's README][heaven] synchronously:
127+
128+
```javascript
129+
var sql = require("sqlate")
130+
131+
var john = modelsDb.create({name: "John", age: 13})
132+
var mike = modelsDb.create({name: "Mike", age: 42})
133+
134+
modelsDb.search(sql`SELECT * FROM models WHERE age < 15`)
135+
modelsDb.read(john.id)
136+
modelsDb.update(model, {age: 42})
137+
modelsDb.delete(model)
138+
```
139+
140+
103141
License
104142
-------
105143
Heaven.js for SQLite is released under a *Lesser GNU Affero General Public License*, which in summary means:

lib/sql.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ exports.insertAllWithMaxVariables = function(maxVariableCount, table, attrs) {
2828
var chunkSize = Math.floor(maxVariableCount / columns.size)
2929
return _.chunk(chunkSize, attrs).map((chunk) => sql`
3030
${insert} VALUES ${sql.csv(chunk.map((attrs) => (
31-
sql.tuple(Array.from(columns, (col) => attrs[col]))
31+
// Node v22.5's embedded SQLite module throws given `undefined`.
32+
sql.tuple(Array.from(columns, (col) => col in attrs ? attrs[col] : null))
3233
)))}
3334
`)
3435
}

node.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
var Heaven = require("heaven/sync")
2+
var SqliteHeaven = require("./prototype")
3+
var Sql = require("sqlate").Sql
4+
var sql = require("sqlate")
5+
var insert = require("./lib/sql").insert
6+
var insertAll = require("./lib/sql").insertAll
7+
var insertAllWithMaxVariables = require("./lib/sql").insertAllWithMaxVariables
8+
var update = require("./lib/sql").update
9+
var MAX_VARIABLE_NUMBER = 32766
10+
exports = module.exports = NodeSqliteHeaven
11+
exports.insert = insert
12+
exports.insertAll = insertAll
13+
exports.insertAllWithMaxVariables = insertAllWithMaxVariables
14+
exports.update = update
15+
16+
function NodeSqliteHeaven(model, sqlite, table) {
17+
Heaven.call(this, model)
18+
this.sqlite = sqlite
19+
this.table = table
20+
}
21+
22+
NodeSqliteHeaven.prototype = Object.create(Heaven.prototype, {
23+
constructor: {value: NodeSqliteHeaven, configurable: true, writeable: true}
24+
})
25+
26+
NodeSqliteHeaven.prototype.idColumn = SqliteHeaven.idColumn
27+
NodeSqliteHeaven.prototype.with = SqliteHeaven.with
28+
NodeSqliteHeaven.prototype._search = SqliteHeaven._search
29+
NodeSqliteHeaven.prototype._read = SqliteHeaven._read
30+
NodeSqliteHeaven.prototype.create_ = SqliteHeaven.create_
31+
NodeSqliteHeaven.prototype.typeof = SqliteHeaven.typeof
32+
33+
// NOTE: Node.js SQLite returned rows are not inheriting from Object.prototype.
34+
NodeSqliteHeaven.prototype._create = function(attrs) {
35+
// Note v22.5 launched with SQLite v3.46.
36+
return insertAllWithMaxVariables(
37+
MAX_VARIABLE_NUMBER,
38+
this.table,
39+
attrs
40+
).flatMap((q) => this.select(sql`${q} RETURNING *`))
41+
}
42+
43+
NodeSqliteHeaven.prototype._create_ = function(attrs) {
44+
insertAllWithMaxVariables(
45+
MAX_VARIABLE_NUMBER,
46+
this.table,
47+
attrs
48+
).forEach(this.execute, this)
49+
}
50+
51+
NodeSqliteHeaven.prototype._update = function(query, attrs) {
52+
SqliteHeaven._update.call(this, query, attrs)
53+
}
54+
55+
NodeSqliteHeaven.prototype._delete = function(query, attrs) {
56+
SqliteHeaven._delete.call(this, query, attrs)
57+
}
58+
59+
NodeSqliteHeaven.prototype.select = function(sql) {
60+
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
61+
var statement = this.sqlite.prepare(String(sql))
62+
return statement.all.apply(statement, sql.parameters)
63+
}
64+
65+
NodeSqliteHeaven.prototype.select1 = function(sql) {
66+
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
67+
var statement = this.sqlite.prepare(String(sql))
68+
return statement.get.apply(statement, sql.parameters)
69+
}
70+
71+
NodeSqliteHeaven.prototype.execute = function(sql) {
72+
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
73+
var statement = this.sqlite.prepare(String(sql))
74+
return statement.run.apply(statement, sql.parameters)
75+
}
76+
77+
NodeSqliteHeaven.prototype.return = function(value) { return value }

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@
3939

4040
"peerDependencies": {
4141
"sqlite3": ">= 4 < 6",
42-
"better-sqlite3": ">= 6 < 9"
42+
"better-sqlite3": ">= 6 < 12"
4343
},
4444

4545
"devDependencies": {
4646
"mocha": ">= 2 < 4",
4747
"must": ">= 0.13.0 < 0.14",
4848
"sqlite3": ">= 4 < 6",
49-
"better-sqlite3": ">= 6 < 9"
49+
"better-sqlite3": ">= 6 < 12"
5050
},
5151

5252
"engines" : {

test/_heaven_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,25 +359,25 @@ module.exports = function(SqliteHeaven, sqlite, execute, SQLITE_VERSION) {
359359

360360
describe("given attributes", function() {
361361
it("must create model", async function() {
362-
var model = await create().create({name: "John", age: 13})
362+
var model = _.clone(await create().create({name: "John", age: 13}))
363363
var rows = await execute(sql`SELECT * FROM models`)
364364
rows.must.eql([{id: 1, name: "John", age: 13}])
365365
model.must.eql({id: 1, name: "John", age: 13})
366366
})
367367

368368
it("must create model given inherited attributes", async function() {
369-
var model = await create().create(Object.create({
369+
var model = _.clone(await create().create(Object.create({
370370
name: "John",
371371
age: 13
372-
}))
372+
})))
373373

374374
var rows = await execute(sql`SELECT * FROM models`)
375375
rows.must.eql([{id: 1, name: "John", age: 13}])
376376
model.must.eql({id: 1, name: "John", age: 13})
377377
})
378378

379379
it("must create model given empty attributes", async function() {
380-
var model = await create().create({})
380+
var model = _.clone(await create().create({}))
381381
var rows = await execute(sql`SELECT * FROM models`)
382382
rows.must.eql([{id: 1, name: "", age: 0}])
383383
model.must.eql({id: 1, name: "", age: 0})

test/node_test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var _ = require("../lib")
2+
var Sql = require("sqlate").Sql
3+
var SqliteHeaven = require("../node")
4+
var NODE_VERSION = process.version.replace(/^v/, "")
5+
6+
if (!_.isVersionGt(NODE_VERSION, "22.5")) xdescribe("NodeSqliteHeaven")
7+
else describe("NodeSqliteHeaven", function() {
8+
var Sqlite = require("node:sqlite").DatabaseSync
9+
var sqlite = new Sqlite(":memory:")
10+
var SQLITE_VERSION = sqlite.prepare("SELECT sqlite_version() AS v").get().v
11+
12+
function execute(sql) {
13+
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
14+
15+
var statement = sqlite.prepare(String(sql))
16+
return statement.all.apply(statement, sql.parameters)
17+
}
18+
19+
require("./_heaven_test")(SqliteHeaven, sqlite, execute, SQLITE_VERSION)
20+
})

0 commit comments

Comments
 (0)