Skip to content

Commit

Permalink
Implemented stdext/sql interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
halvardssm committed Aug 24, 2024
1 parent 35156ce commit b1d0b07
Show file tree
Hide file tree
Showing 19 changed files with 1,296 additions and 181 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,21 @@ console.log(version);
db.close();
```

Using [stdext/sql](https://jsr.io/@stdext/sql) interfaces:

```ts
import { SqliteClient } from "jsr:@db/sqlite@0.11/std_sql";

await using db = new SqliteClient("test.db");

const [version] = await db.queryArray("select sqlite_version()");
console.log(version);
```

## Usage

### Permissions

Since this library depends on the unstable FFI API, you must pass `--allow-env`,
`--allow-ffi` and `--unstable-ffi` flags. Network and FS permissions are also
needed to download and cache prebuilt library.
Expand All @@ -34,6 +47,37 @@ access.
deno run -A --unstable-ffi <file>
```

### std/sql

In addition to the existing `Database` class, a new entrypoint is also exported
to provide compatibility with the [stdext/sql](https://jsr.io/@stdext/sql)
interfaces. Due to the specs, this relies on promises.

```ts
import { SqliteClient } from "jsr:@db/sqlite@0.11/std_sql";

await using db = new SqliteClient("test.db");

await db.execute("create table people (name TEXT)"); // 0
await db.execute("insert into people (name) values ('Alex'), ('Luca');"); // 2
await db.query("select * from people"); // [{name:"Alex"}, {name:"Luca"}]
await db.queryOne("select * from people"); // {name:"Alex"}
Array.fromAsync(db.queryMany("select * from people")); // [{name:"Alex"}, {name:"Luca"}]
await db.queryArray("select * from people"); // [["Alex"], ["Luca"]]
await db.queryOneArray("select * from people"); // ["Alex"]
Array.fromAsync(db.queryManyArray("select * from people")); // [["Alex"], ["Luca"]]
await db.sql`select * from people`; // [{name:"Alex"}, {name:"Luca"}]
await db.sqlArray`select * from people`; // [["Alex"], ["Luca"]]
```

> In general, the `SqliteClient` is good for most cases, and conforms to the
> generalized interfaces in the standard library. However if you are facing
> speed bottlenecks, the `Database` from the main export whould give you some
> more performance.
For more documentation regarding the standard interface, read the
[docs](https://jsr.io/@stdext/sql)

## Benchmark

![image](./bench/results.png)
Expand Down
17 changes: 15 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
"version": "0.11.2",
"github": "https://github.com/denodrivers/sqlite3",

"exports": "./mod.ts",
"exports": {
".": "./mod.ts",
"./std_sql": "./std_sql.ts"
},

"exclude": [
"sqlite",
"scripts"
],

"tasks": {
"test": "deno test --unstable-ffi -A test/test.ts",
"test": "DENO_SQLITE_LOCAL=1 deno test --unstable-ffi -A",
"test:remote": "deno test --unstable-ffi -A",
"build": "deno run -A scripts/build.ts",
"bench-deno": "deno run -A --unstable-ffi bench/bench_deno.js 50 1000000",
"bench-deno-ffi": "deno run -A --unstable-ffi bench/bench_deno_ffi.js 50 1000000",
Expand Down Expand Up @@ -45,5 +49,14 @@
"explicit-module-boundary-types"
]
}
},

"imports": {
"@denosaurs/plug": "jsr:@denosaurs/plug@^1.0.5",
"@std/assert": "jsr:@std/assert@^0.221.0",
"@std/log": "jsr:@std/log@^0.223.0",
"@std/path": "jsr:@std/path@^0.217.0",
"@stdext/collections": "jsr:@stdext/collections@^0.0.5",
"@stdext/sql": "jsr:@stdext/sql@0.0.0-6"
}
}
2 changes: 0 additions & 2 deletions deps.ts

This file was deleted.

3 changes: 1 addition & 2 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ export {
type DatabaseOpenOptions,
type FunctionOptions,
isComplete,
SQLITE_SOURCEID,
SQLITE_VERSION,
type Transaction,
} from "./src/database.ts";
export { SQLITE_SOURCEID, SQLITE_VERSION } from "./src/ffi.ts";
export { type BlobOpenOptions, SQLBlob } from "./src/blob.ts";
export {
type BindParameters,
Expand Down
4 changes: 2 additions & 2 deletions src/blob.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Database } from "./database.ts";
import ffi from "./ffi.ts";
import { toCString, unwrap } from "./util.ts";
import ffi, { unwrap } from "./ffi.ts";
import { toCString } from "./util.ts";

