Skip to content

Commit 76dd3df

Browse files
committed
feat: initial release of @photostructure/knex-sqlite
Knex.js dialect that uses @photostructure/sqlite instead of better-sqlite3. Extends Client_BetterSQLite3 with driver loading, .reader property shim, binding format adaptation, and setReadBigInts/safeIntegers bridging.
0 parents  commit 76dd3df

File tree

11 files changed

+1043
-0
lines changed

11 files changed

+1043
-0
lines changed

.github/workflows/build.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Build & Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: "Version type (patch, minor, major, or specific version)"
12+
required: false
13+
type: string
14+
15+
jobs:
16+
build:
17+
runs-on: ${{ matrix.os }}
18+
19+
strategy:
20+
matrix:
21+
os: [ubuntu-latest, macos-latest, windows-latest]
22+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
23+
node-version: [20, 22, 24, 25]
24+
25+
steps:
26+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
27+
- name: Use Node.js ${{ matrix.node-version }}
28+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
29+
with:
30+
node-version: ${{ matrix.node-version }}
31+
# Replace local file: devDependency with npm registry version for CI
32+
- name: Install dependencies
33+
run: |
34+
npm pkg set "devDependencies.@photostructure/sqlite=>=0.5.0"
35+
npm install
36+
- run: npm test
37+
38+
publish:
39+
runs-on: ubuntu-24.04
40+
needs: [build]
41+
if: ${{ github.event_name == 'workflow_dispatch' }}
42+
permissions:
43+
id-token: write # Required for OIDC trusted publishing
44+
contents: write # Required to push tags and create releases
45+
steps:
46+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
47+
with:
48+
fetch-depth: 0 # Need full history for version tags
49+
lfs: true
50+
51+
# setup-node with registry-url is required for OIDC trusted publishing
52+
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
53+
with:
54+
node-version: 20
55+
cache: "npm"
56+
registry-url: "https://registry.npmjs.org"
57+
58+
- name: Set up SSH signing
59+
uses: photostructure/git-ssh-signing-action@fdd4b062a9ba41473f013258cc9c7eea1640f826 # v1.2.0
60+
with:
61+
ssh-signing-key: ${{ secrets.SSH_SIGNING_KEY }}
62+
git-user-name: ${{ secrets.GIT_USER_NAME }}
63+
git-user-email: ${{ secrets.GIT_USER_EMAIL }}
64+
65+
- name: Update npm to latest to get --provenance support
66+
run: |
67+
npm install -g npm@latest
68+
npm --version
69+
70+
- name: Install dependencies
71+
run: |
72+
npm pkg set "devDependencies.@photostructure/sqlite=>=0.5.0"
73+
npm install
74+
75+
- name: Run tests before publishing
76+
run: npm test
77+
78+
- name: Restore package.json before version bump
79+
run: git checkout -- package.json package-lock.json
80+
81+
- name: Bump version and create tag
82+
run: |
83+
VERSION_ARG="${{ github.event.inputs.version }}"
84+
if [ -z "$VERSION_ARG" ]; then
85+
VERSION_ARG="patch"
86+
fi
87+
npm version "$VERSION_ARG" --sign-git-tag -m "chore(release): %s"
88+
echo "NEW_VERSION=$(npm pkg get version | tr -d '\"')" >> $GITHUB_ENV
89+
90+
- name: Push changes and tags
91+
run: |
92+
git push origin main
93+
git push origin --tags
94+
95+
- name: Create GitHub Release
96+
run: gh release create "v${{ env.NEW_VERSION }}" --generate-notes
97+
env:
98+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
99+
100+
- name: Publish to npm with OIDC
101+
run: npm publish --provenance

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
*.log
3+
*.db
4+
coverage/
5+
.DS_Store

