@pikku/kysely-node-sqlite 0.12.7 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/src/create-node-sqlite-kysely.d.ts +6 -0
- package/dist/src/create-node-sqlite-kysely.js +11 -1
- package/dist/src/node-sqlite-adapter.js +20 -3
- package/package.json +13 -9
- package/run-tests.sh +59 -24
- package/dist/src/coercion-plugin.d.ts +0 -24
- package/dist/src/coercion-plugin.js +0 -77
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/coercion-plugin.test.ts +0 -90
- package/src/create-node-sqlite-kysely.ts +0 -43
- package/src/index.ts +0 -16
- package/src/node-sqlite-adapter.ts +0 -62
- package/src/register-functions.test.ts +0 -52
- package/src/register-functions.ts +0 -27
- package/tsconfig.json +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @pikku/kysely-node-sqlite
|
|
2
2
|
|
|
3
|
+
## 0.12.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1fe79bc: A standalone build of a SQLite app now ships its `db.sqliteExtensions` (sqlite-vec's vec0 by default) inside the artifact, so a migration or query that uses them works in production the way it does under `pikku dev`. The node bundle loads them from `sqlite-extensions/` beside itself; a compiled bun binary embeds them and writes them out under `$PIKKU_DATA_DIR/.pikku-sqlite-extensions/` on start. The libraries are the build machine's, so an extension that cannot be resolved there fails the build; `[]` builds without them.
|
|
8
|
+
|
|
9
|
+
`createNodeSqliteKysely` and `createBunSqliteKysely` take an `extensions` list of library paths to load into the connection.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [1394385]
|
|
12
|
+
- Updated dependencies [7740547]
|
|
13
|
+
- @pikku/core@0.12.124
|
|
14
|
+
- @pikku/kysely-sqlite@0.12.15
|
|
15
|
+
|
|
16
|
+
## 0.12.8
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 4c7a1b5: Run the monorepo's own scripts through bun instead of yarn. What moves is the
|
|
21
|
+
package manager each package's `prepublishOnly` and build scripts invoke, plus
|
|
22
|
+
the two manifest fixes bun needs to resolve the tree: `uWebSockets.js` is
|
|
23
|
+
declared with an explicit `github:` specifier, and `@pikku/uws-handler` marks
|
|
24
|
+
its `uWebSockets.js` peer optional so a bun install of a consumer that brings
|
|
25
|
+
its own uWS app does not try to fetch it from the registry.
|
|
26
|
+
|
|
27
|
+
Three published behaviours change, all of them cases where an isolated
|
|
28
|
+
`node_modules` or bun as the runtime had been papered over by yarn's hoisting:
|
|
29
|
+
|
|
30
|
+
- `@pikku/migrator-sql` turns foreign keys on when it opens a sqlite database
|
|
31
|
+
through bun. `node:sqlite` enforces them by default and `bun:sqlite` does not,
|
|
32
|
+
which silently turned every `ON DELETE CASCADE` into a no-op under
|
|
33
|
+
`bunx --bun pikku`.
|
|
34
|
+
- `@pikku/cli` resolves a deploy provider against the project being deployed
|
|
35
|
+
rather than against wherever the CLI itself is installed, which is what its
|
|
36
|
+
own "is not installed" error asks the user to arrange.
|
|
37
|
+
- `@pikku/cli` treats a specifier a runtime hands straight back — bun does this
|
|
38
|
+
for the modules it implements itself — as not resolved from the project, so
|
|
39
|
+
it falls back rather than loading the runtime's own copy.
|
|
40
|
+
|
|
41
|
+
- Updated dependencies [4c7a1b5]
|
|
42
|
+
- @pikku/core@0.12.108
|
|
43
|
+
- @pikku/kysely@0.13.25
|
|
44
|
+
- @pikku/kysely-sqlite@0.12.14
|
|
45
|
+
|
|
3
46
|
## 0.12.7
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
|
@@ -16,5 +16,11 @@ export interface CreateNodeSqliteKyselyOptions {
|
|
|
16
16
|
* register functions. Using it is a decision to stay on Node.
|
|
17
17
|
*/
|
|
18
18
|
functions?: SqliteFunctionMap;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute paths of loadable SQLite extensions (sqlite-vec's `vec0`, say) to
|
|
21
|
+
* load before the database is handed to Kysely. Loaded through the C API;
|
|
22
|
+
* SQL's own `load_extension()` stays refused.
|
|
23
|
+
*/
|
|
24
|
+
extensions?: string[];
|
|
19
25
|
}
|
|
20
26
|
export declare function createNodeSqliteKysely<DB>(options: CreateNodeSqliteKyselyOptions): Kysely<DB>;
|
|
@@ -3,7 +3,17 @@ import { Kysely, SqliteDialect, CamelCasePlugin, } from 'kysely';
|
|
|
3
3
|
import { NodeSqliteDatabase } from './node-sqlite-adapter.js';
|
|
4
4
|
import { registerSqliteFunctions } from './register-functions.js';
|
|
5
5
|
export function createNodeSqliteKysely(options) {
|
|
6
|
-
const
|
|
6
|
+
const extensions = options.extensions ?? [];
|
|
7
|
+
// node:sqlite refuses loadExtension on a connection not opened allowing it,
|
|
8
|
+
// so allow it only when there is something to load, and shut it again after.
|
|
9
|
+
const db = new DatabaseSync(options.filename, {
|
|
10
|
+
allowExtension: extensions.length > 0,
|
|
11
|
+
});
|
|
12
|
+
if (extensions.length > 0) {
|
|
13
|
+
for (const path of extensions)
|
|
14
|
+
db.loadExtension(path);
|
|
15
|
+
db.enableLoadExtension(false);
|
|
16
|
+
}
|
|
7
17
|
if (options.functions)
|
|
8
18
|
registerSqliteFunctions(db, options.functions);
|
|
9
19
|
const plugins = [];
|
|
@@ -16,6 +16,22 @@ function coerce(v) {
|
|
|
16
16
|
return JSON.stringify(v);
|
|
17
17
|
return v;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a statement returns rows. node:sqlite's StatementSync has no `reader`
|
|
21
|
+
* flag (better-sqlite3, which Kysely's SqliteDialect was written against, does),
|
|
22
|
+
* so the value has to come from the SQL: a writer is run through `.run()` for
|
|
23
|
+
* its `changes`/`lastInsertRowid`, a reader through `.all()` for its rows, and
|
|
24
|
+
* getting it wrong silently drops whichever half the caller wanted.
|
|
25
|
+
*
|
|
26
|
+
* The list is wider than SELECT because Kysely's own SQLite introspector asks
|
|
27
|
+
* for columns through a CTE over `pragma_table_info`, so a statement that opens
|
|
28
|
+
* with WITH is a reader too — treating it as a writer reports every table as
|
|
29
|
+
* having no columns.
|
|
30
|
+
*/
|
|
31
|
+
function isReaderSql(sql) {
|
|
32
|
+
return (/^\s*(select|with|pragma|values|explain)\b/i.test(sql) ||
|
|
33
|
+
/\breturning\b/i.test(sql));
|
|
34
|
+
}
|
|
19
35
|
/**
|
|
20
36
|
* Wraps node:sqlite's DatabaseSync as Kysely's SqliteDatabase. The shapes
|
|
21
37
|
* are close but not identical: node:sqlite's Statement methods take
|
|
@@ -24,9 +40,10 @@ function coerce(v) {
|
|
|
24
40
|
*/
|
|
25
41
|
class NodeSqliteStatement {
|
|
26
42
|
stmt;
|
|
27
|
-
reader
|
|
28
|
-
constructor(stmt) {
|
|
43
|
+
reader;
|
|
44
|
+
constructor(stmt, reader) {
|
|
29
45
|
this.stmt = stmt;
|
|
46
|
+
this.reader = reader;
|
|
30
47
|
}
|
|
31
48
|
all(parameters) {
|
|
32
49
|
return this.stmt.all(...parameters.map(coerce));
|
|
@@ -50,7 +67,7 @@ export class NodeSqliteDatabase {
|
|
|
50
67
|
this.db = db;
|
|
51
68
|
}
|
|
52
69
|
prepare(sql) {
|
|
53
|
-
return new NodeSqliteStatement(this.db.prepare(sql));
|
|
70
|
+
return new NodeSqliteStatement(this.db.prepare(sql), isReaderSql(sql));
|
|
54
71
|
}
|
|
55
72
|
close() {
|
|
56
73
|
this.db.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely-node-sqlite",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.9",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
7
|
+
"directory": "packages/services/kysely-node-sqlite"
|
|
8
|
+
},
|
|
4
9
|
"description": "Kysely driver for Pikku backed by the built-in node:sqlite module",
|
|
5
10
|
"author": "yasser.fadl@gmail.com",
|
|
6
11
|
"license": "MIT",
|
|
@@ -14,26 +19,25 @@
|
|
|
14
19
|
"test": "bash run-tests.sh",
|
|
15
20
|
"test:watch": "bash run-tests.sh --watch",
|
|
16
21
|
"test:coverage": "bash run-tests.sh --coverage",
|
|
17
|
-
"prepublishOnly": "
|
|
22
|
+
"prepublishOnly": "bun run build"
|
|
18
23
|
},
|
|
19
24
|
"engines": {
|
|
20
25
|
"node": ">=22.10"
|
|
21
26
|
},
|
|
22
27
|
"dependencies": {
|
|
23
|
-
"@pikku/kysely": "^0.13.
|
|
24
|
-
"@pikku/kysely-sqlite": "^0.12.
|
|
28
|
+
"@pikku/kysely": "^0.13.25",
|
|
29
|
+
"@pikku/kysely-sqlite": "^0.12.15",
|
|
25
30
|
"kysely": "^0.29.5"
|
|
26
31
|
},
|
|
27
32
|
"peerDependencies": {
|
|
28
|
-
"@pikku/core": "^0.12.
|
|
33
|
+
"@pikku/core": "^0.12.124"
|
|
29
34
|
},
|
|
30
35
|
"devDependencies": {
|
|
31
|
-
"@pikku/core": "^0.12.
|
|
36
|
+
"@pikku/core": "^0.12.124",
|
|
32
37
|
"@types/node": "^22",
|
|
33
|
-
"tsx": "^4.23.12"
|
|
34
|
-
"typescript": "^6.0.3"
|
|
38
|
+
"tsx": "^4.23.12"
|
|
35
39
|
},
|
|
36
40
|
"exports": {
|
|
37
41
|
".": "./dist/src/index.js"
|
|
38
42
|
}
|
|
39
|
-
}
|
|
43
|
+
}
|
package/run-tests.sh
CHANGED
|
@@ -1,33 +1,68 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
# Enable nullglob to handle cases where no files match the pattern
|
|
4
|
+
shopt -s nullglob
|
|
5
|
+
|
|
6
|
+
# Initialize variables for options
|
|
7
|
+
watch_mode=false
|
|
8
|
+
coverage_mode=false
|
|
9
|
+
|
|
10
|
+
# Parse command-line options
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case $1 in
|
|
13
|
+
--watch)
|
|
14
|
+
watch_mode=true
|
|
15
|
+
shift
|
|
16
|
+
;;
|
|
17
|
+
--coverage)
|
|
18
|
+
coverage_mode=true
|
|
19
|
+
shift
|
|
20
|
+
;;
|
|
21
|
+
*)
|
|
22
|
+
echo "Unknown option: $1"
|
|
23
|
+
exit 1
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
19
26
|
done
|
|
20
27
|
|
|
21
|
-
#
|
|
22
|
-
|
|
28
|
+
# Define the pattern to match your test files
|
|
29
|
+
pattern="src/*.test.ts"
|
|
30
|
+
|
|
31
|
+
# Expand the pattern into an array of files
|
|
32
|
+
files=($(find src -type f -name "*.test.ts"))
|
|
33
|
+
|
|
34
|
+
# Check if any files matched the pattern
|
|
35
|
+
if [ ${#files[@]} -eq 0 ]; then
|
|
36
|
+
echo "No test files found matching pattern: $pattern"
|
|
37
|
+
if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Construct the bun command
|
|
44
|
+
#
|
|
45
|
+
# --parallel gives every test file its own worker process, which is what node's
|
|
46
|
+
# `--test` did. Several suites keep module-level caches (a TypeScript program, a
|
|
47
|
+
# temp fixture directory) that a shared process would leak between files.
|
|
48
|
+
#
|
|
49
|
+
# --timeout because bun caps a test at 5s by default where node had no cap, and
|
|
50
|
+
# the suites that build real TypeScript programs run well past that.
|
|
51
|
+
bun_cmd=(bun test --parallel --timeout 120000)
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
# Append options based on flags
|
|
54
|
+
if [ "$watch_mode" = true ]; then
|
|
55
|
+
# --watch reruns in one process; parallel workers have nothing to watch.
|
|
56
|
+
bun_cmd=(bun test --watch --timeout 120000)
|
|
26
57
|
fi
|
|
27
58
|
|
|
28
|
-
if [ "$
|
|
29
|
-
|
|
59
|
+
if [ "$coverage_mode" = true ]; then
|
|
60
|
+
# --coverage-dir=. keeps lcov.info at the package root: CI merges every
|
|
61
|
+
# package's report by prefixing its SF paths with the file's own directory,
|
|
62
|
+
# so a nested coverage/ directory would re-root them one level too deep.
|
|
63
|
+
bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
|
|
64
|
+
export PIKKU_TEST_COVERAGE=1
|
|
30
65
|
fi
|
|
31
66
|
|
|
32
|
-
# Execute the command
|
|
33
|
-
|
|
67
|
+
# Execute the bun command with the expanded list of files
|
|
68
|
+
"${bun_cmd[@]}" "${files[@]}"
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { KyselyPlugin } from 'kysely';
|
|
2
|
-
export type ColumnKind = 'date' | 'bool' | 'json';
|
|
3
|
-
/**
|
|
4
|
-
* Per-table column kind map. Keys are snake_case table names matching the
|
|
5
|
-
* physical SQLite tables; inner keys are snake_case column names.
|
|
6
|
-
* Generated by `pikku db migrate` from `-- @date|@bool|@json` annotations
|
|
7
|
-
* and naming conventions (col ends in _at/_on → date; starts with is_/has_/can_ → bool).
|
|
8
|
-
*/
|
|
9
|
-
export type CoercionMap = Record<string, Record<string, ColumnKind>>;
|
|
10
|
-
export interface CreateCoercionPluginOptions {
|
|
11
|
-
map: CoercionMap;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Convert SQLite-stored values (TEXT/INTEGER) back into the logical types
|
|
15
|
-
* app code expects (Date / boolean / parsed JSON).
|
|
16
|
-
*
|
|
17
|
-
* Write-side coercion (Date → ISO string, boolean → 0/1, object → JSON) is
|
|
18
|
-
* handled in the NodeSqliteDatabase adapter (always-on), so this plugin is
|
|
19
|
-
* read-only.
|
|
20
|
-
*
|
|
21
|
-
* Place AFTER CamelCasePlugin in the plugin array (or it handles both
|
|
22
|
-
* orderings via the dual-keyed global map).
|
|
23
|
-
*/
|
|
24
|
-
export declare function createCoercionPlugin(options: CreateCoercionPluginOptions): KyselyPlugin;
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
function fromDb(value, kind) {
|
|
2
|
-
if (value == null)
|
|
3
|
-
return value;
|
|
4
|
-
switch (kind) {
|
|
5
|
-
case 'date':
|
|
6
|
-
if (typeof value === 'string') {
|
|
7
|
-
const d = new Date(value);
|
|
8
|
-
return Number.isNaN(d.getTime()) ? value : d;
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
case 'bool':
|
|
12
|
-
if (typeof value === 'number')
|
|
13
|
-
return value !== 0;
|
|
14
|
-
if (typeof value === 'bigint')
|
|
15
|
-
return value !== 0n;
|
|
16
|
-
return value;
|
|
17
|
-
case 'json':
|
|
18
|
-
if (typeof value !== 'string')
|
|
19
|
-
return value;
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(value);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function snakeToCamel(name) {
|
|
29
|
-
return name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Flatten the per-table map into a single column-name → kind lookup.
|
|
33
|
-
* Indexed by BOTH snake_case and camelCase so it works regardless of
|
|
34
|
-
* CamelCasePlugin ordering in the plugin array.
|
|
35
|
-
*/
|
|
36
|
-
function buildGlobalMap(map) {
|
|
37
|
-
const out = {};
|
|
38
|
-
for (const tbl of Object.values(map)) {
|
|
39
|
-
for (const [col, kind] of Object.entries(tbl)) {
|
|
40
|
-
out[col] = kind;
|
|
41
|
-
out[snakeToCamel(col)] = kind;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Convert SQLite-stored values (TEXT/INTEGER) back into the logical types
|
|
48
|
-
* app code expects (Date / boolean / parsed JSON).
|
|
49
|
-
*
|
|
50
|
-
* Write-side coercion (Date → ISO string, boolean → 0/1, object → JSON) is
|
|
51
|
-
* handled in the NodeSqliteDatabase adapter (always-on), so this plugin is
|
|
52
|
-
* read-only.
|
|
53
|
-
*
|
|
54
|
-
* Place AFTER CamelCasePlugin in the plugin array (or it handles both
|
|
55
|
-
* orderings via the dual-keyed global map).
|
|
56
|
-
*/
|
|
57
|
-
export function createCoercionPlugin(options) {
|
|
58
|
-
const globalMap = buildGlobalMap(options.map);
|
|
59
|
-
return {
|
|
60
|
-
transformQuery(args) {
|
|
61
|
-
return args.node;
|
|
62
|
-
},
|
|
63
|
-
async transformResult(args) {
|
|
64
|
-
const out = [];
|
|
65
|
-
for (const row of args.result.rows) {
|
|
66
|
-
const next = { ...row };
|
|
67
|
-
for (const [col, val] of Object.entries(row)) {
|
|
68
|
-
const kind = globalMap[col];
|
|
69
|
-
if (kind)
|
|
70
|
-
next[col] = fromDb(val, kind);
|
|
71
|
-
}
|
|
72
|
-
out.push(next);
|
|
73
|
-
}
|
|
74
|
-
return { ...args.result, rows: out };
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"root":["../src/create-node-sqlite-kysely.ts","../src/index.ts","../src/node-sqlite-adapter.ts","../src/register-functions.ts"],"version":"6.0.3"}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { createNodeSqliteKysely, createCoercionPlugin } from './index.js'
|
|
4
|
-
import type { CoercionMap } from './index.js'
|
|
5
|
-
import { sql, type Kysely } from 'kysely'
|
|
6
|
-
|
|
7
|
-
interface TestDB {
|
|
8
|
-
users: { id: number; settings: unknown }
|
|
9
|
-
posts: { id: number; settings: unknown }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The same map the CLI's local database builds from `coercion.gen.ts`: two
|
|
14
|
-
* tables that disagree on the kind of a same-named column. Resolving it needs
|
|
15
|
-
* the query's tables, which is what kept the CLI's copy and the runtime copies
|
|
16
|
-
* from behaving identically.
|
|
17
|
-
*/
|
|
18
|
-
const map: CoercionMap = {
|
|
19
|
-
users: { settings: 'json' },
|
|
20
|
-
posts: { settings: 'bool' },
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const seed = async (db: Kysely<TestDB>) => {
|
|
24
|
-
await sql`create table users (id integer primary key, settings text)`.execute(
|
|
25
|
-
db
|
|
26
|
-
)
|
|
27
|
-
await sql`create table posts (id integer primary key, settings integer)`.execute(
|
|
28
|
-
db
|
|
29
|
-
)
|
|
30
|
-
await sql`insert into users (id, settings) values (1, '{"theme":"dark"}')`.execute(
|
|
31
|
-
db
|
|
32
|
-
)
|
|
33
|
-
await sql`insert into posts (id, settings) values (1, 1)`.execute(db)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('node-sqlite coercion plugin', () => {
|
|
37
|
-
test('resolves a colliding column against the queried table', async () => {
|
|
38
|
-
const db = createNodeSqliteKysely<TestDB>({
|
|
39
|
-
filename: ':memory:',
|
|
40
|
-
plugins: [createCoercionPlugin({ map })],
|
|
41
|
-
})
|
|
42
|
-
await seed(db)
|
|
43
|
-
|
|
44
|
-
const user = await db
|
|
45
|
-
.selectFrom('users')
|
|
46
|
-
.select('settings')
|
|
47
|
-
.executeTakeFirstOrThrow()
|
|
48
|
-
assert.deepEqual(user.settings, { theme: 'dark' })
|
|
49
|
-
|
|
50
|
-
const post = await db
|
|
51
|
-
.selectFrom('posts')
|
|
52
|
-
.select('settings')
|
|
53
|
-
.executeTakeFirstOrThrow()
|
|
54
|
-
assert.equal(post.settings, true)
|
|
55
|
-
|
|
56
|
-
await db.destroy()
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('coerces date, bool and json columns read back from SQLite', async () => {
|
|
60
|
-
const db = createNodeSqliteKysely<{
|
|
61
|
-
rows: { created_at: unknown; is_active: unknown; meta: unknown }
|
|
62
|
-
}>({
|
|
63
|
-
filename: ':memory:',
|
|
64
|
-
camelCase: false,
|
|
65
|
-
plugins: [
|
|
66
|
-
createCoercionPlugin({
|
|
67
|
-
map: {
|
|
68
|
-
rows: { created_at: 'date', is_active: 'bool', meta: 'json' },
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
],
|
|
72
|
-
})
|
|
73
|
-
await sql`create table rows (created_at text, is_active integer, meta text)`.execute(
|
|
74
|
-
db
|
|
75
|
-
)
|
|
76
|
-
await sql`insert into rows values ('2026-06-26T00:00:00.000Z', 0, '{"a":1}')`.execute(
|
|
77
|
-
db
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
const row = await db
|
|
81
|
-
.selectFrom('rows')
|
|
82
|
-
.selectAll()
|
|
83
|
-
.executeTakeFirstOrThrow()
|
|
84
|
-
assert.ok(row.created_at instanceof Date)
|
|
85
|
-
assert.equal(row.is_active, false)
|
|
86
|
-
assert.deepEqual(row.meta, { a: 1 })
|
|
87
|
-
|
|
88
|
-
await db.destroy()
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite'
|
|
2
|
-
import {
|
|
3
|
-
Kysely,
|
|
4
|
-
SqliteDialect,
|
|
5
|
-
CamelCasePlugin,
|
|
6
|
-
type KyselyPlugin,
|
|
7
|
-
} from 'kysely'
|
|
8
|
-
import type { SqliteFunctionMap } from '@pikku/kysely-sqlite'
|
|
9
|
-
import { NodeSqliteDatabase } from './node-sqlite-adapter.js'
|
|
10
|
-
import { registerSqliteFunctions } from './register-functions.js'
|
|
11
|
-
|
|
12
|
-
export interface CreateNodeSqliteKyselyOptions {
|
|
13
|
-
/** Path to the SQLite file. Use ':memory:' for an in-memory DB. */
|
|
14
|
-
filename: string
|
|
15
|
-
/** Apply CamelCasePlugin so DB columns map to camelCase TS fields. Default true. */
|
|
16
|
-
camelCase?: boolean
|
|
17
|
-
/** Extra plugins to layer on top. */
|
|
18
|
-
plugins?: KyselyPlugin[]
|
|
19
|
-
/**
|
|
20
|
-
* Scalar user-defined SQL functions to register, keyed by the name SQL calls
|
|
21
|
-
* them by. Registered as deterministic.
|
|
22
|
-
*
|
|
23
|
-
* Note that this is the one option with no bun equivalent:
|
|
24
|
-
* `createBunSqliteKysely` throws if it is set, because bun:sqlite cannot
|
|
25
|
-
* register functions. Using it is a decision to stay on Node.
|
|
26
|
-
*/
|
|
27
|
-
functions?: SqliteFunctionMap
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function createNodeSqliteKysely<DB>(
|
|
31
|
-
options: CreateNodeSqliteKyselyOptions
|
|
32
|
-
): Kysely<DB> {
|
|
33
|
-
const db = new DatabaseSync(options.filename)
|
|
34
|
-
if (options.functions) registerSqliteFunctions(db, options.functions)
|
|
35
|
-
const plugins: KyselyPlugin[] = []
|
|
36
|
-
if (options.camelCase ?? true) plugins.push(new CamelCasePlugin())
|
|
37
|
-
if (options.plugins) plugins.push(...options.plugins)
|
|
38
|
-
|
|
39
|
-
return new Kysely<DB>({
|
|
40
|
-
dialect: new SqliteDialect({ database: new NodeSqliteDatabase(db) }),
|
|
41
|
-
plugins,
|
|
42
|
-
})
|
|
43
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export { NodeSqliteDatabase } from './node-sqlite-adapter.js'
|
|
2
|
-
export {
|
|
3
|
-
createNodeSqliteKysely,
|
|
4
|
-
type CreateNodeSqliteKyselyOptions,
|
|
5
|
-
} from './create-node-sqlite-kysely.js'
|
|
6
|
-
export { registerSqliteFunctions } from './register-functions.js'
|
|
7
|
-
export {
|
|
8
|
-
SqliteFunctionsUnsupportedError,
|
|
9
|
-
type SqliteFunctionMap,
|
|
10
|
-
} from '@pikku/kysely-sqlite'
|
|
11
|
-
export {
|
|
12
|
-
createCoercionPlugin,
|
|
13
|
-
type ColumnKind,
|
|
14
|
-
type CoercionMap,
|
|
15
|
-
type CreateCoercionPluginOptions,
|
|
16
|
-
} from '@pikku/kysely'
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync, StatementSync, SQLInputValue } from 'node:sqlite'
|
|
2
|
-
import type { SqliteDatabase, SqliteStatement } from 'kysely'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Always-on write-side coercion: converts JS values that SQLite's node:sqlite
|
|
6
|
-
* binding cannot accept into their storable equivalents.
|
|
7
|
-
* boolean → 0 | 1
|
|
8
|
-
* Date → ISO 8601 string
|
|
9
|
-
* object → JSON string (covers arrays and plain objects)
|
|
10
|
-
*/
|
|
11
|
-
function coerce(v: unknown): SQLInputValue {
|
|
12
|
-
if (v === null || v === undefined) return null as unknown as SQLInputValue
|
|
13
|
-
if (typeof v === 'boolean') return v ? 1 : 0
|
|
14
|
-
if (v instanceof Date) return v.toISOString()
|
|
15
|
-
if (typeof v === 'object') return JSON.stringify(v)
|
|
16
|
-
return v as SQLInputValue
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Wraps node:sqlite's DatabaseSync as Kysely's SqliteDatabase. The shapes
|
|
21
|
-
* are close but not identical: node:sqlite's Statement methods take
|
|
22
|
-
* variadic positional params and always return bigint counters; Kysely's
|
|
23
|
-
* dialect passes parameters as a ReadonlyArray and expects number|bigint.
|
|
24
|
-
*/
|
|
25
|
-
class NodeSqliteStatement implements SqliteStatement {
|
|
26
|
-
readonly reader = true
|
|
27
|
-
|
|
28
|
-
constructor(private readonly stmt: StatementSync) {}
|
|
29
|
-
|
|
30
|
-
all(parameters: ReadonlyArray<unknown>): unknown[] {
|
|
31
|
-
return this.stmt.all(...parameters.map(coerce)) as unknown[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
*iterate(parameters: ReadonlyArray<unknown>): IterableIterator<unknown> {
|
|
35
|
-
for (const row of this.stmt.iterate(...parameters.map(coerce))) {
|
|
36
|
-
yield row
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
run(parameters: ReadonlyArray<unknown>): {
|
|
41
|
-
changes: number | bigint
|
|
42
|
-
lastInsertRowid: number | bigint
|
|
43
|
-
} {
|
|
44
|
-
const result = this.stmt.run(...parameters.map(coerce))
|
|
45
|
-
return {
|
|
46
|
-
changes: result.changes,
|
|
47
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class NodeSqliteDatabase implements SqliteDatabase {
|
|
53
|
-
constructor(private readonly db: DatabaseSync) {}
|
|
54
|
-
|
|
55
|
-
prepare(sql: string): SqliteStatement {
|
|
56
|
-
return new NodeSqliteStatement(this.db.prepare(sql))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
close(): void {
|
|
60
|
-
this.db.close()
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { DatabaseSync } from 'node:sqlite'
|
|
4
|
-
import { sql } from 'kysely'
|
|
5
|
-
import { registerSqliteFunctions } from './register-functions.js'
|
|
6
|
-
import { createNodeSqliteKysely } from './create-node-sqlite-kysely.js'
|
|
7
|
-
|
|
8
|
-
describe('registerSqliteFunctions', () => {
|
|
9
|
-
test('makes the function callable from SQL', () => {
|
|
10
|
-
const db = new DatabaseSync(':memory:')
|
|
11
|
-
registerSqliteFunctions(db, {
|
|
12
|
-
shout: (value) => `${String(value).toUpperCase()}!`,
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const row = db.prepare(`SELECT shout('hello') AS v`).get() as { v: string }
|
|
16
|
-
assert.equal(row.v, 'HELLO!')
|
|
17
|
-
db.close()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('registers as deterministic, so it is usable in an index', () => {
|
|
21
|
-
const db = new DatabaseSync(':memory:')
|
|
22
|
-
registerSqliteFunctions(db, { half: (n) => Number(n) / 2 })
|
|
23
|
-
|
|
24
|
-
db.exec('CREATE TABLE t (n INTEGER)')
|
|
25
|
-
// SQLite rejects non-deterministic functions in an index outright, so this
|
|
26
|
-
// statement succeeding IS the assertion about how they were registered.
|
|
27
|
-
db.exec('CREATE INDEX t_half ON t (half(n))')
|
|
28
|
-
db.close()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('registers every entry in the map', () => {
|
|
32
|
-
const db = new DatabaseSync(':memory:')
|
|
33
|
-
registerSqliteFunctions(db, { a: () => 1, b: () => 2 })
|
|
34
|
-
|
|
35
|
-
const row = db.prepare('SELECT a() + b() AS v').get() as { v: number }
|
|
36
|
-
assert.equal(row.v, 3)
|
|
37
|
-
db.close()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('createNodeSqliteKysely registers them on the connection it builds', async () => {
|
|
41
|
-
const db = createNodeSqliteKysely<{ t: { id: number } }>({
|
|
42
|
-
filename: ':memory:',
|
|
43
|
-
functions: { double: (n) => Number(n) * 2 },
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const result = await sql<{
|
|
47
|
-
v: number
|
|
48
|
-
}>`SELECT double(21) AS v`.execute(db)
|
|
49
|
-
assert.equal(result.rows[0]!.v, 42)
|
|
50
|
-
await db.destroy()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync } from 'node:sqlite'
|
|
2
|
-
import type { SqliteFunctionMap } from '@pikku/kysely-sqlite'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Registers scalar user-defined SQL functions on a node:sqlite connection.
|
|
6
|
-
*
|
|
7
|
-
* Exported separately from `createNodeSqliteKysely` because an app that needs
|
|
8
|
-
* pragmas — WAL, `foreign_keys`, `busy_timeout` — has to build the connection
|
|
9
|
-
* itself, and would otherwise have no supported way to add UDFs to it.
|
|
10
|
-
*
|
|
11
|
-
* Registered as `deterministic`, which is what makes them usable in indexes and
|
|
12
|
-
* lets SQLite cache repeated calls with the same arguments. That is a promise
|
|
13
|
-
* about the function, not a hint: one whose output depends on the clock, a
|
|
14
|
-
* random source or anything mutable must not go through here.
|
|
15
|
-
*
|
|
16
|
-
* There is no bun counterpart. `@pikku/kysely-bun-sqlite` exports the same name
|
|
17
|
-
* so the import resolves either way, but it throws — see
|
|
18
|
-
* SqliteFunctionsUnsupportedError.
|
|
19
|
-
*/
|
|
20
|
-
export function registerSqliteFunctions(
|
|
21
|
-
db: DatabaseSync,
|
|
22
|
-
functions: SqliteFunctionMap
|
|
23
|
-
): void {
|
|
24
|
-
for (const [name, fn] of Object.entries(functions)) {
|
|
25
|
-
db.function(name, { deterministic: true }, fn as never)
|
|
26
|
-
}
|
|
27
|
-
}
|
package/tsconfig.json
DELETED