const {
sqlite3_blob_open,
Expand Down
28 changes: 28 additions & 0 deletions src/connection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { assert, assertEquals } from "@std/assert";
import { SqliteConnectable, SqliteConnection } from "./connection.ts";
import { _testSqlConnectable, testSqlConnection } from "@stdext/sql/testing";

Deno.test("connection and connectable contstructs", () => {
const connection = new SqliteConnection(":memory:");
testSqlConnection(connection, { connectionUrl: ":memory:" });
const connectable = new SqliteConnectable(connection);
_testSqlConnectable(connectable, connection);
});

Deno.test("connection can connect and query", async () => {
await using connection = new SqliteConnection(":memory:");
await connection.connect();
assert(connection.connected, "connection should be connected");
const executeResult = await connection.execute(`select 1 as one`);
assertEquals(executeResult, 0);
const queryManyResult = connection.queryMany(`select 1 as one`);
const queryManyResultNext1 = await queryManyResult.next();
assertEquals(queryManyResultNext1, { done: false, value: { one: 1 } });
const queryManyResultNext2 = await queryManyResult.next();
assertEquals(queryManyResultNext2, { done: true, value: undefined });
const queryManyArrayResult = connection.queryManyArray(`select 1 as one`);
const queryManyArrayResultNext1 = await queryManyArrayResult.next();
assertEquals(queryManyArrayResultNext1, { done: false, value: [1] });
const queryManyArrayResultNext2 = await queryManyArrayResult.next();
assertEquals(queryManyArrayResultNext2, { done: true, value: undefined });
});
144 changes: 144 additions & 0 deletions src/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// deno-lint-ignore-file require-await
import type {
ArrayRow,
Row,
SqlConnectable,
SqlConnection,
SqlConnectionOptions,
} from "@stdext/sql";
import { fromFileUrl } from "@std/path";
import ffi from "./ffi.ts";
import { Database, type DatabaseOpenOptions } from "../mod.ts";
import type { SqliteParameterType, SqliteQueryOptions } from "./core.ts";
import { transformToAsyncGenerator } from "./util.ts";

/** Various options that can be configured when opening Database connection. */
export interface SqliteConnectionOptions
extends SqlConnectionOptions, DatabaseOpenOptions {
}

/**
* Represents a SQLx based SQLite3 database connection.
*
* Example:
* ```ts
* // Open a database from file, creates if doesn't exist.
* const db = new SqliteClient("myfile.db");
*
* // Open an in-memory database.
* const db = new SqliteClient(":memory:");
*
* // Open a read-only database.
* const db = new SqliteClient("myfile.db", { readonly: true });
*
* // Or open using File URL
* const db = new SqliteClient(new URL("./myfile.db", import.meta.url));
* ```
*/
export class SqliteConnection implements
SqlConnection<
SqliteConnectionOptions,
SqliteParameterType,
SqliteQueryOptions
> {
readonly connectionUrl: string;
readonly options: SqliteConnectionOptions;

/**
* The FFI SQLite methods.
*/
readonly ffi = ffi;

_db: Database | null = null;

get db(): Database {
if (this._db === null) {
throw new Error("Database connection is not open");
}
return this._db;
}

set db(value: Database | null) {
this._db = value;
}

get connected(): boolean {
return Boolean(this._db?.open);
}

constructor(
connectionUrl: string | URL,
options: SqliteConnectionOptions = {},
) {
this.connectionUrl = connectionUrl instanceof URL
? fromFileUrl(connectionUrl)
: connectionUrl;
this.options = options;
}

async connect(): Promise<void> {
this.db = new Database(this.connectionUrl, this.options);
}

async close(): Promise<void> {
this._db?.close();
this._db = null;
}

execute(
sql: string,
params?: SqliteParameterType[],
_options?: SqliteQueryOptions,
): Promise<number | undefined> {
return Promise.resolve(this.db.exec(sql, ...(params || [])));
}
queryMany<T extends Row<any> = Row<any>>(
sql: string,
params?: SqliteParameterType[],
options?: SqliteQueryOptions,
): AsyncGenerator<T, any, unknown> {
return transformToAsyncGenerator(
this.db.prepare(sql).getMany<T>(params, options),
);
}
queryManyArray<T extends ArrayRow<any> = ArrayRow<any>>(
sql: string,
params?: SqliteParameterType[],
options?: SqliteQueryOptions,
): AsyncGenerator<T, any, unknown> {
return transformToAsyncGenerator(
this.db.prepare(sql).valueMany<T>(params, options),
);
}

async [Symbol.asyncDispose](): Promise<void> {
await this.close();
}

[Symbol.for("Deno.customInspect")](): string {
return `SQLite3.SqliteConnection { path: ${this.connectionUrl} }`;
}
}

export class SqliteConnectable implements
SqlConnectable<
SqliteConnectionOptions,
SqliteConnection
> {
readonly connection: SqliteConnection;
readonly options: SqliteConnectionOptions;
get connected(): boolean {
return this.connection.connected;
}

constructor(
connection: SqliteConnectable["connection"],
options: SqliteConnectable["options"] = {},
) {
this.connection = connection;
this.options = options;
}
[Symbol.asyncDispose](): Promise<void> {
return this.connection.close();
}
}
Loading

0 comments on commit b1d0b07

Please sign in to comment.