Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented stdext/sql interfaces #135

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading