@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 +98 -2
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/types/index.d.ts +28 -0
- package/lib/types/sqlite.gjs.spec.d.ts +2 -0
- package/meson.build +106 -0
- package/package.json +95 -5
- package/src/vala/database.vala +316 -0
- package/src/vala/sqlite-compat.vapi +77 -0
- package/src/vala/statement.vala +285 -0
package/README.md
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gjsify/sqlite-native
|
|
2
2
|
|
|
3
|
-
|
|
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};
|
package/lib/esm/index.js
ADDED
|
@@ -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;
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|