From 83b61cc436a5e79d0c681dacb894c206a89ba3c9 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Mon, 22 Jan 2024 15:39:06 -0500 Subject: [PATCH] Use sqlite cache of pantry data We had to vendor the deno sqlite3 library as it had no other direct way to allow us to configure the path to the sqlite library at runtime. To accomplish this we had to modify the sources somewhat also :/ --- .github/deno-to-node.ts | 4 +- .github/workflows/ci.yml | 2 +- deno.json | 2 +- src/hooks/usePantry.test.ts | 7 + src/hooks/usePantry.ts | 53 +- src/hooks/useSync.test.ts | 67 +- src/hooks/useSync.ts | 27 +- src/hooks/useSyncCache.node.ts | 31 + src/hooks/useSyncCache.test.ts | 27 + src/hooks/useSyncCache.ts | 179 +++++ src/plumbing/which.ts | 20 +- vendor/README.md | 6 + vendor/sqlite3@0.10.0/mod.ts | 3 + vendor/sqlite3@0.10.0/src/constants.ts | 67 ++ vendor/sqlite3@0.10.0/src/database.ts | 606 +++++++++++++++++ vendor/sqlite3@0.10.0/src/ffi.ts | 592 ++++++++++++++++ vendor/sqlite3@0.10.0/src/statement.ts | 891 +++++++++++++++++++++++++ vendor/sqlite3@0.10.0/src/util.ts | 48 ++ 18 files changed, 2588 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useSyncCache.node.ts create mode 100644 src/hooks/useSyncCache.test.ts create mode 100644 src/hooks/useSyncCache.ts create mode 100644 vendor/README.md create mode 100644 vendor/sqlite3@0.10.0/mod.ts create mode 100644 vendor/sqlite3@0.10.0/src/constants.ts create mode 100644 vendor/sqlite3@0.10.0/src/database.ts create mode 100644 vendor/sqlite3@0.10.0/src/ffi.ts create mode 100644 vendor/sqlite3@0.10.0/src/statement.ts create mode 100644 vendor/sqlite3@0.10.0/src/util.ts diff --git a/.github/deno-to-node.ts b/.github/deno-to-node.ts index 008b348..791aa5e 100755 --- a/.github/deno-to-node.ts +++ b/.github/deno-to-node.ts @@ -33,7 +33,9 @@ await build({ mappings: { "https://deno.land/x/is_what@v4.1.15/src/index.ts": "is-what", "https://deno.land/x/outdent@v0.8.0/mod.ts": "outdent", - "./src/utils/flock.deno.ts": "./src/utils/flock.node.ts" + "./src/utils/flock.deno.ts": "./src/utils/flock.node.ts", + "./src/hooks/useSyncCache.ts": "./src/hooks/useSyncCache.node.ts", + "./src/hooks/useSyncCache.test.ts": "./src/hooks/useCache.test.ts" // no other easy way to skip the test }, package: { name: "libpkgx", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82b06fe..926f901 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: with: path: src - uses: denoland/setup-deno@v1 - - run: deno run --no-config --unstable src/mod.ts + - run: deno run --no-config --unstable --allow-all src/mod.ts dnt: runs-on: ${{ matrix.os }} diff --git a/deno.json b/deno.json index 45b775c..a5461a1 100644 --- a/deno.json +++ b/deno.json @@ -12,7 +12,7 @@ }, "pkgx": "deno~1.39 npm", "tasks": { - "test": "deno test --parallel --unstable --allow-env --allow-read --allow-net=dist.pkgx.dev,github.com,codeload.github.com --allow-write --allow-run=tar,uname,/bin/sh,foo,'C:\\Windows\\system32\\cmd.exe'", + "test": "deno test --parallel --unstable --allow-env --allow-read --allow-ffi --allow-net=dist.pkgx.dev,github.com,codeload.github.com --allow-write --allow-run=tar,uname,/bin/sh,foo,'C:\\Windows\\system32\\cmd.exe'", "typecheck": "deno check --unstable ./mod.ts", "dnt": ".github/deno-to-node.ts" }, diff --git a/src/hooks/usePantry.test.ts b/src/hooks/usePantry.test.ts index 99609d0..a49ef59 100644 --- a/src/hooks/usePantry.test.ts +++ b/src/hooks/usePantry.test.ts @@ -85,3 +85,10 @@ Deno.test("validatePackageRequirement - number constraint", () => { const result = validatePackageRequirement("pkgx.sh/test", 1) assertEquals(result?.constraint.toString(), "^1") }) + +Deno.test("find", async () => { + useTestConfig() + const foo = await usePantry().find("python@3.11") + assertEquals(foo.length, 1) + assertEquals(foo[0].project, "python.org") +}) \ No newline at end of file diff --git a/src/hooks/usePantry.ts b/src/hooks/usePantry.ts index 8b96da2..25a1dc7 100644 --- a/src/hooks/usePantry.ts +++ b/src/hooks/usePantry.ts @@ -1,10 +1,12 @@ import { is_what, PlainObject } from "../deps.ts" const { isNumber, isPlainObject, isString, isArray, isPrimitive, isBoolean } = is_what import { Package, Installation, PackageRequirement } from "../types.ts" +import { provides as cache_provides, available as cache_available, runtime_env as cache_runtime_env, companions as cache_companions, dependencies as cache_dependencies } from "./useSyncCache.ts"; import SemVer, * as semver from "../utils/semver.ts" import useMoustaches from "./useMoustaches.ts" import { PkgxError } from "../utils/error.ts" import { validate } from "../utils/misc.ts" +import * as pkgutils from "../utils/pkg.ts" import useConfig from "./useConfig.ts" import host from "../utils/host.ts" import Path from "../utils/Path.ts" @@ -45,6 +47,7 @@ export class PantryNotFoundError extends PantryError { export default function usePantry() { const prefix = useConfig().data.join("pantry/projects") + const is_cache_available = cache_available() && pantry_paths().length == 1 async function* ls(): AsyncGenerator { const seen = new Set() @@ -78,11 +81,23 @@ export default function usePantry() { throw new PackageNotFoundError(project) })() - const companions = async () => parse_pkgs_node((await yaml())["companions"]) + const companions = async () => { + if (is_cache_available) { + return await cache_companions(project) ?? parse_pkgs_node((await yaml())["companions"]) + } else { + return parse_pkgs_node((await yaml())["companions"]) + } + } const runtime_env = async (version: SemVer, deps: Installation[]) => { - const yml = await yaml() - const obj = validate.obj(yml["runtime"]?.["env"] ?? {}) + const obj = await (async () => { + if (is_cache_available) { + const cached = await cache_runtime_env(project) + if (cached) return cached + } + const yml = await yaml() + return validate.obj(yml["runtime"]?.["env"] ?? {}) + })() return expand_env_obj(obj, { project, version }, deps) } @@ -94,7 +109,13 @@ export default function usePantry() { return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`) } - const drydeps = async () => parse_pkgs_node((await yaml()).dependencies) + const drydeps = async () => { + if (is_cache_available) { + return await cache_dependencies(project) ?? parse_pkgs_node((await yaml()).dependencies) + } else { + return parse_pkgs_node((await yaml()).dependencies) + } + } const provides = async () => { let node = (await yaml())["provides"] @@ -164,6 +185,27 @@ export default function usePantry() { async function find(name: string) { type Foo = ReturnType & LsEntry + //lol FIXME + name = pkgutils.parse(name).project + + if (prefix.join(name).isDirectory()) { + const foo = project(name) + return [{...foo, project: name }] + } + + /// only use cache if PKGX_PANTRY_PATH is not set + if (is_cache_available) { + const cached = await cache_provides(name) + if (cached?.length) { + return cached.map(x => ({ + ...project(x), + project: x + })) + } + + // else we need to still check for display-names + } + name = name.toLowerCase() //TODO not very performant due to serial awaits @@ -234,7 +276,8 @@ export default function usePantry() { parse_pkgs_node, expand_env_obj, missing, - neglected + neglected, + pantry_paths } function pantry_paths(): Path[] { diff --git a/src/hooks/useSync.test.ts b/src/hooks/useSync.test.ts index 42195d9..ec99a1b 100644 --- a/src/hooks/useSync.test.ts +++ b/src/hooks/useSync.test.ts @@ -1,44 +1,53 @@ +import specimen, { _internals } from "./useSync.ts" import { useTestConfig } from "./useTestConfig.ts" +import * as mock from "deno/testing/mock.ts" import { assert } from "deno/assert/mod.ts" import usePantry from "./usePantry.ts" -import useSync from "./useSync.ts" // NOTE actually syncs from github // TODO unit tests should not do actual network calls, instead make an implementation suite Deno.test("useSync", async runner => { - await runner.step("w/o git", async () => { - const conf = useTestConfig({}) - usePantry().prefix.rm({ recursive: true }) // we need to delete the fixtured pantry - assert(conf.git === undefined) - await test() - }) - - await runner.step({ - name: "w/git", - ignore: Deno.build.os == 'windows' && !Deno.env.get("CI"), - async fn() { - const conf = useTestConfig({ PATH: "/usr/bin" }) + const stub = mock.stub(_internals, "cache", async () => {}) + + try { + await runner.step("w/o git", async () => { + const conf = useTestConfig({}) usePantry().prefix.rm({ recursive: true }) // we need to delete the fixtured pantry - assert(conf.git !== undefined) + assert(conf.git === undefined) await test() + }) - // test the “already cloned” code-path - await useSync() - } - }) - - async function test() { - let errord = false - try { - await usePantry().project("gnu.org/gcc").available() - } catch { - errord = true - } - assert(errord, `should be no pantry but there is! ${usePantry().prefix}`) + await runner.step({ + name: "w/git", + ignore: Deno.build.os == 'windows' && !Deno.env.get("CI"), + async fn() { + const conf = useTestConfig({ PATH: "/usr/bin" }) + usePantry().prefix.rm({ recursive: true }) // we need to delete the fixtured pantry + assert(conf.git !== undefined) + await test() + + // test the “already cloned” code-path + await specimen() + } + }) - await useSync() + async function test() { + let errord = false + try { + await usePantry().project("gnu.org/gcc").available() + } catch { + errord = true + } + assert(errord, `should be no pantry but there is! ${usePantry().prefix}`) - assert(await usePantry().project("gnu.org/gcc").available()) + await specimen() + + assert(await usePantry().project("gnu.org/gcc").available()) + } + + } finally { + stub.restore() } + }) diff --git a/src/hooks/useSync.ts b/src/hooks/useSync.ts index 55b4dfd..b1e4087 100644 --- a/src/hooks/useSync.ts +++ b/src/hooks/useSync.ts @@ -7,12 +7,14 @@ import useDownload from "./useDownload.ts" import usePantry from "./usePantry.ts" import useConfig from "./useConfig.ts" import Path from "../utils/Path.ts" +import useSyncCache from "./useSyncCache.ts"; //FIXME tar is fetched from PATH :/ we want control //FIXME run in general is not controllable since it delegates to the shell interface Logger { syncing(path: Path): void + caching(path: Path): void syncd(path: Path): void } @@ -23,6 +25,27 @@ export default async function(logger?: Logger) { const unflock = await flock(pantry_dir.mkdir('p')) + try { + await _internals.sync(pantry_dir) + try { + logger?.caching(pantry_dir) + await _internals.cache() + } catch (err) { + console.warn("failed to cache pantry") + console.error(err) + } + } finally { + await unflock() + } + + logger?.syncd(pantry_dir) +} + +export const _internals = { + sync, cache: useSyncCache +} + +async function sync(pantry_dir: Path) { try { //TODO if there was already a lock, just wait on it, don’t do the following stuff @@ -55,11 +78,7 @@ export default async function(logger?: Logger) { proc.close() - } finally { - await unflock() } - - logger?.syncd(pantry_dir) } //////////////////////// utils diff --git a/src/hooks/useSyncCache.node.ts b/src/hooks/useSyncCache.node.ts new file mode 100644 index 0000000..0c35178 --- /dev/null +++ b/src/hooks/useSyncCache.node.ts @@ -0,0 +1,31 @@ +// the sqlite lib we use only works in deno + +import { PackageRequirement } from "../../mod.ts"; + +export default async function() +{} + +export function provides(_program: string): string[] { + throw new Error() +} + +export function dependencies(_project: string): PackageRequirement[] { + throw new Error() +} + +export function completion(_prefix: string): string[] { + throw new Error() +} + +/// is the cache available? +export function available(): boolean { + return false +} + +export function companions(_project: string): PackageRequirement[] { + throw new Error() +} + +export function runtime_env(_project: string): Record { + throw new Error() +} diff --git a/src/hooks/useSyncCache.test.ts b/src/hooks/useSyncCache.test.ts new file mode 100644 index 0000000..b585e75 --- /dev/null +++ b/src/hooks/useSyncCache.test.ts @@ -0,0 +1,27 @@ +import specimen, { provides, dependencies, available, runtime_env, completion, companions } from "./useSyncCache.ts" +import { useTestConfig } from "./useTestConfig.ts" +import { assert, assertEquals } from "deno/assert/mod.ts" +import { _internals } from "./useSync.ts" +import usePantry from "./usePantry.ts" + +// NOTE actually syncs from github +// TODO unit tests should not do actual network calls, instead make an implementation suite + +Deno.test({ + name: "useSyncCache", + ignore: Deno.build.os == 'windows', + sanitizeResources: false, + async fn() { + useTestConfig() + await _internals.sync(usePantry().prefix.parent()) + await specimen() + + //TODO test better + assert(available()) + assertEquals((await provides('node'))?.[0], 'nodejs.org') + // assertEquals((await dependencies('nodejs.org'))?.length, 3) + assert(new Set(await completion('nod')).has("node")) + assertEquals((await companions("nodejs.org"))?.[0]?.project, "npmjs.com") + assert((await runtime_env("numpy.org"))?.["PYTHONPATH"]) + } +}) diff --git a/src/hooks/useSyncCache.ts b/src/hooks/useSyncCache.ts new file mode 100644 index 0000000..05c002d --- /dev/null +++ b/src/hooks/useSyncCache.ts @@ -0,0 +1,179 @@ +import { Database } from "../../vendor/sqlite3@0.10.0/mod.ts"; +import * as pkgutils from "../utils/pkg.ts"; +import usePantry from "./usePantry.ts"; +import useConfig from "./useConfig.ts"; + +export default async function() { + if (Deno.build.os == 'windows') return + + const path = useConfig().cache.join('pantry.db').rm() // delete it first so pantry instantiation doesn't use cache + const { ls, ...pantry } = usePantry() + + const sqlite3 = (await install_sqlite())?.string + if (!sqlite3) return + const db = new Database(path.string, { sqlite3 }) + + // unique or don’t insert what is already there or just dump tables first perhaps + + try { + await db.transaction(async () => { + db.exec(` + DROP TABLE IF EXISTS provides; + DROP TABLE IF EXISTS dependencies; + DROP TABLE IF EXISTS companions; + DROP TABLE IF EXISTS runtime_env; + CREATE TABLE provides ( + project TEXT, + program TEXT + ); + CREATE TABLE dependencies ( + project TEXT, + pkgspec TEXT + ); + CREATE TABLE companions ( + project TEXT, + pkgspec TEXT + ); + CREATE TABLE runtime_env ( + project TEXT, + envline TEXT + ); + CREATE INDEX idx_project ON provides(project); + CREATE INDEX idx_program ON provides(program); + CREATE INDEX idx_project_dependencies ON dependencies(project); + CREATE INDEX idx_project_companions ON companions(project); + `); + + for await (const pkg of ls()) { + if (!pkg.path.string.startsWith(pantry.prefix.string)) { + // don’t cache PKGX_PANTRY_PATH additions + continue; + } + + try { + const project = pantry.project(pkg.project) + const [programs, deps, companions, yaml] = await Promise.all([ + project.provides(), + project.runtime.deps(), + project.companions(), + project.yaml() + ]) + + for (const program of programs) { + db.exec(`INSERT INTO provides (project, program) VALUES ('${pkg.project}', '${program}');`); + } + + for (const dep of deps) { + db.exec(`INSERT INTO dependencies (project, pkgspec) VALUES ('${pkg.project}', '${pkgutils.str(dep)}')`); + } + + for (const companion of companions) { + db.exec(`INSERT INTO companions (project, pkgspec) VALUES ('${pkg.project}', '${pkgutils.str(companion)}')`); + } + + for (const [key, value] of Object.entries(yaml.runtime?.env ?? {})) { + db.exec(`INSERT INTO runtime_env (project, envline) VALUES ('${pkg.project}', '${key}=${value}')`); + } + } catch { + console.warn("corrupt yaml", pkg.path) + } + } + })() + } catch (err) { + path.rm() + throw err + } finally { + db.close(); + } +} + +export async function provides(program: string) { + const db = await _db() + if (!db) return + try { + return db.sql`SELECT project FROM provides WHERE program = ${program}`.map(x => x.project); + } finally { + db.close() + } +} + +export async function dependencies(project: string) { + const db = await _db() + if (!db) return + try { + return db.sql`SELECT pkgspec FROM dependencies WHERE project = ${project}`.map(x => pkgutils.parse(x.pkgspec)); + } finally { + db.close() + } +} + +export async function completion(prefix: string) { + const db = await _db() + try { + return db?.prepare(`SELECT program FROM provides WHERE program LIKE '${prefix}%'`).value<[string]>()!; + } finally { + db?.close() + } +} + +/// is the cache available? +export function available() { + if (Deno.build.os == 'windows') { + return false + } else { + const path = useConfig().cache.join('pantry.db') + return path.isFile() + } +} + +export async function companions(project: string) { + const db = await _db() + if (!db) return + try { + return db.sql`SELECT pkgspec FROM companions WHERE project = ${project}`.map(x => pkgutils.parse(x.pkgspec)); + } finally { + db.close() + } +} + +export async function runtime_env(project: string) { + const db = await _db() + if (!db) return + try { + const rv: Record = {} + for (const {envline: line} of db.sql`SELECT envline FROM runtime_env WHERE project = ${project}`) { + const [key, ...rest] = line.split("=") + rv[key] = rest.join('=') + } + return rv + } finally { + db.close() + } +} + +import useCellar from "./useCellar.ts" + +async function _db() { + if (Deno.build.os == 'windows') return + const path = useConfig().cache.join('pantry.db') + if (!path.isFile()) return + const sqlite = await useCellar().has({ project: "sqlite.org", constraint: new semver.Range('*') }) + if (!sqlite) return + const ext = host().platform == 'darwin' ? 'dylib' : 'so' + return new Database(path.string, {readonly: true, sqlite3: sqlite.path.join(`lib/libsqlite3.${ext}`).string}) +} + +import install from "../porcelain/install.ts" +import host from "../utils/host.ts"; +import Path from "../utils/Path.ts"; +import { semver } from "../../mod.ts"; + +async function install_sqlite(): Promise { + const foo = await install("sqlite.org") + for (const bar of foo) { + if (bar.pkg.project == 'sqlite.org') { + const ext = host().platform == 'darwin' ? 'dylib' : 'so' + return bar.path.join(`lib/libsqlite3.${ext}`) + } + } +} diff --git a/src/plumbing/which.ts b/src/plumbing/which.ts index c69d47d..a63568a 100644 --- a/src/plumbing/which.ts +++ b/src/plumbing/which.ts @@ -1,12 +1,12 @@ -import { PackageRequirement } from "../types.ts" +import { provides as cache_provides, available as cache_available } from "../hooks/useSyncCache.ts" import usePantry, { PantryError } from "../hooks/usePantry.ts" +import { PackageRequirement } from "../types.ts" import * as semver from "../utils/semver.ts" export type WhichResult = PackageRequirement & { shebang: string[] } - export default async function which(arg0: string, opts?: { providers?: boolean }): Promise; export default async function which(arg0: string, opts: { providers?: boolean, all: false }): Promise; export default async function which(arg0: string, opts: { providers?: boolean, all: true }): Promise; @@ -36,6 +36,19 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat const pantry = usePantry() let found: WhichResult[] = [] + + // don't use the cache if PKGX_PANTRY_PATH is set + if (cache_available()) { + const cached = await cache_provides(arg0) + if (cached) { + for (const project of cached) { + yield { project, constraint: new semver.Range("*"), shebang: [arg0] } + } + // NOTE probs wrong, but we need a rewrite + if (cached.length) return + } + } + const promises: Promise[] = [] for await (const entry of pantry.ls()) { @@ -92,9 +105,10 @@ async function *_which(arg0: string, opts: { providers: boolean }): AsyncGenerat } } + await Promise.all(promises) + // if we didn’t find anything yet then we have to wait on the promises // otherwise we can ignore them - await Promise.all(promises) for (const f of found) { yield f diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..c5c4bee --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,6 @@ +# sqlite3@0.10.0 + +vendored and modified to not download their binary of sqlite and instead be +customizable to use our own. + +https://github.com/denodrivers/sqlite3/issues/119 diff --git a/vendor/sqlite3@0.10.0/mod.ts b/vendor/sqlite3@0.10.0/mod.ts new file mode 100644 index 0000000..26b5bce --- /dev/null +++ b/vendor/sqlite3@0.10.0/mod.ts @@ -0,0 +1,3 @@ +export * from "./src/database.ts"; +export * from "./src/statement.ts"; +export { SqliteError } from "./src/util.ts"; diff --git a/vendor/sqlite3@0.10.0/src/constants.ts b/vendor/sqlite3@0.10.0/src/constants.ts new file mode 100644 index 0000000..aa67db7 --- /dev/null +++ b/vendor/sqlite3@0.10.0/src/constants.ts @@ -0,0 +1,67 @@ +// Result Codes +export const SQLITE3_OK = 0; +export const SQLITE3_ERROR = 1; +export const SQLITE3_INTERNAL = 2; +export const SQLITE3_PERM = 3; +export const SQLITE3_ABORT = 4; +export const SQLITE3_BUSY = 5; +export const SQLITE3_LOCKED = 6; +export const SQLITE3_NOMEM = 7; +export const SQLITE3_READONLY = 8; +export const SQLITE3_INTERRUPT = 9; +export const SQLITE3_IOERR = 10; +export const SQLITE3_CORRUPT = 11; +export const SQLITE3_NOTFOUND = 12; +export const SQLITE3_FULL = 13; +export const SQLITE3_CANTOPEN = 14; +export const SQLITE3_PROTOCOL = 15; +export const SQLITE3_EMPTY = 16; +export const SQLITE3_SCHEMA = 17; +export const SQLITE3_TOOBIG = 18; +export const SQLITE3_CONSTRAINT = 19; +export const SQLITE3_MISMATCH = 20; +export const SQLITE3_MISUSE = 21; +export const SQLITE3_NOLFS = 22; +export const SQLITE3_AUTH = 23; +export const SQLITE3_FORMAT = 24; +export const SQLITE3_RANGE = 25; +export const SQLITE3_NOTADB = 26; +export const SQLITE3_NOTICE = 27; +export const SQLITE3_WARNING = 28; +export const SQLITE3_ROW = 100; +export const SQLITE3_DONE = 101; + +// Open Flags +export const SQLITE3_OPEN_READONLY = 0x00000001; +export const SQLITE3_OPEN_READWRITE = 0x00000002; +export const SQLITE3_OPEN_CREATE = 0x00000004; +export const SQLITE3_OPEN_DELETEONCLOSE = 0x00000008; +export const SQLITE3_OPEN_EXCLUSIVE = 0x00000010; +export const SQLITE3_OPEN_AUTOPROXY = 0x00000020; +export const SQLITE3_OPEN_URI = 0x00000040; +export const SQLITE3_OPEN_MEMORY = 0x00000080; +export const SQLITE3_OPEN_MAIN_DB = 0x00000100; +export const SQLITE3_OPEN_TEMP_DB = 0x00000200; +export const SQLITE3_OPEN_TRANSIENT_DB = 0x00000400; +export const SQLITE3_OPEN_MAIN_JOURNAL = 0x00000800; +export const SQLITE3_OPEN_TEMP_JOURNAL = 0x00001000; +export const SQLITE3_OPEN_SUBJOURNAL = 0x00002000; +export const SQLITE3_OPEN_SUPER_JOURNAL = 0x00004000; +export const SQLITE3_OPEN_NONMUTEX = 0x00008000; +export const SQLITE3_OPEN_FULLMUTEX = 0x00010000; +export const SQLITE3_OPEN_SHAREDCACHE = 0x00020000; +export const SQLITE3_OPEN_PRIVATECACHE = 0x00040000; +export const SQLITE3_OPEN_WAL = 0x00080000; +export const SQLITE3_OPEN_NOFOLLOW = 0x01000000; + +// Prepare Flags +export const SQLITE3_PREPARE_PERSISTENT = 0x00000001; +export const SQLITE3_PREPARE_NORMALIZE = 0x00000002; +export const SQLITE3_PREPARE_NO_VTAB = 0x00000004; + +// Fundamental Datatypes +export const SQLITE_INTEGER = 1; +export const SQLITE_FLOAT = 2; +export const SQLITE_TEXT = 3; +export const SQLITE_BLOB = 4; +export const SQLITE_NULL = 5; diff --git a/vendor/sqlite3@0.10.0/src/database.ts b/vendor/sqlite3@0.10.0/src/database.ts new file mode 100644 index 0000000..d0e527e --- /dev/null +++ b/vendor/sqlite3@0.10.0/src/database.ts @@ -0,0 +1,606 @@ +import ffi from "./ffi.ts"; +import { deno } from "../../../src/deps.ts"; +import { + SQLITE3_OPEN_CREATE, + SQLITE3_OPEN_MEMORY, + SQLITE3_OPEN_READONLY, + SQLITE3_OPEN_READWRITE, + SQLITE_BLOB, + SQLITE_FLOAT, + SQLITE_INTEGER, + SQLITE_NULL, + SQLITE_TEXT, +} from "./constants.ts"; +import { readCstr, toCString, unwrap } from "./util.ts"; +import { RestBindParameters, Statement, STATEMENTS } from "./statement.ts"; +const { fromFileUrl } = deno + +/** Various options that can be configured when opening Database connection. */ +export interface DatabaseOpenOptions { + /** Whether to open database only in read-only mode. By default, this is false. */ + readonly?: boolean; + /** Whether to create a new database file at specified path if one does not exist already. By default this is true. */ + create?: boolean; + /** Raw SQLite C API flags. Specifying this ignores all other options. */ + flags?: number; + /** Opens an in-memory database. */ + memory?: boolean; + /** Whether to support BigInt columns. False by default, integers larger than 32 bit will be inaccurate. */ + int64?: boolean; + /** Apply agressive optimizations that are not possible with concurrent clients. */ + unsafeConcurrency?: boolean; + /** Enable or disable extension loading */ + enableLoadExtension?: boolean; + + sqlite3?: string +} + +/** Transaction function created using `Database#transaction`. */ +export type Transaction void> = + & ((...args: Parameters) => ReturnType) + & { + /** BEGIN */ + default: Transaction; + /** BEGIN DEFERRED */ + deferred: Transaction; + /** BEGIN IMMEDIATE */ + immediate: Transaction; + /** BEGIN EXCLUSIVE */ + exclusive: Transaction; + database: Database; + }; + +/** + * Options for user-defined functions. + * + * @link https://www.sqlite.org/c3ref/c_deterministic.html + */ +export interface FunctionOptions { + varargs?: boolean; + deterministic?: boolean; + directOnly?: boolean; + innocuous?: boolean; + subtype?: boolean; +} + +export interface AggregateFunctionOptions extends FunctionOptions { + start: any | (() => any); + step: (aggregate: any, ...args: any[]) => void; + final?: (aggregate: any) => any; +} + +/** + * Whether the given SQL statement is complete. + * + * @param statement SQL statement string + */ +// export function isComplete(statement: string): boolean { +// return Boolean(sqlite3_complete(toCString(statement))); +// } + +/** + * Represents a SQLite3 database connection. + * + * Example: + * ```ts + * // Open a database from file, creates if doesn't exist. + * const db = new Database("myfile.db"); + * + * // Open an in-memory database. + * const db = new Database(":memory:"); + * + * // Open a read-only database. + * const db = new Database("myfile.db", { readonly: true }); + * + * // Or open using File URL + * const db = new Database(new URL("./myfile.db", import.meta.url)); + * ``` + */ +export class Database { + #path: string; + #handle: Deno.PointerValue; + #open = true; + #enableLoadExtension = false; + + /** Whether to support BigInt columns. False by default, integers larger than 32 bit will be inaccurate. */ + int64: boolean; + + unsafeConcurrency: boolean; + + /** Whether DB connection is open */ + get open(): boolean { + return this.#open; + } + + /** Unsafe Raw (pointer) to the sqlite object */ + get unsafeHandle(): Deno.PointerValue { + return this.#handle; + } + + /** Path of the database file. */ + get path(): string { + return this.#path; + } + + /** Number of rows changed by the last executed statement. */ + get changes(): number { + const { sqlite3_changes } = ffi() + return sqlite3_changes(this.#handle); + } + + /** Number of rows changed since the database connection was opened. */ + get totalChanges(): number { + const { sqlite3_total_changes } = ffi() + return sqlite3_total_changes(this.#handle); + } + + /** Gets last inserted Row ID */ + get lastInsertRowId(): number { + const { sqlite3_last_insert_rowid } = ffi() + return Number(sqlite3_last_insert_rowid(this.#handle)); + } + + /** Whether autocommit is enabled. Enabled by default, can be disabled using BEGIN statement. */ + get autocommit(): boolean { + const { sqlite3_get_autocommit } = ffi() + return sqlite3_get_autocommit(this.#handle) === 1; + } + + /** Whether DB is in mid of a transaction */ + get inTransaction(): boolean { + return this.#open && !this.autocommit; + } + + get enableLoadExtension(): boolean { + return this.#enableLoadExtension; + } + + // deno-lint-ignore explicit-module-boundary-types + set enableLoadExtension(enabled: boolean) { + const { sqlite3_enable_load_extension } = ffi() + const result = sqlite3_enable_load_extension(this.#handle, Number(enabled)); + unwrap(result, this.#handle); + this.#enableLoadExtension = enabled; + } + + constructor(path: string | URL, options: DatabaseOpenOptions = {}) { + + const { sqlite3_open_v2, sqlite3_close_v2 } = ffi(options.sqlite3) + + this.#path = path instanceof URL ? fromFileUrl(path) : path; + let flags = 0; + this.int64 = options.int64 ?? false; + this.unsafeConcurrency = options.unsafeConcurrency ?? false; + if (options.flags !== undefined) { + flags = options.flags; + } else { + if (options.memory) { + flags |= SQLITE3_OPEN_MEMORY; + } + + if (options.readonly ?? false) { + flags |= SQLITE3_OPEN_READONLY; + } else { + flags |= SQLITE3_OPEN_READWRITE; + } + + if ((options.create ?? true) && !options.readonly) { + flags |= SQLITE3_OPEN_CREATE; + } + } + + const pHandle = new Uint32Array(2); + const result = sqlite3_open_v2(toCString(this.#path), pHandle, flags, null); + this.#handle = Deno.UnsafePointer.create(pHandle[0] + 2 ** 32 * pHandle[1]); + if (result !== 0) sqlite3_close_v2(this.#handle); + unwrap(result); + + if (options.enableLoadExtension) { + this.enableLoadExtension = options.enableLoadExtension; + } + } + + /** + * Creates a new Prepared Statement from the given SQL statement. + * + * Example: + * ```ts + * const stmt = db.prepare("SELECT * FROM mytable WHERE id = ?"); + * + * for (const row of stmt.all(1)) { + * console.log(row); + * } + * ``` + * + * Bind parameters can be either provided as an array of values, or as an object + * mapping the parameter name to the value. + * + * Example: + * ```ts + * const stmt = db.prepare("SELECT * FROM mytable WHERE id = ?"); + * const row = stmt.get(1); + * + * // or + * + * const stmt = db.prepare("SELECT * FROM mytable WHERE id = :id"); + * const row = stmt.get({ id: 1 }); + * ``` + * + * Statements are automatically freed once GC catches them, however + * you can also manually free using `finalize` method. + * + * @param sql SQL statement string + * @returns Statement object + */ + prepare(sql: string): Statement { + return new Statement(this, sql); + } + + /** + * Simply executes the SQL statement (supports multiple statements separated by semicolon). + * Returns the number of changes made by last statement. + * + * Example: + * ```ts + * // Create table + * db.exec("create table users (id integer not null, username varchar(20) not null)"); + * + * // Inserts + * db.exec("insert into users (id, username) values(?, ?)", id, username); + * + * // Insert with named parameters + * db.exec("insert into users (id, username) values(:id, :username)", { id, username }); + * + * // Pragma statements + * db.exec("pragma journal_mode = WAL"); + * db.exec("pragma synchronous = normal"); + * db.exec("pragma temp_store = memory"); + * ``` + * + * Under the hood, it uses `sqlite3_exec` if no parameters are given to bind + * with the SQL statement, a prepared statement otherwise. + */ + exec(sql: string, ...params: RestBindParameters): number { + const { sqlite3_exec, sqlite3_free, sqlite3_changes } = ffi() + + if (params.length === 0) { + const pErr = new Uint32Array(2); + sqlite3_exec( + this.#handle, + toCString(sql), + null, + null, + new Uint8Array(pErr.buffer), + ); + const errPtr = Deno.UnsafePointer.create(pErr[0] + 2 ** 32 * pErr[1]); + if (errPtr !== null) { + const err = readCstr(errPtr); + sqlite3_free(errPtr); + throw new Error(err); + } + return sqlite3_changes(this.#handle); + } + + const stmt = this.prepare(sql); + stmt.run(...params); + return sqlite3_changes(this.#handle); + } + + /** Alias for `exec`. */ + run(sql: string, ...params: RestBindParameters): number { + return this.exec(sql, ...params); + } + + /** Safely execute SQL with parameters using a tagged template */ + sql = Record>( + strings: TemplateStringsArray, + ...parameters: RestBindParameters + ): T[] { + const sql = strings.join("?"); + const stmt = this.prepare(sql); + return stmt.all(...parameters); + } + + /** + * Wraps a callback function in a transaction. + * + * - When function is called, the transaction is started. + * - When function returns, the transaction is committed. + * - When function throws an error, the transaction is rolled back. + * + * Example: + * ```ts + * const stmt = db.prepare("insert into users (id, username) values(?, ?)"); + * + * interface User { + * id: number; + * username: string; + * } + * + * const insertUsers = db.transaction((data: User[]) => { + * for (const user of data) { + * stmt.run(user); + * } + * }); + * + * insertUsers([ + * { id: 1, username: "alice" }, + * { id: 2, username: "bob" }, + * ]); + * + * // May also use `insertUsers.deferred`, `immediate`, or `exclusive`. + * // They corresspond to using `BEGIN DEFERRED`, `BEGIN IMMEDIATE`, and `BEGIN EXCLUSIVE`. + * // For eg. + * + * insertUsers.deferred([ + * { id: 1, username: "alice" }, + * { id: 2, username: "bob" }, + * ]); + * ``` + */ + transaction, ...args: any[]) => void>( + fn: T, + ): Transaction { + // Based on https://github.com/WiseLibs/better-sqlite3/blob/master/lib/methods/transaction.js + const controller = getController(this); + + // Each version of the transaction function has these same properties + const properties = { + default: { value: wrapTransaction(fn, this, controller.default) }, + deferred: { value: wrapTransaction(fn, this, controller.deferred) }, + immediate: { value: wrapTransaction(fn, this, controller.immediate) }, + exclusive: { value: wrapTransaction(fn, this, controller.exclusive) }, + database: { value: this, enumerable: true }, + }; + + Object.defineProperties(properties.default.value, properties); + Object.defineProperties(properties.deferred.value, properties); + Object.defineProperties(properties.immediate.value, properties); + Object.defineProperties(properties.exclusive.value, properties); + + // Return the default version of the transaction function + return properties.default.value as Transaction; + } + + #callbacks = new Set(); + + /** + * Creates a new user-defined function. + * + * Example: + * ```ts + * db.function("add", (a: number, b: number) => a + b); + * db.prepare("select add(1, 2)").value<[number]>()!; // [3] + * ``` + */ + function( + name: string, + fn: CallableFunction, + options?: FunctionOptions, + ): void { + const { + sqlite3_value_type, + sqlite3_value_int64, + sqlite3_value_double, + sqlite3_value_text, + sqlite3_value_bytes, + sqlite3_value_blob, + sqlite3_result_error, + sqlite3_result_double, + sqlite3_result_int, + sqlite3_result_int64, + sqlite3_result_blob, + sqlite3_result_text, + sqlite3_result_null, + sqlite3_create_function + } = ffi() + + const cb = new Deno.UnsafeCallback( + { + parameters: ["pointer", "i32", "pointer"], + result: "void", + } as const, + (ctx, nArgs, pArgs) => { + const argptr = new Deno.UnsafePointerView(pArgs!); + const args: any[] = []; + for (let i = 0; i < nArgs; i++) { + const arg = Deno.UnsafePointer.create( + Number(argptr.getBigUint64(i * 8)), + ); + const type = sqlite3_value_type(arg); + switch (type) { + case SQLITE_INTEGER: + args.push(sqlite3_value_int64(arg)); + break; + case SQLITE_FLOAT: + args.push(sqlite3_value_double(arg)); + break; + case SQLITE_TEXT: + args.push( + new TextDecoder().decode( + new Uint8Array( + Deno.UnsafePointerView.getArrayBuffer( + sqlite3_value_text(arg)!, + sqlite3_value_bytes(arg), + ), + ), + ), + ); + break; + case SQLITE_BLOB: + args.push( + new Uint8Array( + Deno.UnsafePointerView.getArrayBuffer( + sqlite3_value_blob(arg)!, + sqlite3_value_bytes(arg), + ), + ), + ); + break; + case SQLITE_NULL: + args.push(null); + break; + default: + throw new Error(`Unknown type: ${type}`); + } + } + + let result: any; + try { + result = fn(...args); + } catch (err) { + const buf = new TextEncoder().encode(err.message); + sqlite3_result_error(ctx, buf, buf.byteLength); + return; + } + + if (result === undefined || result === null) { + sqlite3_result_null(ctx); + } else if (typeof result === "boolean") { + sqlite3_result_int(ctx, result ? 1 : 0); + } else if (typeof result === "number") { + if (Number.isSafeInteger(result)) sqlite3_result_int64(ctx, result); + else sqlite3_result_double(ctx, result); + } else if (typeof result === "bigint") { + sqlite3_result_int64(ctx, result); + } else if (typeof result === "string") { + const buffer = new TextEncoder().encode(result); + sqlite3_result_text(ctx, buffer, buffer.byteLength, 0); + } else if (result instanceof Uint8Array) { + sqlite3_result_blob(ctx, result, result.length, -1); + } else { + const buffer = new TextEncoder().encode( + `Invalid return value: ${Deno.inspect(result)}`, + ); + sqlite3_result_error(ctx, buffer, buffer.byteLength); + } + }, + ); + + let flags = 1; + + if (options?.deterministic) { + flags |= 0x000000800; + } + + if (options?.directOnly) { + flags |= 0x000080000; + } + + if (options?.subtype) { + flags |= 0x000100000; + } + + if (options?.directOnly) { + flags |= 0x000200000; + } + + const err = sqlite3_create_function( + this.#handle, + toCString(name), + options?.varargs ? -1 : fn.length, + flags, + null, + cb.pointer, + null, + null, + ); + + unwrap(err, this.#handle); + + this.#callbacks.add(cb as Deno.UnsafeCallback); + } + + /** + * Closes the database connection. + * + * Calling this method more than once is no-op. + */ + close(): void { + const { sqlite3_finalize, sqlite3_close_v2 } = ffi(); + if (!this.#open) return; + for (const [stmt, db] of STATEMENTS) { + if (db === this.#handle) { + sqlite3_finalize(stmt); + STATEMENTS.delete(stmt); + } + } + for (const cb of this.#callbacks) { + cb.close(); + } + unwrap(sqlite3_close_v2(this.#handle)); + this.#open = false; + } +} + +const controllers = new WeakMap(); + +// Return the database's cached transaction controller, or create a new one +const getController = (db: Database) => { + let controller = controllers.get(db); + if (!controller) { + const shared = { + commit: db.prepare("COMMIT"), + rollback: db.prepare("ROLLBACK"), + savepoint: db.prepare("SAVEPOINT `\t_bs3.\t`"), + release: db.prepare("RELEASE `\t_bs3.\t`"), + rollbackTo: db.prepare("ROLLBACK TO `\t_bs3.\t`"), + }; + + controllers.set( + db, + controller = { + default: Object.assign( + { begin: db.prepare("BEGIN") }, + shared, + ), + deferred: Object.assign( + { begin: db.prepare("BEGIN DEFERRED") }, + shared, + ), + immediate: Object.assign( + { begin: db.prepare("BEGIN IMMEDIATE") }, + shared, + ), + exclusive: Object.assign( + { begin: db.prepare("BEGIN EXCLUSIVE") }, + shared, + ), + }, + ); + } + return controller; +}; + +// Return a new transaction function by wrapping the given function +const wrapTransaction = void>( + fn: T, + db: Database, + { begin, commit, rollback, savepoint, release, rollbackTo }: any, +) => + function sqliteTransaction(...args: Parameters): ReturnType { + const { apply } = Function.prototype; + let before, after, undo; + if (db.inTransaction) { + before = savepoint; + after = release; + undo = rollbackTo; + } else { + before = begin; + after = commit; + undo = rollback; + } + before.run(); + try { + // @ts-ignore An outer value of 'this' is shadowed by this container. + const result = apply.call(fn, this, args); + after.run(); + return result; + } catch (ex) { + if (!db.autocommit) { + undo.run(); + if (undo !== rollback) after.run(); + } + throw ex; + } + }; diff --git a/vendor/sqlite3@0.10.0/src/ffi.ts b/vendor/sqlite3@0.10.0/src/ffi.ts new file mode 100644 index 0000000..8fc5cc2 --- /dev/null +++ b/vendor/sqlite3@0.10.0/src/ffi.ts @@ -0,0 +1,592 @@ +const symbols = { + sqlite3_open_v2: { + parameters: [ + "buffer", // const char *filename + "buffer", // sqlite3 **ppDb + "i32", // int flags + "pointer", // const char *zVfs + ], + result: "i32", + }, + + sqlite3_close_v2: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_changes: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_total_changes: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_last_insert_rowid: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_get_autocommit: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_prepare_v2: { + parameters: [ + "pointer", // sqlite3 *db + "buffer", // const char *zSql + "i32", // int nByte + "buffer", // sqlite3_stmt **ppStmt + "pointer", // const char **pzTail + ], + result: "i32", + }, + + sqlite3_reset: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_clear_bindings: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_step: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_step_cb: { + name: "sqlite3_step", + callback: true, + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_column_count: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_column_type: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "i32", + }, + + sqlite3_column_text: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "pointer", + }, + sqlite3_column_value: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "pointer", + }, + + sqlite3_finalize: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_exec: { + parameters: [ + "pointer", // sqlite3 *db + "buffer", // const char *sql + "pointer", // sqlite3_callback callback + "pointer", // void *arg + "buffer", // char **errmsg + ], + result: "i32", + }, + + sqlite3_free: { + parameters: [ + "pointer", // void *p + ], + result: "void", + }, + + sqlite3_column_int: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "i32", + }, + + sqlite3_column_double: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "f64", + }, + + sqlite3_column_blob: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "pointer", + }, + + sqlite3_column_bytes: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "i32", + }, + + sqlite3_column_name: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "pointer", + }, + + sqlite3_column_decltype: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "u64", + }, + + sqlite3_bind_parameter_index: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "buffer", // const char *zName + ], + result: "i32", + }, + + sqlite3_bind_text: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + "buffer", // const char *zData + "i32", // int nData + "pointer", // void (*xDel)(void*) + ], + result: "i32", + }, + + sqlite3_bind_blob: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + "buffer", // const void *zData + "i32", // int nData + "pointer", // void (*xDel)(void*) + ], + result: "i32", + }, + + sqlite3_bind_double: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + "f64", // double rValue + ], + result: "i32", + }, + + sqlite3_bind_int: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + "i32", // int iValue + ], + result: "i32", + }, + + sqlite3_bind_int64: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + "i64", // i64 iValue + ], + result: "i32", + }, + + sqlite3_bind_null: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "i32", + }, + + sqlite3_expanded_sql: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "pointer", + }, + + sqlite3_bind_parameter_count: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_complete: { + parameters: [ + "buffer", // const char *sql + ], + result: "i32", + }, + + sqlite3_sourceid: { + parameters: [], + result: "pointer", + }, + + sqlite3_libversion: { + parameters: [], + result: "pointer", + }, + + sqlite3_blob_open: { + parameters: [ + "pointer", /* sqlite3 *db */ + "buffer", /* const char *zDb */ + "buffer", /* const char *zTable */ + "buffer", /* const char *zColumn */ + "i64", /* sqlite3_int64 iRow */ + "i32", /* int flags */ + "buffer", /* sqlite3_blob **ppBlob */ + ], + result: "i32", + }, + + sqlite3_blob_read: { + parameters: [ + "pointer", /* sqlite3_blob *blob */ + "buffer", /* void *Z */ + "i32", /* int N */ + "i32", /* int iOffset */ + ], + result: "i32", + }, + + sqlite3_blob_write: { + parameters: [ + "pointer", /* sqlite3_blob *blob */ + "buffer", /* const void *z */ + "i32", /* int n */ + "i32", /* int iOffset */ + ], + result: "i32", + }, + + sqlite3_blob_bytes: { + parameters: ["pointer" /* sqlite3_blob *blob */], + result: "i32", + }, + + sqlite3_blob_close: { + parameters: ["pointer" /* sqlite3_blob *blob */], + result: "i32", + }, + + sqlite3_sql: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "pointer", + }, + + sqlite3_stmt_readonly: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + ], + result: "i32", + }, + + sqlite3_bind_parameter_name: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "pointer", + }, + + sqlite3_errcode: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "i32", + }, + + sqlite3_errmsg: { + parameters: [ + "pointer", // sqlite3 *db + ], + result: "pointer", + }, + + sqlite3_errstr: { + parameters: [ + "i32", // int rc + ], + result: "pointer", + }, + + sqlite3_column_int64: { + parameters: [ + "pointer", // sqlite3_stmt *pStmt + "i32", // int iCol + ], + result: "i64", + }, + + sqlite3_backup_init: { + parameters: [ + "pointer", // sqlite3 *pDest + "buffer", // const char *zDestName + "pointer", // sqlite3 *pSource + "buffer", // const char *zSourceName + ], + result: "pointer", + }, + + sqlite3_backup_step: { + parameters: [ + "pointer", // sqlite3_backup *p + "i32", // int nPage + ], + result: "i32", + }, + + sqlite3_backup_finish: { + parameters: [ + "pointer", // sqlite3_backup *p + ], + result: "i32", + }, + + sqlite3_backup_remaining: { + parameters: [ + "pointer", // sqlite3_backup *p + ], + result: "i32", + }, + + sqlite3_backup_pagecount: { + parameters: [ + "pointer", // sqlite3_backup *p + ], + result: "i32", + }, + + sqlite3_create_function: { + parameters: [ + "pointer", // sqlite3 *db + "buffer", // const char *zFunctionName + "i32", // int nArg + "i32", // int eTextRep + "pointer", // void *pApp + "pointer", // void (*xFunc)(sqlite3_context*,int,sqlite3_value**) + "pointer", // void (*xStep)(sqlite3_context*,int,sqlite3_value**) + "pointer", // void (*xFinal)(sqlite3_context*) + ], + result: "i32", + }, + + sqlite3_result_blob: { + parameters: [ + "pointer", // sqlite3_context *p + "buffer", // const void *z + "i32", // int n + "isize", // void (*xDel)(void*) + ], + result: "void", + }, + + sqlite3_result_double: { + parameters: [ + "pointer", // sqlite3_context *p + "f64", // double rVal + ], + result: "void", + }, + + sqlite3_result_error: { + parameters: [ + "pointer", // sqlite3_context *p + "buffer", // const char *z + "i32", // int n + ], + result: "void", + }, + + sqlite3_result_int: { + parameters: [ + "pointer", // sqlite3_context *p + "i32", // int iVal + ], + result: "void", + }, + + sqlite3_result_int64: { + parameters: [ + "pointer", // sqlite3_context *p + "i64", // sqlite3_int64 iVal + ], + result: "void", + }, + + sqlite3_result_null: { + parameters: [ + "pointer", // sqlite3_context *p + ], + result: "void", + }, + + sqlite3_result_text: { + parameters: [ + "pointer", // sqlite3_context *p + "buffer", // const char *z + "i32", // int n + "isize", // void (*xDel)(void*) + ], + result: "void", + }, + + sqlite3_value_type: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "i32", + }, + sqlite3_value_subtype: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "i32", + }, + + sqlite3_value_blob: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "pointer", + }, + + sqlite3_value_double: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "f64", + }, + + sqlite3_value_int: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "i32", + }, + + sqlite3_value_int64: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "i64", + }, + + sqlite3_value_text: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "pointer", + }, + + sqlite3_value_bytes: { + parameters: [ + "pointer", // sqlite3_value *pVal + ], + result: "i32", + }, + + sqlite3_aggregate_context: { + parameters: [ + "pointer", // sqlite3_context *p + "i32", // int nBytes + ], + result: "pointer", + }, + + sqlite3_enable_load_extension: { + parameters: [ + "pointer", // sqlite3 *db + "i32", // int onoff + ], + result: "i32", + }, + + sqlite3_load_extension: { + parameters: [ + "pointer", // sqlite3 *db + "buffer", // const char *zFile + "buffer", // const char *zProc + "buffer", // const char **pzErrMsg + ], + result: "i32", + }, + + sqlite3_initialize: { + parameters: [], + result: "i32", + }, +} as const satisfies Deno.ForeignLibraryInterface; + +let lib: Deno.DynamicLibrary["symbols"]; + +export default function(path?: string) { + if (!lib) { + lib = Deno.dlopen(path!, symbols).symbols; + const init = lib.sqlite3_initialize(); + if (init !== 0) { + throw new Error(`Failed to initialize SQLite3: ${init}`); + } + } + return lib +} diff --git a/vendor/sqlite3@0.10.0/src/statement.ts b/vendor/sqlite3@0.10.0/src/statement.ts new file mode 100644 index 0000000..aacf545 --- /dev/null +++ b/vendor/sqlite3@0.10.0/src/statement.ts @@ -0,0 +1,891 @@ +import type { Database } from "./database.ts"; +import { readCstr, toCString, unwrap } from "./util.ts"; +import ffi from "./ffi.ts"; +import { + SQLITE3_DONE, + SQLITE3_ROW, + SQLITE_BLOB, + SQLITE_FLOAT, + SQLITE_INTEGER, + SQLITE_TEXT, +} from "./constants.ts"; + +/** Types that can be possibly serialized as SQLite bind values */ +export type BindValue = + | number + | string + | symbol + | bigint + | boolean + | null + | undefined + | Date + | Uint8Array + | BindValue[] + | { [key: string]: BindValue }; + +export type BindParameters = BindValue[] | Record; +export type RestBindParameters = BindValue[] | [BindParameters]; + +export const STATEMENTS = new Map(); + +const emptyStringBuffer = new Uint8Array(1); + +const statementFinalizer = new FinalizationRegistry( + (ptr: Deno.PointerValue) => { + const { + sqlite3_finalize, + } = ffi(); + + if (STATEMENTS.has(ptr)) { + sqlite3_finalize(ptr); + STATEMENTS.delete(ptr); + } + }, +); + +// https://github.com/sqlite/sqlite/blob/195611d8e6fc0bba559a49e91e6ceb42e4bdd6ba/src/json.c#L125-L126 +const JSON_SUBTYPE = 74; + +function getColumn(handle: Deno.PointerValue, i: number, int64: boolean): any { + const { + sqlite3_column_type, + sqlite3_column_value, + sqlite3_value_subtype, + sqlite3_column_text, + sqlite3_column_int64, + sqlite3_column_double, + sqlite3_column_blob, + sqlite3_column_bytes, + sqlite3_column_int, + } = ffi(); + + const ty = sqlite3_column_type(handle, i); + + if (ty === SQLITE_INTEGER && !int64) return sqlite3_column_int(handle, i); + + switch (ty) { + case SQLITE_TEXT: { + const ptr = sqlite3_column_text(handle, i); + if (ptr === null) return null; + const text = readCstr(ptr, 0); + const value = sqlite3_column_value(handle, i); + const subtype = sqlite3_value_subtype(value); + if (subtype === JSON_SUBTYPE) { + try { + return JSON.parse(text); + } catch (_error) { + return text; + } + } + return text; + } + + case SQLITE_INTEGER: { + return sqlite3_column_int64(handle, i); + } + + case SQLITE_FLOAT: { + return sqlite3_column_double(handle, i); + } + + case SQLITE_BLOB: { + const ptr = sqlite3_column_blob(handle, i); + const bytes = sqlite3_column_bytes(handle, i); + return new Uint8Array( + Deno.UnsafePointerView.getArrayBuffer(ptr!, bytes).slice(0), + ); + } + + default: { + return null; + } + } +} + +/** + * Represents a prepared statement. + * + * See `Database#prepare` for more information. + */ +export class Statement { + #handle: Deno.PointerValue; + #finalizerToken: { handle: Deno.PointerValue }; + #bound = false; + #hasNoArgs = false; + #unsafeConcurrency; + + /** + * Whether the query might call into JavaScript or not. + * + * Must enable if the query makes use of user defined functions, + * otherwise there can be V8 crashes. + * + * Off by default. Causes performance degradation. + */ + callback = false; + + /** Unsafe Raw (pointer) to the sqlite object */ + get unsafeHandle(): Deno.PointerValue { + return this.#handle; + } + + /** SQL string including bindings */ + get expandedSql(): string { + const { + sqlite3_expanded_sql, + } = ffi(); + + return readCstr(sqlite3_expanded_sql(this.#handle)!); + } + + /** The SQL string that we passed when creating statement */ + get sql(): string { + const { + sqlite3_sql, + } = ffi(); + + return readCstr(sqlite3_sql(this.#handle)!); + } + + /** Whether this statement doesn't make any direct changes to the DB */ + get readonly(): boolean { + const { + sqlite3_stmt_readonly, + } = ffi(); + + return sqlite3_stmt_readonly(this.#handle) !== 0; + } + + /** Simply run the query without retrieving any output there may be. */ + run(...args: RestBindParameters): number { + return this.#runWithArgs(...args); + } + + /** + * Run the query and return the resulting rows where rows are array of columns. + */ + values(...args: RestBindParameters): T[] { + return this.#valuesWithArgs(...args); + } + + /** + * Run the query and return the resulting rows where rows are objects + * mapping column name to their corresponding values. + */ + all = Record>( + ...args: RestBindParameters + ): T[] { + return this.#allWithArgs(...args); + } + + #bindParameterCount: number; + + /** Number of parameters (to be) bound */ + get bindParameterCount(): number { + return this.#bindParameterCount; + } + + constructor(public db: Database, sql: string) { + const { + sqlite3_prepare_v2, + sqlite3_bind_parameter_count, + } = ffi(); + + const pHandle = new Uint32Array(2); + unwrap( + sqlite3_prepare_v2( + db.unsafeHandle, + toCString(sql), + sql.length, + pHandle, + null, + ), + db.unsafeHandle, + ); + this.#handle = Deno.UnsafePointer.create(pHandle[0] + 2 ** 32 * pHandle[1]); + STATEMENTS.set(this.#handle, db.unsafeHandle); + this.#unsafeConcurrency = db.unsafeConcurrency; + this.#finalizerToken = { handle: this.#handle }; + statementFinalizer.register(this, this.#handle, this.#finalizerToken); + + if ( + (this.#bindParameterCount = sqlite3_bind_parameter_count( + this.#handle, + )) === 0 + ) { + this.#hasNoArgs = true; + this.all = this.#allNoArgs; + this.values = this.#valuesNoArgs; + this.run = this.#runNoArgs; + this.value = this.#valueNoArgs; + this.get = this.#getNoArgs; + } + } + + /** Shorthand for `this.callback = true`. Enables calling user defined functions. */ + enableCallback(): this { + this.callback = true; + return this; + } + + /** Get bind parameter name by index */ + bindParameterName(i: number): string { + const { + sqlite3_bind_parameter_name, + } = ffi(); + + return readCstr(sqlite3_bind_parameter_name(this.#handle, i)!); + } + + /** Get bind parameter index by name */ + bindParameterIndex(name: string): number { + const { + sqlite3_bind_parameter_index, + } = ffi(); + + if (name[0] !== ":" && name[0] !== "@" && name[0] !== "$") { + name = ":" + name; + } + return sqlite3_bind_parameter_index(this.#handle, toCString(name)); + } + + #begin(): void { + const { + sqlite3_reset, + sqlite3_clear_bindings, + } = ffi(); + + sqlite3_reset(this.#handle); + if (!this.#bound && !this.#hasNoArgs) { + sqlite3_clear_bindings(this.#handle); + this.#bindRefs.clear(); + } + } + + #bindRefs: Set = new Set(); + + #bind(i: number, param: BindValue): void { + const { + sqlite3_bind_int, + sqlite3_bind_int64, + sqlite3_bind_text, + sqlite3_bind_blob, + sqlite3_bind_double, + } = ffi(); + + switch (typeof param) { + case "number": { + if (Number.isInteger(param)) { + if ( + Number.isSafeInteger(param) && param >= -(2 ** 31) && + param < 2 ** 31 + ) { + unwrap(sqlite3_bind_int(this.#handle, i + 1, param)); + } else { + unwrap(sqlite3_bind_int64(this.#handle, i + 1, BigInt(param))); + } + } else { + unwrap(sqlite3_bind_double(this.#handle, i + 1, param)); + } + break; + } + case "string": { + if (param === "") { + // Empty string is encoded as empty buffer in Deno. And as of + // right now (Deno 1.29.1), ffi layer converts it to NULL pointer, + // which causes sqlite3_bind_text to bind the NULL value instead + // of an empty string. As a workaround let's use a special + // non-empty buffer, but specify zero length. + unwrap( + sqlite3_bind_text(this.#handle, i + 1, emptyStringBuffer, 0, null), + ); + } else { + const str = new TextEncoder().encode(param); + this.#bindRefs.add(str); + unwrap( + sqlite3_bind_text(this.#handle, i + 1, str, str.byteLength, null), + ); + } + break; + } + case "object": { + if (param === null) { + // pass + } else if (param instanceof Uint8Array) { + this.#bindRefs.add(param); + unwrap( + sqlite3_bind_blob( + this.#handle, + i + 1, + param, + param.byteLength, + null, + ), + ); + } else if (param instanceof Date) { + const cstring = toCString(param.toISOString()); + this.#bindRefs.add(cstring); + unwrap( + sqlite3_bind_text( + this.#handle, + i + 1, + cstring, + -1, + null, + ), + ); + } else { + const cstring = toCString(JSON.stringify(param)); + this.#bindRefs.add(cstring); + unwrap( + sqlite3_bind_text( + this.#handle, + i + 1, + cstring, + -1, + null, + ), + ); + } + break; + } + + case "bigint": { + unwrap(sqlite3_bind_int64(this.#handle, i + 1, param)); + break; + } + + case "boolean": + unwrap(sqlite3_bind_int( + this.#handle, + i + 1, + param ? 1 : 0, + )); + break; + default: { + throw new Error(`Value of unsupported type: ${Deno.inspect(param)}`); + } + } + } + + /** + * Bind parameters to the statement. This method can only be called once + * to set the parameters to be same throughout the statement. You cannot + * change the parameters after this method is called. + * + * This method is merely just for optimization to avoid binding parameters + * each time in prepared statement. + */ + bind(...params: RestBindParameters): this { + this.#bindAll(params); + this.#bound = true; + return this; + } + + #bindAll(params: RestBindParameters | BindParameters): void { + if (this.#bound) throw new Error("Statement already bound to values"); + if ( + typeof params[0] === "object" && params[0] !== null && + !(params[0] instanceof Uint8Array) && !(params[0] instanceof Date) + ) { + params = params[0]; + } + if (Array.isArray(params)) { + for (let i = 0; i < params.length; i++) { + this.#bind(i, (params as BindValue[])[i]); + } + } else { + for (const [name, param] of Object.entries(params)) { + const i = this.bindParameterIndex(name); + if (i === 0) { + throw new Error(`No such parameter "${name}"`); + } + this.#bind(i - 1, param as BindValue); + } + } + } + + #runNoArgs(): number { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_changes, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + this.#begin(); + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + if (status !== SQLITE3_ROW && status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return sqlite3_changes(this.db.unsafeHandle); + } + + #runWithArgs(...params: RestBindParameters): number { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_changes, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + this.#begin(); + this.#bindAll(params); + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + if (!this.#hasNoArgs && !this.#bound && params.length) { + this.#bindRefs.clear(); + } + if (status !== SQLITE3_ROW && status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return sqlite3_changes(this.db.unsafeHandle); + } + + #valuesNoArgs>(): T[] { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_column_count, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const callback = this.callback; + this.#begin(); + const columnCount = sqlite3_column_count(handle); + const result: T[] = []; + const getRowArray = new Function( + "getColumn", + ` + return function(h) { + return [${ + Array.from({ length: columnCount }).map((_, i) => + `getColumn(h, ${i}, ${this.db.int64})` + ) + .join(", ") + }]; + }; + `, + )(getColumn); + let status; + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + while (status === SQLITE3_ROW) { + result.push(getRowArray(handle)); + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + } + if (status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return result as T[]; + } + + #valuesWithArgs>( + ...params: RestBindParameters + ): T[] { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_column_count, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const callback = this.callback; + this.#begin(); + this.#bindAll(params); + const columnCount = sqlite3_column_count(handle); + const result: T[] = []; + const getRowArray = new Function( + "getColumn", + ` + return function(h) { + return [${ + Array.from({ length: columnCount }).map((_, i) => + `getColumn(h, ${i}, ${this.db.int64})` + ) + .join(", ") + }]; + }; + `, + )(getColumn); + let status; + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + while (status === SQLITE3_ROW) { + result.push(getRowArray(handle)); + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + } + if (!this.#hasNoArgs && !this.#bound && params.length) { + this.#bindRefs.clear(); + } + if (status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return result as T[]; + } + + #rowObjectFn: ((h: Deno.PointerValue) => any) | undefined; + + getRowObject(): (h: Deno.PointerValue) => any { + if (!this.#rowObjectFn || !this.#unsafeConcurrency) { + const columnNames = this.columnNames(); + const getRowObject = new Function( + "getColumn", + ` + return function(h) { + return { + ${ + columnNames.map((name, i) => + `"${name}": getColumn(h, ${i}, ${this.db.int64})` + ).join(",\n") + } + }; + }; + `, + )(getColumn); + this.#rowObjectFn = getRowObject; + } + return this.#rowObjectFn!; + } + + #allNoArgs>(): T[] { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const callback = this.callback; + this.#begin(); + const getRowObject = this.getRowObject(); + const result: T[] = []; + let status; + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + while (status === SQLITE3_ROW) { + result.push(getRowObject(handle)); + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + } + if (status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return result as T[]; + } + + #allWithArgs>( + ...params: RestBindParameters + ): T[] { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const callback = this.callback; + this.#begin(); + this.#bindAll(params); + const getRowObject = this.getRowObject(); + const result: T[] = []; + let status; + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + while (status === SQLITE3_ROW) { + result.push(getRowObject(handle)); + if (callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + } + if (!this.#hasNoArgs && !this.#bound && params.length) { + this.#bindRefs.clear(); + } + if (status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(handle); + return result as T[]; + } + + /** Fetch only first row as an array, if any. */ + value>( + ...params: RestBindParameters + ): T | undefined { + const { + sqlite3_reset, + sqlite3_clear_bindings, + sqlite3_step, + sqlite3_column_count, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const int64 = this.db.int64; + const arr = new Array(sqlite3_column_count(handle)); + sqlite3_reset(handle); + if (!this.#hasNoArgs && !this.#bound) { + sqlite3_clear_bindings(handle); + this.#bindRefs.clear(); + if (params.length) { + this.#bindAll(params); + } + } + + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + + if (!this.#hasNoArgs && !this.#bound && params.length) { + this.#bindRefs.clear(); + } + + if (status === SQLITE3_ROW) { + for (let i = 0; i < arr.length; i++) { + arr[i] = getColumn(handle, i, int64); + } + sqlite3_reset(this.#handle); + return arr as T; + } else if (status === SQLITE3_DONE) { + return; + } else { + unwrap(status, this.db.unsafeHandle); + } + } + + #valueNoArgs>(): T | undefined { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_column_count, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const int64 = this.db.int64; + const cc = sqlite3_column_count(handle); + const arr = new Array(cc); + sqlite3_reset(handle); + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + if (status === SQLITE3_ROW) { + for (let i = 0; i < cc; i++) { + arr[i] = getColumn(handle, i, int64); + } + sqlite3_reset(this.#handle); + return arr as T; + } else if (status === SQLITE3_DONE) { + return; + } else { + unwrap(status, this.db.unsafeHandle); + } + } + + #columnNames: string[] | undefined; + #rowObject: Record = {}; + + columnNames(): string[] { + const { + sqlite3_column_count, + sqlite3_column_name, + } = ffi(); + + if (!this.#columnNames || !this.#unsafeConcurrency) { + const columnCount = sqlite3_column_count(this.#handle); + const columnNames = new Array(columnCount); + for (let i = 0; i < columnCount; i++) { + columnNames[i] = readCstr(sqlite3_column_name(this.#handle, i)!); + } + this.#columnNames = columnNames; + this.#rowObject = {}; + for (const name of columnNames) { + this.#rowObject![name] = undefined; + } + } + return this.#columnNames!; + } + + /** Fetch only first row as an object, if any. */ + get>( + ...params: RestBindParameters + ): T | undefined { + const { + sqlite3_reset, + sqlite3_clear_bindings, + sqlite3_step, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const int64 = this.db.int64; + + const columnNames = this.columnNames(); + + const row: Record = {}; + sqlite3_reset(handle); + if (!this.#hasNoArgs && !this.#bound) { + sqlite3_clear_bindings(handle); + this.#bindRefs.clear(); + if (params.length) { + this.#bindAll(params); + } + } + + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + + if (!this.#hasNoArgs && !this.#bound && params.length) { + this.#bindRefs.clear(); + } + + if (status === SQLITE3_ROW) { + for (let i = 0; i < columnNames.length; i++) { + row[columnNames[i]] = getColumn(handle, i, int64); + } + sqlite3_reset(this.#handle); + return row as T; + } else if (status === SQLITE3_DONE) { + return; + } else { + unwrap(status, this.db.unsafeHandle); + } + } + + #getNoArgs>(): T | undefined { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_step_cb, + } = ffi(); + + const handle = this.#handle; + const int64 = this.db.int64; + const columnNames = this.columnNames(); + const row: Record = this.#rowObject; + sqlite3_reset(handle); + let status; + if (this.callback) { + status = sqlite3_step_cb(handle); + } else { + status = sqlite3_step(handle); + } + if (status === SQLITE3_ROW) { + for (let i = 0; i < columnNames?.length; i++) { + row[columnNames[i]] = getColumn(handle, i, int64); + } + sqlite3_reset(handle); + return row as T; + } else if (status === SQLITE3_DONE) { + return; + } else { + unwrap(status, this.db.unsafeHandle); + } + } + + /** Free up the statement object. */ + finalize(): void { + const { + sqlite3_finalize, + } = ffi(); + + if (!STATEMENTS.has(this.#handle)) return; + this.#bindRefs.clear(); + statementFinalizer.unregister(this.#finalizerToken); + STATEMENTS.delete(this.#handle); + unwrap(sqlite3_finalize(this.#handle)); + } + + /** Coerces the statement to a string, which in this case is expanded SQL. */ + toString(): string { + const { + sqlite3_expanded_sql, + } = ffi(); + + return readCstr(sqlite3_expanded_sql(this.#handle)!); + } + + /** Iterate over resultant rows from query. */ + *[Symbol.iterator](): IterableIterator { + const { + sqlite3_reset, + sqlite3_step, + sqlite3_step_cb, + } = ffi(); + + this.#begin(); + const getRowObject = this.getRowObject(); + let status; + if (this.callback) { + status = sqlite3_step_cb(this.#handle); + } else { + status = sqlite3_step(this.#handle); + } + while (status === SQLITE3_ROW) { + yield getRowObject(this.#handle); + if (this.callback) { + status = sqlite3_step_cb(this.#handle); + } else { + status = sqlite3_step(this.#handle); + } + } + if (status !== SQLITE3_DONE) { + unwrap(status, this.db.unsafeHandle); + } + sqlite3_reset(this.#handle); + } +} diff --git a/vendor/sqlite3@0.10.0/src/util.ts b/vendor/sqlite3@0.10.0/src/util.ts new file mode 100644 index 0000000..a926937 --- /dev/null +++ b/vendor/sqlite3@0.10.0/src/util.ts @@ -0,0 +1,48 @@ +import { SQLITE3_DONE, SQLITE3_MISUSE, SQLITE3_OK } from "./constants.ts"; +import ffi from "./ffi.ts"; + +export const encoder = new TextEncoder(); + +export function toCString(str: string): Uint8Array { + return encoder.encode(str + "\0"); +} + +export function isObject(value: unknown): boolean { + return typeof value === "object" && value !== null; +} + +export class SqliteError extends Error { + name = "SqliteError"; + + constructor( + public code: number = 1, + message: string = "Unknown Error", + ) { + super(`${code}: ${message}`); + } +} + +export function unwrap(code: number, db?: Deno.PointerValue): void { + const { + sqlite3_errmsg, + sqlite3_errstr, + } = ffi(); + + if (code === SQLITE3_OK || code === SQLITE3_DONE) return; + if (code === SQLITE3_MISUSE) { + throw new SqliteError(code, "SQLite3 API misuse"); + } else if (db !== undefined) { + const errmsg = sqlite3_errmsg(db); + if (errmsg === null) throw new SqliteError(code); + throw new Error(Deno.UnsafePointerView.getCString(errmsg)); + } else { + throw new SqliteError( + code, + Deno.UnsafePointerView.getCString(sqlite3_errstr(code)!), + ); + } +} + +export const buf = Deno.UnsafePointerView.getArrayBuffer; + +export const readCstr = Deno.UnsafePointerView.getCString;