Skip to content

Commit

Permalink
Add support for Node v22.5's experimental SQLite.
Browse files Browse the repository at this point in the history
  • Loading branch information
moll committed Oct 8, 2024
1 parent b266960 commit 25d773d
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 11 deletions.
24 changes: 22 additions & 2 deletions .github/workflows/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
}}
runs-on: ubuntu-latest
continue-on-error: ${{!!matrix.versions.flaky}}

strategy:
matrix:
Expand All @@ -24,13 +25,17 @@ jobs:
# v6 depends on Node >= v8 and <= 15.
# v7 depends on Node >= 10, <= 18.
# v8 depends on Node >= 14.
# v9 depends on Node >= 14.
# v10 depends on Node >= 14.
# v11 depends on Node >= 14.
versions:
- node: "8"
mapbox-sqlite3: "4"
better-sqlite3: "6"

- node: "8"
better-sqlite3: "6"
flaky: true

# Better SQLite v6 on Node v9 requires Python 2, which isn't
# available on GitHub Actions.
Expand All @@ -50,9 +55,9 @@ jobs:
- node: "14"

- node: "15"
better-sqlite3: "6"

- node: "15"
better-sqlite3: "6"

- node: "16"

Expand All @@ -68,9 +73,24 @@ jobs:
- node: "20"
mapbox-sqlite3: "4"

- node: "20"
- node: "21"

- node: "21"
mapbox-sqlite3: "4"

- node: "22"

- node: "22"
better-sqlite3: "10"

- node: "22"
better-sqlite3: "9"

- node: "22"
better-sqlite3: "8"

- node: "22"
mapbox-sqlite3: "4"

steps:
- uses: actions/checkout@v3
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
- Refactors SQLite version detection for [Better SQLite3][better-sqlite3] to be per-SQLite connection, permitting parallel use of different versions.
This also permits using the Better SQLite3 Heaven adapter with different SQLite modules.

- Adds support for [Node v22.5's experimental SQLite module][node-sqlite3].

```js
var SqliteHeaven = require("heaven-sqlite")
var Sqlite = require("node:sqlite").DatabaseSync
var sqlite = new Sqlite(":memory:")
var heaven = new SqliteHeaven(sqlite, "models")
heaven.read(42)
```

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).

[node-sqlite3]: https://nodejs.org/api/sqlite.html

