@gjsify/sqlite-native 0.0.0-stage → 0.64.12-next.281

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/README.md CHANGED
@@ -1,3 +1,99 @@
1
- # Temporary Holding Version
1
+ # @gjsify/sqlite-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Optional native Vala bridge over the system **libsqlite3** for GJS: a connection handle
4
+ (`GjsifySqlite.Database`), prepared statements with 1-based parameter binding
5
+ (`GjsifySqlite.Statement`), and a typed column cursor. Errors carry sqlite's **extended** result
6
+ code, so a caller can tell `SQLITE_CONSTRAINT_UNIQUE` (2067) from bare `SQLITE_CONSTRAINT` (19).
7
+
8
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS
9
+ (GNOME JavaScript).
10
+
11
+ ## What is and is not here
12
+
13
+ The bridge is deliberately thin. It exposes only what needs a C pointer — a live `sqlite3_stmt`
14
+ with its bindings and cursor — and nothing it can avoid. Everything `node:sqlite` promises on top
15
+ of that (the `DatabaseSync`/`StatementSync` surface, statement caching, JS value coercion,
16
+ `readBigInts`, the error class hierarchy) belongs to [`@gjsify/sqlite`](../sqlite) and is written
17
+ in TypeScript.
18
+
19
+ Text is UTF-8 in both directions, straight through: sqlite's native encoding is GJS's.
20
+
21
+ ## Installation
22
+
23
+ This package is loaded automatically by `@gjsify/sqlite` when present.
24
+
25
+ ```bash
26
+ gjsify install @gjsify/sqlite-native
27
+
28
+ # npm or yarn also work:
29
+ npm install @gjsify/sqlite-native
30
+ yarn add @gjsify/sqlite-native
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```typescript
36
+ import { hasNativeSqlite, loadNativeSqlite } from '@gjsify/sqlite-native';
37
+
38
+ if (hasNativeSqlite()) {
39
+ const { Database, OpenMode, StepResult } = loadNativeSqlite()!;
40
+
41
+ const db = Database.open(':memory:', OpenMode.READWRITE_CREATE, false);
42
+ db.exec('CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT)');
43
+
44
+ const insert = db.prepare('INSERT INTO t (b) VALUES (?)');
45
+ insert.bind_text(1, 'hello');
46
+ insert.step();
47
+ insert.close();
48
+ console.log(db.last_insert_rowid, db.changes);
49
+
50
+ const select = db.prepare('SELECT b FROM t');
51
+ while (select.step() === StepResult.ROW) {
52
+ console.log(select.column_text(0));
53
+ }
54
+ db.close();
55
+ }
56
+ ```
57
+
58
+ `Database.close()` finalizes every statement still open on the connection first, so closing in
59
+ either order is safe — and so is leaving both to the garbage collector, since each `Statement`
60
+ holds a strong reference to its database.
61
+
62
+ ## Lifetime and naming notes
63
+
64
+ - **`Statement.close()`, not `finalize()`.** The sqlite call behind it is `sqlite3_finalize`, but
65
+ a Vala method named `finalize` on a `GLib.Object` subclass collides with the generated
66
+ `GObjectClass.finalize` slot, so the method carries the name every other resource type in this
67
+ workspace uses.
68
+ - **`Database.close()` drains before it drops.** The Vala binding exposes plain `sqlite3_close`,
69
+ which refuses a connection with outstanding statements and leaks the handle with only a
70
+ warning. Finalizing the live statements first is what lets this bridge do without
71
+ `sqlite3_close_v2`.
72
+
73
+ ## Requirements
74
+
75
+ **libsqlite3 ≥ 3.37** — `sqlite3_changes64` is checked for at meson configure time and a older
76
+ library fails the build loudly rather than truncating a bulk `UPDATE`'s row count in silence.
77
+
78
+ `sqlite3_column_origin_name` is deliberately **not** exposed: it needs
79
+ `SQLITE_ENABLE_COLUMN_METADATA`, which macOS's system libsqlite3 is not built with, so a darwin
80
+ prebuild would link against a symbol that is not there. `column_decltype()` is available
81
+ everywhere and is what the JS layer uses.
82
+
83
+ ## Platform coverage
84
+
85
+ | Platform | Prebuild | Built by |
86
+ |---|---|---|
87
+ | `linux-x64`, `linux-arm64` | ✅ `.so` + `.gir` + `.typelib` | native runner |
88
+ | `linux-ppc64`, `linux-s390x`, `linux-riscv64` | ✅ | QEMU emulation |
89
+ | `darwin-arm64`, `darwin-x64` (macOS) | ✅ `.dylib` + `.gir` + `.typelib` | `macos-latest` / `macos-13` runner |
90
+ | Windows | ❌ | — no Vala toolchain leg |
91
+
92
+ Linux builds link the distribution's `sqlite-devel`; macOS links the libsqlite3 that ships in the
93
+ SDK, so no Homebrew formula is involved. All prebuilds are produced by
94
+ [`.github/workflows/prebuilds.yml`](../../../.github/workflows/prebuilds.yml) and committed back
95
+ to the repository.
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1 @@
1
+ var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{loadOptionalNativeModule as e}from"@gjsify/utils/core";let t=null,n=!1,r=null;function loadNativeSqlite(){if(n)return t;n=!0;let{module:i,error:a}=e(`GjsifySqlite`,[`Database`,`Statement`]);return t=i,r=a,t}function hasNativeSqlite(){return loadNativeSqlite()!==null}function getLoadError(){return r}export{getLoadError,hasNativeSqlite,loadNativeSqlite};
@@ -0,0 +1,28 @@
1
+ import type { GjsifySqlite as GjsifySqliteNS } from 'gi://GjsifySqlite?version=1.0';
2
+ type DatabaseInstance = GjsifySqliteNS.Database;
3
+ type DatabaseStatic = typeof GjsifySqliteNS.Database;
4
+ type StatementInstance = GjsifySqliteNS.Statement;
5
+ type StatementStatic = typeof GjsifySqliteNS.Statement;
6
+ export interface NativeSqliteModule {
7
+ Database: DatabaseStatic;
8
+ Statement: StatementStatic;
9
+ OpenMode: typeof GjsifySqliteNS.OpenMode;
10
+ StepResult: typeof GjsifySqliteNS.StepResult;
11
+ ColumnType: typeof GjsifySqliteNS.ColumnType;
12
+ }
13
+ export type { DatabaseInstance as Database };
14
+ export type { StatementInstance as Statement };
15
+ /**
16
+ * Lazily load the GjsifySqlite typelib. Idempotent — subsequent calls
17
+ * return the cached module (or `null` if the first call failed).
18
+ */
19
+ export declare function loadNativeSqlite(): NativeSqliteModule | null;
20
+ /**
21
+ * Returns `true` if the GjsifySqlite typelib is loadable (prebuild present).
22
+ */
23
+ export declare function hasNativeSqlite(): boolean;
24
+ /**
25
+ * Returns the load error from the first `loadNativeSqlite()` call, or `null`
26
+ * if loading succeeded or hasn't been attempted yet.
27
+ */
28
+ export declare function getLoadError(): Error | null;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/meson.build ADDED
@@ -0,0 +1,106 @@
1
+ # `buildtype=plain` = NO `-g`. ld64 emits a DEBUG MAP into any image whose objects
2
+ # carry DWARF — one `N_OSO` stab per object, holding its MTIME — so with `-g` every
3
+ # rebuild of this commit differed in bytes (plus the `LC_UUID` and the ad-hoc signature
4
+ # computed over them) and `commit-prebuilds` re-committed all 16 darwin dylibs. That map
5
+ # names intermediate `.o` files no consumer can have. Why: docs/prebuilds.md.
6
+ project('GjsifySqlite', ['c', 'vala'], version: '1.0',
7
+ default_options: ['buildtype=plain'])
8
+
9
+ root_dir = meson.current_source_dir()
10
+
11
+ # Required system packages (Fedora 43+):
12
+ # dnf install vala meson ninja-build sqlite-devel
13
+ #
14
+ # Bindings: Vala SHIPS a `sqlite3.vapi`, so unlike the nghttp2 bridge this one
15
+ # needs no C shim — `--pkg=sqlite3` gives Vala the whole API it uses. The four
16
+ # entry points that vapi predates are declared in src/vala/sqlite-compat.vapi,
17
+ # which is why `--vapidir` points back into this package.
18
+ #
19
+ # We use cc.find_library instead of `dependency()` for sqlite3 — `dependency()`
20
+ # makes meson pass `--pkg sqlite3` to valac AS A PKG-CONFIG NAME and then also
21
+ # hand it the pkg-config vapi lookup, which collides with the bundled vapi we
22
+ # want. find_library only affects the C compile + link stages, not Vala.
23
+ # macOS needs no formula for it: libsqlite3 and sqlite3.h are both in the SDK.
24
+ cc = meson.get_compiler('c')
25
+
26
+ sqlite3_lib = cc.find_library('sqlite3', has_headers: ['sqlite3.h'])
27
+
28
+ # `sqlite3_changes64` arrived in sqlite 3.37 (2021-11). Asserting it here turns a
29
+ # too-old libsqlite3 into a configure error naming the version, instead of an
30
+ # undefined symbol at link time or — on a host that links lazily — at dlopen,
31
+ # which docs/prebuilds.md records as the failure nobody reads correctly.
32
+ assert(cc.has_function('sqlite3_changes64',
33
+ prefix: '#include <sqlite3.h>',
34
+ dependencies: sqlite3_lib),
35
+ 'libsqlite3 is older than 3.37: sqlite3_changes64 is missing')
36
+
37
+ host_system = host_machine.system()
38
+
39
+ # On darwin every GNOME library is resolved against the HOST PROCESS rather than
40
+ # recorded as an `@rpath/` dependency: a second GLib beside the one the host
41
+ # already loaded gives the process two GType registries, then SIGSEGV.
42
+ # docs/prebuilds.md § One GLib. libsqlite3 is NOT host-provided — it reaches no
43
+ # GLib, so it stays an ordinary link dependency.
44
+ host_provided = ['glib-2.0', 'gobject-2.0']
45
+
46
+ vala_deps = []
47
+ foreach pkg : host_provided
48
+ dep = dependency(pkg)
49
+ vala_deps += host_system == 'darwin' \
50
+ ? dep.partial_dependency(compile_args: true, includes: true) \
51
+ : dep
52
+ endforeach
53
+
54
+ host_provided_link_args = host_system == 'darwin' ? ['-Wl,-undefined,dynamic_lookup'] : []
55
+
56
+ sources = files(
57
+ 'src/vala/database.vala',
58
+ 'src/vala/statement.vala',
59
+ )
60
+
61
+ libGjsifySqlite = library('gjsifysqlite', sources,
62
+ dependencies: vala_deps + [sqlite3_lib],
63
+ vala_args: [
64
+ '--pkg=sqlite3',
65
+ '--vapidir=' + root_dir / 'src/vala',
66
+ '--pkg=sqlite-compat',
67
+ ],
68
+ link_args: host_provided_link_args,
69
+ vala_gir: meson.project_name() + '-1.0.gir',
70
+ install: true,
71
+ install_dir: [true, true, true, true],
72
+ )
73
+
74
+ # ----------------------------------------------------------------------------
75
+ # Host-dependent shared-library leaf name.
76
+ #
77
+ # The typelib records the exact filename gjs' g_module_open() resolves at load
78
+ # time, and meson names the shared library per host convention: lib<name>.so on
79
+ # ELF hosts, lib<name>.dylib on macOS. Recording the wrong leaf yields a typelib
80
+ # that IS found via GI_TYPELIB_PATH but whose library never loads — a silent,
81
+ # confusing failure. Mirrors the branch in packages/napi/napi/meson.build.
82
+ #
83
+ # Windows is deliberately absent: this bridge has had no Windows portability
84
+ # work, so `else` (ELF naming) is the honest fallback rather than a .dll branch
85
+ # that would imply support which does not exist.
86
+ # ----------------------------------------------------------------------------
87
+ if host_system == 'darwin'
88
+ shared_lib_name = 'libgjsifysqlite.dylib'
89
+ else
90
+ shared_lib_name = 'libgjsifysqlite.so'
91
+ endif
92
+
93
+ g_ir_compiler = find_program('g-ir-compiler')
94
+
95
+ custom_target(meson.project_name() + '-1.0.typelib',
96
+ command: [
97
+ g_ir_compiler,
98
+ '--shared-library', shared_lib_name,
99
+ '--output', '@OUTPUT@',
100
+ meson.current_build_dir() / meson.project_name() + '-1.0.gir',
101
+ ],
102
+ output: meson.project_name() + '-1.0.typelib',
103
+ depends: libGjsifySqlite,
104
+ install: true,
105
+ install_dir: get_option('libdir') / 'girepository-1.0',
106
+ )
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
- "name": "@gjsify/sqlite-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@gjsify/sqlite-native",
3
+ "version": "0.64.12-next.281",
4
+ "description": "Optional Vala/GObject bridge over the system libsqlite3, exposing the parts of the C API that cannot be expressed in JavaScript: a connection handle, prepared statements with 1-based parameter binding, and a typed column cursor. Errors carry sqlite's extended result code. Used by @gjsify/sqlite to implement node:sqlite directly on sqlite3 instead of through libgda.",
5
+ "type": "module",
6
+ "main": "lib/esm/index.js",
7
+ "module": "lib/esm/index.js",
8
+ "types": "lib/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/types/index.d.ts",
12
+ "default": "./lib/esm/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "meson.build",
18
+ "src/vala"
19
+ ],
20
+ "gjsify": {
21
+ "platforms": [
22
+ "linux-x64",
23
+ "linux-arm64",
24
+ "linux-ppc64",
25
+ "linux-s390x",
26
+ "linux-riscv64",
27
+ "darwin-arm64",
28
+ "darwin-x64"
29
+ ],
30
+ "runtimes": {
31
+ "gjs": "polyfill",
32
+ "node": "none",
33
+ "browser": "none",
34
+ "nativescript": "none"
35
+ },
36
+ "tier": 1
37
+ },
38
+ "scripts": {
39
+ "clear": "gjsify clear lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs",
40
+ "check": "gjsify tsc --noEmit",
41
+ "init:meson": "meson setup build .",
42
+ "init:meson:wipe": "gjsify run init:meson --wipe",
43
+ "build": "gjsify run build:gjsify && gjsify run build:types",
44
+ "build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}' --exclude 'src/ts/**/*.spec.{mts,ts}' 'src/ts/test.{mts,ts}'",
45
+ "build:meson": "gjsify run init:meson && meson compile -C build",
46
+ "build:types": "gjsify tsc",
47
+ "build:prebuilds": "gjsify run build:meson && node ../../../scripts/stage-prebuild.mjs .",
48
+ "build:gir-types": "ts-for-gir generate --externalDeps --allowMissingDeps --girDirectories=../sqlite-native-linux-x64/prebuilds/linux-x64 --girDirectories=/usr/share/gir-1.0 --modules=GjsifySqlite-1.0 --outdir=src/ts --npmScope=@girs --package=false --ignoreVersionConflicts=true",
49
+ "build:test": "gjsify run build:test:gjs",
50
+ "build:test:gjs": "gjsify build src/ts/test.mts --app gjs --outfile test.gjs.mjs",
51
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
52
+ "test:gjs": "gjsify run test.gjs.mjs"
53
+ },
54
+ "keywords": [
55
+ "gjs",
56
+ "sqlite",
57
+ "sqlite3",
58
+ "database",
59
+ "prepared-statement",
60
+ "vala",
61
+ "native"
62
+ ],
63
+ "dependencies": {
64
+ "@girs/gjs": "5.4.0",
65
+ "@girs/glib-2.0": "5.4.0",
66
+ "@girs/gobject-2.0": "5.4.0",
67
+ "@gjsify/utils": "0.64.12-next.281"
68
+ },
69
+ "devDependencies": {
70
+ "@gjsify/cli": "0.64.12-next.281",
71
+ "@gjsify/unit": "0.64.12-next.281",
72
+ "@ts-for-gir/cli": "^4.1.0",
73
+ "@types/node": "^25.9.2",
74
+ "typescript": "^6.0.3"
75
+ },
76
+ "license": "MIT",
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "git+https://github.com/gjsify/gjsify.git",
80
+ "directory": "packages/node/sqlite-native"
81
+ },
82
+ "bugs": {
83
+ "url": "https://github.com/gjsify/gjsify/issues"
84
+ },
85
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/node/sqlite-native#readme",
86
+ "optionalDependencies": {
87
+ "@gjsify/sqlite-native-darwin-arm64": "0.64.12-next.281",
88
+ "@gjsify/sqlite-native-darwin-x64": "0.64.12-next.281",
89
+ "@gjsify/sqlite-native-linux-arm64": "0.64.12-next.281",
90
+ "@gjsify/sqlite-native-linux-ppc64": "0.64.12-next.281",
91
+ "@gjsify/sqlite-native-linux-riscv64": "0.64.12-next.281",
92
+ "@gjsify/sqlite-native-linux-s390x": "0.64.12-next.281",
93
+ "@gjsify/sqlite-native-linux-x64": "0.64.12-next.281"
94
+ },
95
+ "gitHead": "c4d2e266c0d9849860b9828c14ebfe37b6d493b8"
96
+ }
@@ -0,0 +1,316 @@
1
+ /*
2
+ * GjsifySqlite.Database — a libsqlite3 connection, introspectable.
3
+ *
4
+ * This bridge is DELIBERATELY THIN. Everything `node:sqlite` promises on top of
5
+ * sqlite — statement caching, the `DatabaseSync`/`StatementSync` surface, the
6
+ * JS value coercions, `readBigInts`, the error class hierarchy — belongs to
7
+ * `@gjsify/sqlite` and lives in TypeScript. What cannot live there is the part
8
+ * that needs a C pointer: a prepared `sqlite3_stmt` with bindings and a column
9
+ * cursor. So this file exposes exactly that and nothing it can avoid.
10
+ *
11
+ * Errors travel as a GLib.Error in the {@link GjsifySqlite.SqliteError} domain
12
+ * whose `code` is sqlite's EXTENDED result code (2067, not 19) — GJS surfaces
13
+ * that unchanged on the thrown exception, which is the only reason the JS layer
14
+ * can rebuild Node's `err.errcode` without a second round trip.
15
+ */
16
+
17
+ namespace GjsifySqlite {
18
+
19
+ /**
20
+ * The error domain every failure here is reported in.
21
+ *
22
+ * The enumerated member is a formality: the `code` carried by a thrown
23
+ * error is never FAILED but sqlite's own extended result code, so that the
24
+ * JS layer sees what `sqlite3_extended_errcode()` saw. Only the DOMAIN is
25
+ * load-bearing — see {@link sqlite_error}.
26
+ */
27
+ public errordomain SqliteError {
28
+ FAILED
29
+ }
30
+
31
+ /**
32
+ * Mint an error in the {@link SqliteError} domain carrying @code.
33
+ *
34
+ * Vala offers no way to name an errordomain's quark, and its `throw new
35
+ * SqliteError.FAILED (…)` form can only produce the enumerated code — which
36
+ * is the one value that tells the JS layer nothing. Constructing the error
37
+ * and then overwriting its public `code` field is how both halves are had
38
+ * at once: the domain Vala generated, and sqlite's own result code.
39
+ */
40
+ internal SqliteError sqlite_error (int code, string message) {
41
+ var error = new SqliteError.FAILED ("%s", message);
42
+ error.code = code;
43
+ return error;
44
+ }
45
+
46
+ /** How {@link Database.open} should treat a missing or read-only file. */
47
+ public enum OpenMode {
48
+ /** Open an existing database for reading; fail if it does not exist. */
49
+ READONLY,
50
+ /** Open an existing database for writing; fail if it does not exist. */
51
+ READWRITE,
52
+ /** Open for writing, creating the database when it does not exist. */
53
+ READWRITE_CREATE
54
+ }
55
+
56
+ /** What {@link Statement.step} found. Anything else is thrown. */
57
+ public enum StepResult {
58
+ /** A row is available; the column accessors may be read. */
59
+ ROW,
60
+ /** The statement ran to completion and has no (more) rows. */
61
+ DONE
62
+ }
63
+
64
+ /** The storage class of a value in the current row. */
65
+ public enum ColumnType {
66
+ INTEGER,
67
+ FLOAT,
68
+ TEXT,
69
+ BLOB,
70
+ NULL
71
+ }
72
+
73
+ /**
74
+ * A connection to a SQLite database.
75
+ *
76
+ * Obtained from {@link Database.open}; there is no public constructor,
77
+ * because an instance without a handle has no useful state.
78
+ */
79
+ public class Database : GLib.Object {
80
+
81
+ private Sqlite.Database? handle = null;
82
+
83
+ /**
84
+ * Every statement prepared from this connection and not yet closed.
85
+ *
86
+ * Entries are UNOWNED: the reference runs the other way, each
87
+ * {@link Statement} holding a strong ref on its database. That
88
+ * direction is what makes the teardown order safe — a connection
89
+ * cannot be finalized while a statement can still reach its handle —
90
+ * and this list exists only so {@link close} can reach the statements
91
+ * it must finalize first.
92
+ */
93
+ private GLib.List<unowned Statement> live = new GLib.List<unowned Statement> ();
94
+
95
+ /** Whether the connection is still usable. */
96
+ public bool is_open {
97
+ get { return this.handle != null; }
98
+ }
99
+
100
+ /**
101
+ * ROWID of the most recent successful INSERT, or 0 when there was none
102
+ * — and 0 on a closed connection, since a property cannot throw.
103
+ */
104
+ public int64 last_insert_rowid {
105
+ get {
106
+ if (this.handle == null) {
107
+ return 0;
108
+ }
109
+ return this.handle.last_insert_rowid ();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Rows changed by the most recent INSERT/UPDATE/DELETE.
115
+ *
116
+ * `sqlite3_changes64`, not `sqlite3_changes`: the 32-bit one truncates
117
+ * a bulk UPDATE's count silently, and a wrong number is worse here than
118
+ * no number.
119
+ */
120
+ public int64 changes {
121
+ get {
122
+ if (this.handle == null) {
123
+ return 0;
124
+ }
125
+ return GjsifySqliteCompat.changes64 (this.handle);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Whether an explicit transaction is open.
131
+ *
132
+ * `sqlite3_get_autocommit()` answers the inverse — autocommit is ON
133
+ * exactly when no transaction is pending — so the sense is flipped here
134
+ * rather than in every caller.
135
+ */
136
+ public bool in_transaction {
137
+ get {
138
+ if (this.handle == null) {
139
+ return false;
140
+ }
141
+ return this.handle.get_autocommit () == 0;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Open a database file.
147
+ *
148
+ * @param path filesystem path, `:memory:`, or a URI when @uri
149
+ * @param mode which of the three useful `SQLITE_OPEN_*` combinations
150
+ * @param uri interpret @path as a `file:` URI with query parameters
151
+ */
152
+ public static Database open (string path,
153
+ OpenMode mode = OpenMode.READWRITE_CREATE,
154
+ bool uri = false) throws SqliteError {
155
+ int flags;
156
+ switch (mode) {
157
+ case OpenMode.READONLY:
158
+ flags = Sqlite.OPEN_READONLY;
159
+ break;
160
+ case OpenMode.READWRITE:
161
+ flags = Sqlite.OPEN_READWRITE;
162
+ break;
163
+ default:
164
+ flags = Sqlite.OPEN_READWRITE | Sqlite.OPEN_CREATE;
165
+ break;
166
+ }
167
+ if (uri) {
168
+ flags |= Sqlite.OPEN_URI;
169
+ }
170
+
171
+ Sqlite.Database opened;
172
+ int rc = Sqlite.Database.open_v2 (path, out opened, flags, null);
173
+
174
+ if (rc != Sqlite.OK) {
175
+ // sqlite hands back a handle even for most failures, and it is
176
+ // the only thing that knows WHY. Reading the message off it
177
+ // before it goes out of scope (where the vapi's free function
178
+ // closes it) is the difference between "unable to open database
179
+ // file" and bare "error 14".
180
+ string message;
181
+ int code;
182
+ if (opened != null) {
183
+ message = opened.errmsg ();
184
+ code = GjsifySqliteCompat.extended_errcode (opened);
185
+ } else {
186
+ message = GjsifySqliteCompat.errstr (rc);
187
+ code = rc;
188
+ }
189
+ throw sqlite_error ((code & 0xff) == rc ? code : rc, message);
190
+ }
191
+
192
+ // Without this, errcode() and extended_errcode() answer the same
193
+ // primary code and the JS layer can never tell SQLITE_CONSTRAINT
194
+ // from SQLITE_CONSTRAINT_UNIQUE.
195
+ opened.extended_result_codes (1);
196
+
197
+ var db = new Database ();
198
+ db.handle = (owned) opened;
199
+ return db;
200
+ }
201
+
202
+ /**
203
+ * Run one or more statements, discarding any rows.
204
+ *
205
+ * This is `sqlite3_exec`, so it accepts a whole script — the JS layer
206
+ * uses it for `DatabaseSync.exec()`.
207
+ */
208
+ public void exec (string sql) throws SqliteError {
209
+ unowned Sqlite.Database h = this.require_handle ();
210
+ string? message = null;
211
+ int rc = h.exec (sql, null, out message);
212
+ if (rc != Sqlite.OK) {
213
+ throw this.make_error (rc, message);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Compile the FIRST statement in @sql.
219
+ *
220
+ * Text after that statement is ignored, exactly as `sqlite3_prepare_v2`
221
+ * ignores it; callers wanting a script want {@link exec}.
222
+ */
223
+ public Statement prepare (string sql) throws SqliteError {
224
+ unowned Sqlite.Database h = this.require_handle ();
225
+
226
+ Sqlite.Statement compiled;
227
+ int rc = h.prepare_v2 (sql, -1, out compiled);
228
+ if (rc != Sqlite.OK) {
229
+ throw this.make_error (rc);
230
+ }
231
+ if (compiled == null) {
232
+ // SQLITE_OK with a null statement is how sqlite reports input
233
+ // that held only whitespace or a comment. Node raises here, so
234
+ // the bridge does too rather than hand back a statement whose
235
+ // every method would misuse-fail.
236
+ throw sqlite_error (Sqlite.ERROR,
237
+ "the SQL text contains no statement");
238
+ }
239
+
240
+ return new Statement (this, (owned) compiled);
241
+ }
242
+
243
+ /**
244
+ * Close the connection. Idempotent.
245
+ *
246
+ * Every statement still open is finalized FIRST: the vapi binds the
247
+ * plain `sqlite3_close`, which refuses a connection with outstanding
248
+ * statements and would leave the handle leaked with only a warning.
249
+ * Draining the list here is what lets this bridge do without
250
+ * `sqlite3_close_v2`.
251
+ */
252
+ public void close () {
253
+ if (this.handle == null) {
254
+ return;
255
+ }
256
+
257
+ // Emptied before the loop so that each statement's unregister()
258
+ // becomes a no-op and cannot mutate a list being walked.
259
+ GLib.List<unowned Statement> pending = (owned) this.live;
260
+ this.live = new GLib.List<unowned Statement> ();
261
+
262
+ foreach (unowned Statement stmt in pending) {
263
+ stmt.close ();
264
+ }
265
+
266
+ this.handle = null;
267
+ }
268
+
269
+ internal void register (Statement stmt) {
270
+ this.live.prepend (stmt);
271
+ }
272
+
273
+ internal void unregister (Statement stmt) {
274
+ this.live.remove (stmt);
275
+ }
276
+
277
+ /**
278
+ * Build — not throw — the error for a failed call on this connection.
279
+ *
280
+ * Returning it keeps Vala's flow analysis satisfied at the call sites,
281
+ * which all spell `throw this.make_error (rc)` and so still read as the
282
+ * exits they are.
283
+ */
284
+ internal SqliteError make_error (int rc, string? message = null) {
285
+ int code = rc;
286
+ string text;
287
+
288
+ if (this.handle != null) {
289
+ // The extended code belongs to the CONNECTION, not to the call,
290
+ // so it only describes this failure when its low byte is the
291
+ // primary code the call just returned.
292
+ int extended = GjsifySqliteCompat.extended_errcode (this.handle);
293
+ if ((extended & 0xff) == rc) {
294
+ code = extended;
295
+ }
296
+ }
297
+
298
+ if (message != null) {
299
+ text = message;
300
+ } else if (this.handle != null) {
301
+ text = this.handle.errmsg ();
302
+ } else {
303
+ text = GjsifySqliteCompat.errstr (rc);
304
+ }
305
+
306
+ return sqlite_error (code, text);
307
+ }
308
+
309
+ private unowned Sqlite.Database require_handle () throws SqliteError {
310
+ if (this.handle == null) {
311
+ throw sqlite_error (Sqlite.MISUSE, "database is not open");
312
+ }
313
+ return this.handle;
314
+ }
315
+ }
316
+ }
@@ -0,0 +1,77 @@
1
+ /*
2
+ * sqlite-compat.vapi — the libsqlite3 entry points Vala 0.56's bundled
3
+ * `sqlite3.vapi` does not declare.
4
+ *
5
+ * WHY THIS FILE EXISTS
6
+ * ────────────────────
7
+ * `sqlite3.vapi` ships with Vala and covers most of the C API, but it was
8
+ * written against sqlite 3.4 and has not tracked every addition since. Four
9
+ * symbols this bridge needs are missing from it, and each has a reason it
10
+ * cannot be worked around:
11
+ *
12
+ * • sqlite3_changes64 — the vapi only has `changes()`, which returns
13
+ * `int`. `node:sqlite` reports `changes` as a
14
+ * BigInt-capable count, so truncating at 2^31
15
+ * would be a silent wrong answer on a bulk
16
+ * UPDATE. Added in sqlite 3.37 (2021-11).
17
+ * • sqlite3_extended_errcode — `errcode()` answers the PRIMARY result code
18
+ * (19 = SQLITE_CONSTRAINT) regardless of
19
+ * `extended_result_codes()`. Node reports the
20
+ * extended one (2067 = …_CONSTRAINT_UNIQUE) on
21
+ * `err.errcode`, and the JS layer can only
22
+ * build that if it travels out of here intact.
23
+ * • sqlite3_errstr — the only way to describe a result code when
24
+ * there is no connection to ask (an open that
25
+ * failed before a handle existed).
26
+ * • sqlite3_column_decltype — declared column type, needed so the JS layer
27
+ * can apply sqlite's type affinity rules.
28
+ *
29
+ * NOT declared here, deliberately: `sqlite3_column_origin_name`. It exists
30
+ * only in a libsqlite3 built with SQLITE_ENABLE_COLUMN_METADATA, which the
31
+ * macOS system library is not. Referencing it would link on Fedora and leave
32
+ * the darwin prebuild with an undefined symbol — found by GI, refused by the
33
+ * loader, which docs/prebuilds.md calls out as the silent failure mode.
34
+ *
35
+ * `sqlite3_close_v2` is likewise absent: {@link GjsifySqlite.Database.close}
36
+ * finalizes every live statement before it drops the handle, so the plain
37
+ * `sqlite3_close` the vapi already binds as the compact class's free function
38
+ * never meets an outstanding statement.
39
+ */
40
+
41
+ [CCode (cheader_filename = "sqlite3.h")]
42
+ namespace GjsifySqliteCompat {
43
+
44
+ /**
45
+ * Rows modified by the most recent statement, as int64.
46
+ *
47
+ * sqlite >= 3.37. The build asserts its presence at configure time rather
48
+ * than letting the link fail (see meson.build).
49
+ */
50
+ [CCode (cname = "sqlite3_changes64")]
51
+ public int64 changes64 (Sqlite.Database db);
52
+
53
+ /**
54
+ * The EXTENDED result code of the most recent failed call on @db — the
55
+ * value `node:sqlite` surfaces as `err.errcode`.
56
+ */
57
+ [CCode (cname = "sqlite3_extended_errcode")]
58
+ public int extended_errcode (Sqlite.Database db);
59
+
60
+ /**
61
+ * English text for a result code, with no connection involved.
62
+ *
63
+ * The string is owned by libsqlite3 and is valid for the process lifetime.
64
+ */
65
+ [CCode (cname = "sqlite3_errstr")]
66
+ public unowned string errstr (int result_code);
67
+
68
+ /**
69
+ * Declared type of a result column as written in the CREATE TABLE
70
+ * statement, or %null for an expression column.
71
+ *
72
+ * The string is owned by the statement and is invalidated by
73
+ * `sqlite3_finalize`; callers copy before returning it.
74
+ */
75
+ [CCode (cname = "sqlite3_column_decltype")]
76
+ public unowned string? column_decltype (Sqlite.Statement stmt, int column);
77
+ }
@@ -0,0 +1,285 @@
1
+ /*
2
+ * GjsifySqlite.Statement — one compiled `sqlite3_stmt`, introspectable.
3
+ *
4
+ * Parameters are addressed by their 1-BASED sqlite index throughout, never by
5
+ * name: resolving `:foo` to an index is `bind_parameter_index()`, which is
6
+ * exposed so the JS layer can do that mapping once per statement instead of
7
+ * once per bind.
8
+ */
9
+
10
+ namespace GjsifySqlite {
11
+
12
+ /**
13
+ * A prepared statement, obtained from {@link Database.prepare}.
14
+ *
15
+ * The instance holds a STRONG reference to its database. That is what makes
16
+ * the object graph safe in a garbage-collected host: GJS finalizes in no
17
+ * order anyone can predict, and without the ref a collected `Database`
18
+ * would close the connection under a statement that is still reachable from
19
+ * JS. With it, the connection outlives every statement by construction.
20
+ */
21
+ public class Statement : GLib.Object {
22
+
23
+ private Database? owner;
24
+ private Sqlite.Statement? stmt;
25
+
26
+ internal Statement (Database owner, owned Sqlite.Statement stmt) {
27
+ this.owner = owner;
28
+ this.stmt = (owned) stmt;
29
+ owner.register (this);
30
+ }
31
+
32
+ ~Statement () {
33
+ this.release ();
34
+ }
35
+
36
+ /** Whether the statement is still compiled and usable. */
37
+ public bool is_valid {
38
+ get { return this.stmt != null; }
39
+ }
40
+
41
+ /** Number of result columns; 0 for a statement that returns no rows. */
42
+ public int column_count {
43
+ get {
44
+ if (this.stmt == null) {
45
+ return 0;
46
+ }
47
+ return this.stmt.column_count ();
48
+ }
49
+ }
50
+
51
+ /** Highest 1-based parameter index used by the statement. */
52
+ public int bind_parameter_count {
53
+ get {
54
+ if (this.stmt == null) {
55
+ return 0;
56
+ }
57
+ return this.stmt.bind_parameter_count ();
58
+ }
59
+ }
60
+
61
+ /** The SQL text this statement was compiled from. */
62
+ public string? sql {
63
+ owned get {
64
+ if (this.stmt == null) {
65
+ return null;
66
+ }
67
+ return this.stmt.sql ();
68
+ }
69
+ }
70
+
71
+ /** The SQL text with current bindings substituted in, for diagnostics. */
72
+ public string? expanded_sql {
73
+ owned get {
74
+ if (this.stmt == null) {
75
+ return null;
76
+ }
77
+ return this.stmt.expanded_sql ();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Finalize the statement. Idempotent, and safe after the database was
83
+ * closed — {@link Database.close} calls exactly this on every statement
84
+ * it still knows about.
85
+ */
86
+ public void close () {
87
+ this.release ();
88
+ }
89
+
90
+ public void bind_null (int index) throws SqliteError {
91
+ this.check (this.require_stmt ().bind_null (index));
92
+ }
93
+
94
+ public void bind_int64 (int index, int64 value) throws SqliteError {
95
+ this.check (this.require_stmt ().bind_int64 (index, value));
96
+ }
97
+
98
+ public void bind_double (int index, double value) throws SqliteError {
99
+ this.check (this.require_stmt ().bind_double (index, value));
100
+ }
101
+
102
+ public void bind_text (int index, string value) throws SqliteError {
103
+ // The vapi's `bind_text` overload takes an OWNED string and hands
104
+ // sqlite `g_free` as the destructor, so the copy outlives this
105
+ // frame. The raw binding's default is SQLITE_STATIC, which would
106
+ // leave sqlite pointing at a freed buffer.
107
+ this.check (this.require_stmt ().bind_text (index, value));
108
+ }
109
+
110
+ /**
111
+ * Bind a BLOB.
112
+ *
113
+ * The bytes are COPIED. SQLITE_TRANSIENT — the sentinel that asks
114
+ * sqlite to copy for us — is a cast of -1 to a function pointer that
115
+ * Vala has no way to spell, and the alternative the binding defaults to
116
+ * (SQLITE_STATIC) would have sqlite read @value's buffer long after
117
+ * this frame and possibly the GLib.Bytes are gone.
118
+ */
119
+ public void bind_blob (int index, GLib.Bytes value) throws SqliteError {
120
+ unowned Sqlite.Statement s = this.require_stmt ();
121
+ size_t size = value.get_size ();
122
+
123
+ if (size == 0) {
124
+ // sqlite3_bind_blob with a null pointer binds NULL, not an
125
+ // empty blob, and a zero-length GLib.Bytes has no data pointer.
126
+ // bind_zeroblob(0) is how sqlite spells "a BLOB of no bytes".
127
+ this.check (s.bind_zeroblob (index, 0));
128
+ return;
129
+ }
130
+
131
+ if (size > int.MAX) {
132
+ throw sqlite_error (Sqlite.TOOBIG,
133
+ "blob exceeds the maximum size sqlite can bind");
134
+ }
135
+
136
+ void* copy = GLib.malloc (size);
137
+ GLib.Memory.copy (copy, value.get_data (), size);
138
+ // sqlite invokes the destructor even when the bind itself fails, so
139
+ // the failing path below leaks nothing.
140
+ this.check (s.bind_blob (index, copy, (int) size, GLib.g_free));
141
+ }
142
+
143
+ /** 1-based index of a named parameter, or 0 when there is no such name. */
144
+ public int bind_parameter_index (string name) throws SqliteError {
145
+ return this.require_stmt ().bind_parameter_index (name);
146
+ }
147
+
148
+ /** Name of a 1-based parameter, or %null for a positional `?`. */
149
+ public string? bind_parameter_name (int index) throws SqliteError {
150
+ unowned string? name = this.require_stmt ().bind_parameter_name (index);
151
+ return name;
152
+ }
153
+
154
+ /** Reset every binding to NULL without recompiling the statement. */
155
+ public void clear_bindings () throws SqliteError {
156
+ this.check (this.require_stmt ().clear_bindings ());
157
+ }
158
+
159
+ /**
160
+ * Advance the cursor.
161
+ *
162
+ * @return ROW when column values are available, DONE at the end
163
+ */
164
+ public StepResult step () throws SqliteError {
165
+ int rc = this.require_stmt ().step ();
166
+ if (rc == Sqlite.ROW) {
167
+ return StepResult.ROW;
168
+ }
169
+ if (rc == Sqlite.DONE) {
170
+ return StepResult.DONE;
171
+ }
172
+ throw this.owner.make_error (rc);
173
+ }
174
+
175
+ /**
176
+ * Rewind the statement so it can run again. Bindings are KEPT, matching
177
+ * `sqlite3_reset`; {@link clear_bindings} drops them.
178
+ */
179
+ public void reset () throws SqliteError {
180
+ // sqlite3_reset answers the error code of the statement's LAST run,
181
+ // which the caller has already been told about. Re-raising it here
182
+ // would report the same failure twice, so only the reset itself is
183
+ // checked — and it cannot fail on its own.
184
+ this.require_stmt ().reset ();
185
+ }
186
+
187
+ /** Name of a 0-based result column, as the SELECT labelled it. */
188
+ public string? column_name (int index) throws SqliteError {
189
+ unowned string? name = this.require_stmt ().column_name (index);
190
+ return name;
191
+ }
192
+
193
+ /**
194
+ * The column's declared type from CREATE TABLE, or %null for an
195
+ * expression column. The JS layer needs it to apply type affinity.
196
+ */
197
+ public string? column_decltype (int index) throws SqliteError {
198
+ unowned string? decl = GjsifySqliteCompat.column_decltype (this.require_stmt (), index);
199
+ return decl;
200
+ }
201
+
202
+ /** Storage class of the value in the current row. */
203
+ public ColumnType column_type (int index) throws SqliteError {
204
+ int type = this.require_stmt ().column_type (index);
205
+
206
+ // if/else rather than a switch: these are C macros reaching Vala as
207
+ // vapi constants, not case labels it will fold.
208
+ if (type == Sqlite.INTEGER) {
209
+ return ColumnType.INTEGER;
210
+ }
211
+ if (type == Sqlite.FLOAT) {
212
+ return ColumnType.FLOAT;
213
+ }
214
+ if (type == Sqlite.TEXT) {
215
+ return ColumnType.TEXT;
216
+ }
217
+ if (type == Sqlite.BLOB) {
218
+ return ColumnType.BLOB;
219
+ }
220
+ return ColumnType.NULL;
221
+ }
222
+
223
+ public int64 column_int64 (int index) throws SqliteError {
224
+ return this.require_stmt ().column_int64 (index);
225
+ }
226
+
227
+ public double column_double (int index) throws SqliteError {
228
+ return this.require_stmt ().column_double (index);
229
+ }
230
+
231
+ public string? column_text (int index) throws SqliteError {
232
+ unowned string? text = this.require_stmt ().column_text (index);
233
+ return text;
234
+ }
235
+
236
+ /**
237
+ * The column's bytes, copied into a fresh GLib.Bytes.
238
+ *
239
+ * A copy is not optional: sqlite's pointer is invalidated by the next
240
+ * step/reset/finalize, and GJS hands a GLib.Bytes to JS that may well
241
+ * outlive all three.
242
+ */
243
+ public GLib.Bytes column_blob (int index) throws SqliteError {
244
+ unowned Sqlite.Statement s = this.require_stmt ();
245
+
246
+ // Order matters: sqlite3_column_blob may convert the value, and
247
+ // sqlite3_column_bytes only reports the post-conversion length once
248
+ // it has. Asking for the length first can answer for the old type.
249
+ void* data = s.column_blob (index);
250
+ int size = s.column_bytes (index);
251
+
252
+ if (data == null || size <= 0) {
253
+ return new GLib.Bytes (new uint8[0]);
254
+ }
255
+
256
+ uint8[] copy = new uint8[size];
257
+ GLib.Memory.copy (copy, data, (size_t) size);
258
+ return new GLib.Bytes.take ((owned) copy);
259
+ }
260
+
261
+ private void release () {
262
+ // Dropping the field runs the vapi's free function, sqlite3_finalize.
263
+ this.stmt = null;
264
+
265
+ if (this.owner != null) {
266
+ this.owner.unregister (this);
267
+ this.owner = null;
268
+ }
269
+ }
270
+
271
+ private unowned Sqlite.Statement require_stmt () throws SqliteError {
272
+ if (this.stmt == null) {
273
+ throw sqlite_error (Sqlite.MISUSE, "statement is finalized");
274
+ }
275
+ return this.stmt;
276
+ }
277
+
278
+ private void check (int rc) throws SqliteError {
279
+ if (rc == Sqlite.OK) {
280
+ return;
281
+ }
282
+ throw this.owner.make_error (rc);
283
+ }
284
+ }
285
+ }