From db14c9e9451ab8c16efb628c5837e6e97f015380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andri=20M=C3=B6ll?= Date: Mon, 7 Oct 2024 23:51:53 +0000 Subject: [PATCH] Add support for Node v22.5's experimental SQLite. --- .github/workflows/node.yaml | 17 +++++++- CHANGELOG.md | 14 +++++++ Makefile | 4 ++ README.md | 42 +++++++++++++++++++- lib/sql.js | 3 +- node.js | 77 +++++++++++++++++++++++++++++++++++++ package.json | 4 +- test/_heaven_test.js | 8 ++-- test/node_test.js | 20 ++++++++++ 9 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 node.js create mode 100644 test/node_test.js diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index ab554a9..0a8cfb2 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -13,6 +13,7 @@ jobs: }} runs-on: ubuntu-latest + continue-on-error: ${{!!matrix.versions.flaky}} strategy: matrix: @@ -24,6 +25,9 @@ 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 >= 22. versions: - node: "8" mapbox-sqlite3: "4" @@ -31,6 +35,7 @@ jobs: - node: "8" better-sqlite3: "6" + flaky: true # Better SQLite v6 on Node v9 requires Python 2, which isn't # available on GitHub Actions. @@ -48,28 +53,36 @@ jobs: better-sqlite3: "7" - node: "14" + better-sqlite3: "10" - node: "15" + better-sqlite3: "6" - node: "15" - better-sqlite3: "6" + better-sqlite3: "10" - node: "16" + better-sqlite3: "10" - node: "17" + better-sqlite3: "10" - node: "18" better-sqlite3: "7" - node: "18" + better-sqlite3: "10" - node: "19" + better-sqlite3: "10" - node: "20" mapbox-sqlite3: "4" + better-sqlite3: "10" - - node: "20" - node: "21" + better-sqlite3: "10" + - node: "22" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d2398..72d7ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Makefile b/Makefile index 9aca214..c1b1120 100644 --- a/Makefile +++ b/Makefile @@ -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." diff --git a/README.md b/README.md index 4b971d9..0ed5ce7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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") @@ -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: diff --git a/lib/sql.js b/lib/sql.js index c17d6e1..db13a2e 100644 --- a/lib/sql.js +++ b/lib/sql.js @@ -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)) )))} `) } diff --git a/node.js b/node.js new file mode 100644 index 0000000..82c4220 --- /dev/null +++ b/node.js @@ -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 } diff --git a/package.json b/package.json index ec8c166..c34175a 100644 --- a/package.json +++ b/package.json @@ -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" : { diff --git a/test/_heaven_test.js b/test/_heaven_test.js index ce3e55b..389371c 100644 --- a/test/_heaven_test.js +++ b/test/_heaven_test.js @@ -359,17 +359,17 @@ 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}]) @@ -377,7 +377,7 @@ module.exports = function(SqliteHeaven, sqlite, execute, SQLITE_VERSION) { }) 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}) diff --git a/test/node_test.js b/test/node_test.js new file mode 100644 index 0000000..0832d55 --- /dev/null +++ b/test/node_test.js @@ -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) +})