## 2.0.0 (Jul 19, 2023)
- Adds support for [Mapbox's/Ghost's SQLite3][mapbox-sqlite3] v5.
- Adds support for [Joshua Wise's Better SQLite3][better-sqlite3] v8.
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ MOCHA = ./node_modules/.bin/_mocha
TEST = test/**/*_test.js
NPM_REBUILD = $(NPM) --ignore-scripts false rebuild --build-from-source

ifneq ($(filter v22.%, $(shell node -v)),)
NODE_OPTS += --experimental-sqlite
endif

love:
@echo "Feel like makin' love."

Expand Down
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Heaven.js for SQLite
[![NPM version][npm-badge]](https://www.npmjs.com/package/heaven-sqlite)
[![Build status][build-badge]](https://github.com/moll/node-heaven-sqlite/actions/workflows/node.yaml)

**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.
**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.

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

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

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


Using with Node v22's experimental SQLite
-----------------------------------------
Instantiate `SqliteHeaven` by passing it the constructor for your model, an SQLite connection and a table name:

```javascript
var SqliteHeaven = require("heaven-sqlite")
var Sqlite = require("node:sqlite").DatabaseSync
var sqlite = new Sqlite(":memory:")

function Model(attrs) { Object.assign(this, attrs)}
var modelsDb = new SqliteHeaven(Model, sqlite, "models")
```

Suppose the "models" table looks like this:
```sql
CREATE TABLE "models" (
"id" INTEGER PRIMARY KEY NOT NULL,
"name" TEXT DEFAULT '',
"age" INTEGER DEFAULT 0
)
```

You can then call the five [CRUD][crud] methods like described in [Heaven.js's README][heaven] synchronously:

```javascript
var sql = require("sqlate")

var john = modelsDb.create({name: "John", age: 13})
var mike = modelsDb.create({name: "Mike", age: 42})

modelsDb.search(sql`SELECT * FROM models WHERE age < 15`)
modelsDb.read(john.id)
modelsDb.update(model, {age: 42})
modelsDb.delete(model)
```


License
-------
Heaven.js for SQLite is released under a *Lesser GNU Affero General Public License*, which in summary means:
Expand Down
3 changes: 2 additions & 1 deletion lib/sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ exports.insertAllWithMaxVariables = function(maxVariableCount, table, attrs) {
var chunkSize = Math.floor(maxVariableCount / columns.size)
return _.chunk(chunkSize, attrs).map((chunk) => sql`
${insert} VALUES ${sql.csv(chunk.map((attrs) => (
sql.tuple(Array.from(columns, (col) => attrs[col]))
// Node v22.5's embedded SQLite module throws given `undefined`.
sql.tuple(Array.from(columns, (col) => col in attrs ? attrs[col] : null))
)))}
`)
}
Expand Down
77 changes: 77 additions & 0 deletions node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
var Heaven = require("heaven/sync")
var SqliteHeaven = require("./prototype")
var Sql = require("sqlate").Sql
var sql = require("sqlate")
var insert = require("./lib/sql").insert
var insertAll = require("./lib/sql").insertAll
var insertAllWithMaxVariables = require("./lib/sql").insertAllWithMaxVariables
var update = require("./lib/sql").update
var MAX_VARIABLE_NUMBER = 32766
exports = module.exports = NodeSqliteHeaven
exports.insert = insert
exports.insertAll = insertAll
exports.insertAllWithMaxVariables = insertAllWithMaxVariables
exports.update = update

function NodeSqliteHeaven(model, sqlite, table) {
Heaven.call(this, model)
this.sqlite = sqlite
this.table = table
}

NodeSqliteHeaven.prototype = Object.create(Heaven.prototype, {
constructor: {value: NodeSqliteHeaven, configurable: true, writeable: true}
})

NodeSqliteHeaven.prototype.idColumn = SqliteHeaven.idColumn
NodeSqliteHeaven.prototype.with = SqliteHeaven.with
NodeSqliteHeaven.prototype._search = SqliteHeaven._search
NodeSqliteHeaven.prototype._read = SqliteHeaven._read
NodeSqliteHeaven.prototype.create_ = SqliteHeaven.create_
NodeSqliteHeaven.prototype.typeof = SqliteHeaven.typeof

// NOTE: Node.js SQLite returned rows are not inheriting from Object.prototype.
NodeSqliteHeaven.prototype._create = function(attrs) {
// Note v22.5 launched with SQLite v3.46.
return insertAllWithMaxVariables(
MAX_VARIABLE_NUMBER,
this.table,
attrs
).flatMap((q) => this.select(sql`${q} RETURNING *`))
}

NodeSqliteHeaven.prototype._create_ = function(attrs) {
insertAllWithMaxVariables(
MAX_VARIABLE_NUMBER,
this.table,
attrs
).forEach(this.execute, this)
}

NodeSqliteHeaven.prototype._update = function(query, attrs) {
SqliteHeaven._update.call(this, query, attrs)
}

NodeSqliteHeaven.prototype._delete = function(query, attrs) {
SqliteHeaven._delete.call(this, query, attrs)
}

NodeSqliteHeaven.prototype.select = function(sql) {
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
var statement = this.sqlite.prepare(String(sql))
return statement.all.apply(statement, sql.parameters)
}

NodeSqliteHeaven.prototype.select1 = function(sql) {
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
var statement = this.sqlite.prepare(String(sql))
return statement.get.apply(statement, sql.parameters)
}

NodeSqliteHeaven.prototype.execute = function(sql) {
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)
var statement = this.sqlite.prepare(String(sql))
return statement.run.apply(statement, sql.parameters)
}

NodeSqliteHeaven.prototype.return = function(value) { return value }
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@

"peerDependencies": {
"sqlite3": ">= 4 < 6",
"better-sqlite3": ">= 6 < 9"
"better-sqlite3": ">= 6 < 12"
},

"devDependencies": {
"mocha": ">= 2 < 4",
"must": ">= 0.13.0 < 0.14",
"sqlite3": ">= 4 < 6",
"better-sqlite3": ">= 6 < 9"
"better-sqlite3": ">= 6 < 12"
},

"engines" : {
Expand Down
8 changes: 4 additions & 4 deletions test/_heaven_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,25 +359,25 @@ module.exports = function(SqliteHeaven, sqlite, execute, SQLITE_VERSION) {

describe("given attributes", function() {
it("must create model", async function() {
var model = await create().create({name: "John", age: 13})
var model = _.clone(await create().create({name: "John", age: 13}))
var rows = await execute(sql`SELECT * FROM models`)
rows.must.eql([{id: 1, name: "John", age: 13}])
model.must.eql({id: 1, name: "John", age: 13})
})

it("must create model given inherited attributes", async function() {
var model = await create().create(Object.create({
var model = _.clone(await create().create(Object.create({
name: "John",
age: 13
}))
})))

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

it("must create model given empty attributes", async function() {
var model = await create().create({})
var model = _.clone(await create().create({}))
var rows = await execute(sql`SELECT * FROM models`)
rows.must.eql([{id: 1, name: "", age: 0}])
model.must.eql({id: 1, name: "", age: 0})
Expand Down
20 changes: 20 additions & 0 deletions test/node_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var _ = require("../lib")
var Sql = require("sqlate").Sql
var SqliteHeaven = require("../node")
var NODE_VERSION = process.version.replace(/^v/, "")

if (!_.isVersionGt(NODE_VERSION, "22.5")) xdescribe("NodeSqliteHeaven")
else describe("NodeSqliteHeaven", function() {
var Sqlite = require("node:sqlite").DatabaseSync
var sqlite = new Sqlite(":memory:")
var SQLITE_VERSION = sqlite.prepare("SELECT sqlite_version() AS v").get().v

function execute(sql) {
if (!(sql instanceof Sql)) throw new TypeError("Not Sql: " + sql)

var statement = sqlite.prepare(String(sql))
return statement.all.apply(statement, sql.parameters)
}

require("./_heaven_test")(SqliteHeaven, sqlite, execute, SQLITE_VERSION)
})

0 comments on commit 25d773d

Please sign in to comment.