diff --git a/.github/deno-to-node.ts b/.github/deno-to-node.ts index 008b348..f969adc 100755 --- a/.github/deno-to-node.ts +++ b/.github/deno-to-node.ts @@ -16,6 +16,17 @@ const version = (() => { } })() +const mappings: Record = { + "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/hooks/useSyncCache.ts": "./src/hooks/useSyncCache.node.ts" +} + +if (test) { + mappings["./src/hooks/useSyncCache.test.ts"] = "./src/hooks/useCache.test.ts" // no other easy way to skip the test +} + await build({ entryPoints: ["./mod.ts"], outDir: "./dist", @@ -30,11 +41,7 @@ await build({ }], }, importMap: "deno.json", - 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" - }, + mappings, package: { name: "libpkgx", version, 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;