.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
test.js
2+
*.db
3+
.gitignore
4+
.npmrc
5+
.npmignore

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Security hardening: Disable lifecycle scripts by default to prevent supply chain attacks
2+
ignore-scripts=true

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2026-02-09
9+
10+
### Added
11+
12+
- Initial release
13+
- Knex.js dialect extending `Client_BetterSQLite3` to use `@photostructure/sqlite`
14+
- `.reader` property shim via `stmt.columns().length > 0` (handles `RETURNING` clauses)
15+
- Binding format adaptation (variadic args instead of array)
16+
- `setReadBigInts()` / `safeIntegers()` bridging
17+
- CI workflow testing Node.js 20/22/24/25 on Linux/macOS/Windows
18+
- OIDC-based npm publishing via workflow dispatch
19+
20+
[0.1.0]: https://github.com/photostructure/knex-sqlite/releases/tag/v0.1.0

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 PhotoStructure Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# @photostructure/knex-sqlite
2+
3+
[![npm version](https://img.shields.io/npm/v/@photostructure/knex-sqlite.svg)](https://www.npmjs.com/package/@photostructure/knex-sqlite)
4+
[![license](https://img.shields.io/npm/l/@photostructure/knex-sqlite.svg)](https://github.com/photostructure/knex-sqlite/blob/main/LICENSE)
5+
6+
[Knex.js](https://knexjs.org/) dialect for
7+
[@photostructure/sqlite](https://github.com/photostructure/node-sqlite).
8+
9+
Uses `@photostructure/sqlite` as the SQLite driver instead of `better-sqlite3`
10+
or `sqlite3` -- no Python, no build tools, just pre-built binaries that work out
11+
of the box.
12+
13+
## Installation
14+
15+
```bash
16+
npm install @photostructure/knex-sqlite @photostructure/sqlite knex
17+
```
18+
19+
## Usage
20+
21+
```javascript
22+
const knex = require("knex");
23+
const Client = require("@photostructure/knex-sqlite");
24+
25+
const db = knex({
26+
client: Client,
27+
connection: {
28+
filename: "./mydb.sqlite",
29+
},
30+
useNullAsDefault: true,
31+
});
32+
```
33+
34+
### Connection options
35+
36+
```javascript
37+
const db = knex({
38+
client: Client,
39+
connection: {
40+
filename: "./mydb.sqlite",
41+
options: {
42+
readonly: false, // open as read-only
43+
safeIntegers: false, // return BigInt for large integers
44+
},
45+
},
46+
useNullAsDefault: true,
47+
});
48+
```
49+
50+
### All Knex features work
51+
52+
```javascript
53+
// Schema
54+
await db.schema.createTable("users", (table) => {
55+
table.increments("id");
56+
table.string("name");
57+
table.integer("age");
58+
});
59+
60+
// Queries
61+
const users = await db("users").select("*");
62+
await db("users").insert({ name: "Alice", age: 30 });
63+
await db("users").where("age", ">", 25).update({ active: true });
64+
65+
// Transactions
66+
await db.transaction(async (trx) => {
67+
await trx("users").insert({ name: "Bob", age: 25 });
68+
await trx("posts").insert({ user_id: 1, title: "Hello" });
69+
});
70+
71+
// Joins
72+
const results = await db("users")
73+
.join("posts", "users.id", "posts.user_id")
74+
.select("users.name", "posts.title");
75+
76+
// Raw queries
77+
const stats = await db.raw("SELECT COUNT(*) as count FROM users");
78+
```
79+
80+
## How it works
81+
82+
This package extends Knex's built-in `Client_BetterSQLite3` class and adapts
83+
three things:
84+
85+
1. **Driver**: loads `@photostructure/sqlite` and calls `enhance()` to add
86+
better-sqlite3-style convenience methods (`.pragma()`, `.transaction()`,
87+
`.pluck()`, `.raw()`, `.expand()`)
88+
89+
2. **The `.reader` property**: better-sqlite3 exposes `.reader` on prepared
90+
statements so Knex knows whether to call `.all()` (SELECT) or `.run()`
91+
(INSERT/UPDATE/DELETE). Since `@photostructure/sqlite` doesn't provide this,
92+
the dialect adds it via `stmt.columns().length > 0`, which correctly handles
93+
`RETURNING` clauses too.
94+
95+
3. **Binding format and BigInt**: better-sqlite3 takes bindings as a single
96+
array; `@photostructure/sqlite` takes variadic arguments. And
97+
`safeIntegers()` becomes `setReadBigInts()`.
98+
99+
| better-sqlite3 | @photostructure/sqlite |
100+
| --------------------------- | ---------------------------- |
101+
| `statement.safeIntegers()` | `statement.setReadBigInts()` |
102+
| `statement.all(bindings)` | `statement.all(...bindings)` |
103+
| `statement.run(bindings)` | `statement.run(...bindings)` |
104+
105+
## Requirements
106+
107+
- Node.js >= 20.0.0
108+
- `knex` >= 3.0.0
109+
- `@photostructure/sqlite` >= 0.5.0
110+
111+
## License
112+
113+
MIT

index.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @module @photostructure/knex-sqlite
3+
*
4+
* Knex.js dialect for @photostructure/sqlite. Extends the built-in
5+
* better-sqlite3 client, adapting the connection and query layer to use
6+
* @photostructure/sqlite's DatabaseSync driver.
7+
*
8+
* @see https://github.com/photostructure/knex-sqlite
9+
* @see https://github.com/photostructure/node-sqlite
10+
* @license MIT
11+
*/
12+
13+
const Client_BetterSQLite3 = require('knex/lib/dialects/better-sqlite3');
14+
15+
class Client_PhotoStructureSQLite extends Client_BetterSQLite3 {
16+
_driver() {
17+
// Load @photostructure/sqlite instead of better-sqlite3
18+
return require('@photostructure/sqlite');
19+
}
20+
21+
async acquireRawConnection() {
22+
const driver = this.driver;
23+
const options = this.connectionSettings.options || {};
24+
25+
// Create the database connection with @photostructure/sqlite
26+
const db = new driver.DatabaseSync(
27+
this.connectionSettings.filename,
28+
{
29+
readonly: !!options.readonly,
30+
readBigInts: !!options.safeIntegers,
31+
}
32+
);
33+
34+
// Enhance the database with better-sqlite3-style methods
35+
// (adds .pragma(), .transaction(), .pluck(), .raw(), .expand())
36+
const enhancedDb = driver.enhance(db);
37+
38+
// Wrap prepare() to add the .reader property that better-sqlite3 provides.
39+
// Knex uses .reader to decide between .all() (for SELECTs) and .run()
40+
// (for INSERT/UPDATE/DELETE). We use stmt.columns().length > 0 which
41+
// matches better-sqlite3's native behavior (sqlite3_column_count >= 1)
42+
// and correctly handles RETURNING clauses.
43+
const originalPrepare = enhancedDb.prepare.bind(enhancedDb);
44+
enhancedDb.prepare = (sql, prepareOptions) => {
45+
const stmt = originalPrepare(sql, prepareOptions);
46+
47+
Object.defineProperty(stmt, 'reader', {
48+
value: stmt.columns().length > 0,
49+
enumerable: true,
50+
configurable: true,
51+
writable: false
52+
});
53+
54+
return stmt;
55+
};
56+
57+
return enhancedDb;
58+
}
59+
60+
// Override _query to handle the difference between better-sqlite3's safeIntegers()
61+
// and @photostructure/sqlite's setReadBigInts()
62+
async _query(connection, obj) {
63+
if (!obj.sql) throw new Error('The query is empty');
64+
65+
if (!connection) {
66+
throw new Error('No connection provided');
67+
}
68+
69+
const statement = connection.prepare(obj.sql);
70+
71+
const safeIntegers = this._optSafeIntegers(obj.options);
72+
if (safeIntegers !== undefined) {
73+
// @photostructure/sqlite uses setReadBigInts() instead of safeIntegers()
74+
if (typeof statement.setReadBigInts === 'function') {
75+
statement.setReadBigInts(safeIntegers);
76+
} else if (typeof statement.safeIntegers === 'function') {
77+
// Fallback for better-sqlite3 compatibility
78+
statement.safeIntegers(safeIntegers);
79+
}
80+
}
81+
82+
const bindings = this._formatBindings(obj.bindings);
83+
84+
if (statement.reader) {
85+
// @photostructure/sqlite expects variadic arguments, not an array
86+
const response = await statement.all(...bindings);
87+
obj.response = response;
88+
return obj;
89+
}
90+
91+
// @photostructure/sqlite expects variadic arguments, not an array
92+
const response = await statement.run(...bindings);
93+
obj.response = response;
94+
obj.context = {
95+
lastID: response.lastInsertRowid,
96+
changes: response.changes,
97+
};
98+
99+
return obj;
100+
}
101+
102+
_optSafeIntegers(options) {
103+
if (
104+
options &&
105+
typeof options === 'object' &&
106+
typeof options.safeIntegers === 'boolean'
107+
) {
108+
return options.safeIntegers;
109+
}
110+
return undefined;
111+
}
112+
}
113+
114+
Object.assign(Client_PhotoStructureSQLite.prototype, {
115+
driverName: '@photostructure/sqlite',
116+
});
117+
118+
module.exports = Client_PhotoStructureSQLite;

0 commit comments

Comments
 (0)