diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e09f33fa6..90e17fc2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,3 +45,7 @@ jobs: FORCE_COLOR: true TAG: ${{ steps.hxe.outputs.TAG }} IMAGE_ID: ${{ steps.hxe.outputs.IMAGE_ID }} + - run: npm install sql.js && rm -rf node_modules/better-sqlite3/ && npm test -w sqlite + env: + cds_features_pool: true + FORCE_COLOR: true diff --git a/package-lock.json b/package-lock.json index 929d3e6cf..24b71a4b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ }, "db-service": { "name": "@cap-js/db-service", - "version": "2.3.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "generic-pool": "^3.9.0" @@ -32,7 +32,7 @@ }, "hana": { "name": "@cap-js/hana", - "version": "2.1.2", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "@cap-js/db-service": "^2.1.1", @@ -1739,6 +1739,13 @@ "node": ">= 10.x" } }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT", + "optional": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1894,7 +1901,7 @@ }, "postgres": { "name": "@cap-js/postgres", - "version": "2.0.4", + "version": "2.0.5", "license": "Apache-2.0", "dependencies": { "@cap-js/db-service": "^2", @@ -1918,6 +1925,9 @@ "@cap-js/db-service": "^2", "better-sqlite3": "^12.0.0" }, + "optionalDependencies": { + "sql.js": "^1.13.0" + }, "peerDependencies": { "@sap/cds": ">=9" } diff --git a/sqlite/lib/SQLiteService.js b/sqlite/lib/SQLiteService.js index 90d966101..9b2f84105 100644 --- a/sqlite/lib/SQLiteService.js +++ b/sqlite/lib/SQLiteService.js @@ -1,6 +1,13 @@ const { SQLService } = require('@cap-js/db-service') -const cds = require('@sap/cds') -const sqlite = require('better-sqlite3') +const cds = require('@sap/cds/lib') +let sqlite +try { + sqlite = require('better-sqlite3') +} catch (err) { + // When failing to load better-sqlite3 it fallsback to sql.js (wasm version of sqlite) + sqlite = require('./sql.js.js') +} + const $session = Symbol('dbc.session') const sessionVariableMap = require('./session.json') // Adjust the path as necessary for your project const convStrm = require('stream/consumers') @@ -28,9 +35,11 @@ class SQLiteService extends SQLService { get factory() { return { options: { max: 1, ...this.options.pool }, - create: tenant => { + create: async tenant => { const database = this.url4(tenant) const dbc = new sqlite(database, this.options.client) + await dbc.ready + const deterministic = { deterministic: true } dbc.function('session_context', key => dbc[$session][key]) dbc.function('regexp', deterministic, (re, x) => (RegExp(re).test(x) ? 1 : 0)) @@ -134,10 +143,8 @@ class SQLiteService extends SQLService { } async _allStream(stmt, binding_params, one, objectMode) { - stmt = stmt.constructor.name === 'Statement' ? stmt : stmt.__proto__ - stmt.raw(true) - const get = stmt.get(binding_params) - if (!get) return [] + stmt = stmt.iterate ? stmt : stmt.__proto__ + stmt.raw?.(true) const rs = stmt.iterate(binding_params) const stream = Readable.from(objectMode ? this._iteratorObjectMode(rs) : this._iteratorRaw(rs, one), { objectMode }) const close = () => rs.return() // finish result set when closed early diff --git a/sqlite/lib/sql.js.js b/sqlite/lib/sql.js.js new file mode 100644 index 000000000..8c9be320d --- /dev/null +++ b/sqlite/lib/sql.js.js @@ -0,0 +1,90 @@ +const initSqlJs = require('sql.js'); + +const init = initSqlJs({}) + +class WasmSqlite { + constructor(/*database*/) { + // TODO: load / store database file contents + this.ready = init + .then(SQL => { this.db = new SQL.Database() }) + + this.memory = true + this.gc = new FinalizationRegistry(stmt => { stmt.free() }) + } + + prepare(sql) { + const stmt = this.db.prepare(sql) + const ret = { + run(params) { + try { + stmt.bind(params) + stmt.step() + return { changes: stmt.db.getRowsModified(stmt) } + } catch (err) { + if (err.message.indexOf('NOT NULL constraint failed:') === 0) { + err.code = 'SQLITE_CONSTRAINT_NOTNULL' + } + throw err + } + }, + get(params) { + const columns = stmt.getColumnNames() + stmt.bind(params) + stmt.step() + const row = stmt.get() + const ret = {} + for (let i = 0; i < columns.length; i++) { + ret[columns[i]] = row[i] + } + return ret + }, + all(params) { + const columns = stmt.getColumnNames() + const ret = [] + stmt.bind(params) + while (stmt.step()) { + const row = stmt.get() + const obj = {} + for (let i = 0; i < columns.length; i++) { + obj[columns[i]] = row[i] + } + ret.push(obj) + } + return ret + }, + *iterate(params) { + stmt.bind(params) + while (stmt.step()) { + yield stmt.get() + } + } + } + this.gc.register(ret, stmt) + return ret + } + + exec(sql) { + try { + const { columns, values } = this.db.exec(sql) + return !Array.isArray(values) ? values : values.map(val => { + const ret = {} + for (let i = 0; i < columns.length; i++) { + ret[columns[i]] = val[i] + } + return ret + }) + } catch (err) { + // REVISIT: address transaction errors + if (sql === 'BEGIN' || sql === 'ROLLBACK') { return } + throw err + } + } + + function(name, config, func) { + this.db.create_function(name, func || config) + } + + close() { this.db.close() } +} + +module.exports = WasmSqlite diff --git a/sqlite/package.json b/sqlite/package.json index 4d090b418..c132dc7d6 100644 --- a/sqlite/package.json +++ b/sqlite/package.json @@ -30,7 +30,13 @@ "better-sqlite3": "^12.0.0" }, "peerDependencies": { - "@sap/cds": ">=9" + "@sap/cds": ">=9", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } }, "cds": { "requires": {