Skip to content

Commit

Permalink
[bun:sqlite] Support unprefixed bindings, safe integers / BigInt, `as…
Browse files Browse the repository at this point in the history
…(Class)` (#11887)
  • Loading branch information
Jarred-Sumner authored Jun 17, 2024
1 parent fa952b1 commit 7719207
Show file tree
Hide file tree
Showing 9 changed files with 1,203 additions and 228 deletions.
178 changes: 176 additions & 2 deletions docs/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ Features include:
- Parameters (named & positional)
- Prepared statements
- Datatype conversions (`BLOB` becomes `Uint8Array`)
- World's Tiniest ORM (`query.as(MyClass)`)
- The fastest performance of any SQLite driver for JavaScript
- `bigint` support
- Multi-query statements (e.g. `SELECT 1; SELECT 2;`) in a single call to database.run(query)

The `bun:sqlite` module is roughly 3-6x faster than `better-sqlite3` and 8-9x faster than `deno.land/x/sqlite` for read queries. Each driver was benchmarked against the [Northwind Traders](https://github.com/jpwhite3/northwind-SQLite3/blob/46d5f8a64f396f87cd374d1600dbf521523980e8/Northwind_large.sqlite.zip) dataset. View and run the [benchmark source](https://github.com/oven-sh/bun/tree/main/bench/sqlite).

Expand Down Expand Up @@ -57,6 +60,29 @@ import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });
```

### Strict mode

{% callout %}
Added in Bun v1.1.14
{% /callout %}

By default, `bun:sqlite` requires binding parameters to include the `$`, `:`, or `@` prefix, and does not throw an error if a parameter is missing.

To instead throw an error when a parameter is missing and allow binding without a prefix, set `strict: true` on the `Database` constructor:

```ts
import { Database } from "bun:sqlite";

const strict = new Database(":memory:", { strict: true });
const notStrict = new Database(":memory:");

// does not throw error:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });

// throws error because of the typo:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
```

### Load via ES module import

You can also use an import attribute to load a database.
Expand Down Expand Up @@ -174,6 +200,47 @@ const query = db.query(`SELECT $param1, $param2;`);

Values are bound to these parameters when the query is executed. A `Statement` can be executed with several different methods, each returning the results in a different form.

### Binding values

To bind values to a statement, pass an object to the `.all()`, `.get()`, `.run()`, or `.values()` method.

```ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });
```

You can bind using positional parameters too:

```ts
const query = db.query(`select ?1;`);
query.all("Hello world");
```

#### `strict: true` lets you bind values without prefixes

{% callout %}
Added in Bun v1.1.14
{% /callout %}

By default, the `$`, `:`, and `@` prefixes are **included** when binding values to named parameters. To bind without these prefixes, use the `bind`

```ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", {
// bind values without prefixes
strict: true,
});

const query = db.query(`select $message;`);

// strict: true
query.all({ message: "Hello world" });

// strict: false
// query.all({ $message: "Hello world" });
```

### `.all()`

Use `.all()` to run a query and get back the results as an array of objects.
Expand Down Expand Up @@ -205,11 +272,49 @@ Use `.run()` to run a query and get back `undefined`. This is useful for schema-
```ts
const query = db.query(`create table foo;`);
query.run();
// => undefined
// {
// lastInsertRowid: 0,
// changes: 0,
// }
```

Internally, this calls [`sqlite3_reset`](https://www.sqlite.org/capi3ref.html#sqlite3_reset) and calls [`sqlite3_step`](https://www.sqlite.org/capi3ref.html#sqlite3_step) once. Stepping through all the rows is not necessary when you don't care about the results.

{% callout %}
Since Bun v1.1.14, `.run()` returns an object with two properties: `lastInsertRowid` and `changes`.
{% /callout %}

The `lastInsertRowid` property returns the ID of the last row inserted into the database. The `changes` property is the number of rows affected by the query.

### `.as(Class)` - World's smallest ORM

{% callout %}
Added in Bun v1.1.14
{% /callout %}

Use `.as(Class)` to run a query and get back the results as instances of a class. This lets you attach methods & getters/setters to results.

```ts
class Movie {
title: string;
year: number;

get isMarvel() {
return this.title.includes("Marvel");
}
}

const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();
console.log(movies[0].isMarvel); // => true
console.log(first.isMarvel); // => true
```

As a performance optimization, the class constructor is not called, default initializers are not run, and private fields are not accessible. This is more like using `Object.create` than `new`. The class's prototype is assigned to the object, methods are attached, and getters/setters are set up, but the constructor is not called.

The database columns are set as properties on the class instance.

### `.values()`

Use `values()` to run a query and get back all results as an array of arrays.
Expand Down Expand Up @@ -300,6 +405,65 @@ const results = query.all("hello", "goodbye");

{% /codetabs %}

## Integers

sqlite supports signed 64 bit integers, but JavaScript only supports signed 52 bit integers or arbitrary precision integers with `bigint`.

`bigint` input is supported everywhere, but by default `bun:sqlite` returns integers as `number` types. If you need to handle integers larger than 2^53, set `safeInteger` option to `true` when creating a `Database` instance. This also validates that `bigint` passed to `bun:sqlite` do not exceed 64 bits.

By default, `bun:sqlite` returns integers as `number` types. If you need to handle integers larger than 2^53, you can use the `bigint` type.

### `safeIntegers: true`

{% callout %}
Added in Bun v1.1.14
{% /callout %}

When `safeIntegers` is `true`, `bun:sqlite` will return integers as `bigint` types:

```ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(
`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`,
);
const result = query.get();
console.log(result.max_int); // => 9007199254741093n
```

When `safeIntegers` is `true`, `bun:sqlite` will throw an error if a `bigint` value in a bound parameter exceeds 64 bits:

```ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");

const query = db.query("INSERT INTO test (value) VALUES ($value)");

try {
query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
console.log(e.message); // => BigInt value '81129638414606663681390495662081' is out of range
}
```

### `safeIntegers: false` (default)

When `safeIntegers` is `false`, `bun:sqlite` will return integers as `number` types and truncate any bits beyond 53:

```ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(
`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`,
);
const result = query.get();
console.log(result.max_int); // => 9007199254741092
```

## Transactions

Transactions are a mechanism for executing multiple queries in an _atomic_ way; that is, either all of the queries succeed or none of them do. Create a transaction with the `db.transaction()` method:
Expand Down Expand Up @@ -447,12 +611,20 @@ class Database {
);

query<Params, ReturnType>(sql: string): Statement<Params, ReturnType>;
run(
sql: string,
params?: SQLQueryBindings,
): { lastInsertRowid: number; changes: number };
exec = this.run;
}

class Statement<Params, ReturnType> {
all(params: Params): ReturnType[];
get(params: Params): ReturnType | undefined;
run(params: Params): void;
run(params: Params): {
lastInsertRowid: number;
changes: number;
};
values(params: Params): unknown[][];

finalize(): void; // destroy statement and clean up resources
Expand All @@ -461,6 +633,8 @@ class Statement<Params, ReturnType> {
columnNames: string[]; // the column names of the result set
paramsCount: number; // the number of parameters expected by the statement
native: any; // the native object representing the statement

as(Class: new () => ReturnType): this;
}

type SQLQueryBindings =
Expand Down
95 changes: 92 additions & 3 deletions packages/bun-types/sqlite.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ declare module "bun:sqlite" {
* Equivalent to {@link constants.SQLITE_OPEN_READWRITE}
*/
readwrite?: boolean;

/**
* When set to `true`, integers are returned as `bigint` types.
*
* When set to `false`, integers are returned as `number` types and truncated to 52 bits.
*
* @default false
* @since v1.1.14
*/
safeInteger?: boolean;

/**
* When set to `false` or `undefined`:
* - Queries missing bound parameters will NOT throw an error
* - Bound named parameters in JavaScript need to exactly match the SQL query.
*
* @example
* ```ts
* const db = new Database(":memory:", { strict: false });
* db.run("INSERT INTO foo (name) VALUES ($name)", { $name: "foo" });
* ```
*
* When set to `true`:
* - Queries missing bound parameters will throw an error
* - Bound named parameters in JavaScript no longer need to be `$`, `:`, or `@`. The SQL query will remain prefixed.
*
* @example
* ```ts
* const db = new Database(":memory:", { strict: true });
* db.run("INSERT INTO foo (name) VALUES ($name)", { name: "foo" });
* ```
* @since v1.1.14
*/
strict?: boolean;
},
);

Expand Down Expand Up @@ -165,11 +199,11 @@ declare module "bun:sqlite" {
* | `bigint` | `INTEGER` |
* | `null` | `NULL` |
*/
run<ParamsType extends SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): void;
run<ParamsType extends SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): Changes;
/**
This is an alias of {@link Database.prototype.run}
*/
exec<ParamsType extends SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): void;
exec<ParamsType extends SQLQueryBindings[]>(sqlQuery: string, ...bindings: ParamsType[]): Changes;

/**
* Compile a SQL query and return a {@link Statement} object. This is the
Expand Down Expand Up @@ -575,7 +609,7 @@ declare module "bun:sqlite" {
* | `bigint` | `INTEGER` |
* | `null` | `NULL` |
*/
run(...params: ParamsType): void;
run(...params: ParamsType): Changes;

/**
* Execute the prepared statement and return the results as an array of arrays.
Expand Down Expand Up @@ -680,6 +714,44 @@ declare module "bun:sqlite" {
*/
toString(): string;

/**
*
* Make {@link get} and {@link all} return an instance of the provided
* `Class` instead of the default `Object`.
*
* @param Class A class to use
* @returns The same statement instance, modified to return an instance of `Class`
*
* This lets you attach methods, getters, and setters to the returned
* objects.
*
* For performance reasons, constructors for classes are not called, which means
* initializers will not be called and private fields will not be
* accessible.
*
* @example
*
* ## Custom class
* ```ts
* class User {
* rawBirthdate: string;
* get birthdate() {
* return new Date(this.rawBirthdate);
* }
* }
*
* const db = new Database(":memory:");
* db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, rawBirthdate TEXT)");
* db.run("INSERT INTO users (rawBirthdate) VALUES ('1995-12-19')");
* const query = db.query("SELECT * FROM users");
* query.as(User);
* const user = query.get();
* console.log(user.birthdate);
* // => Date(1995, 11, 19)
* ```
*/
as<T = unknown>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;

/**
* Native object representing the underlying `sqlite3_stmt`
*
Expand Down Expand Up @@ -1040,4 +1112,21 @@ declare module "bun:sqlite" {
*/
readonly byteOffset: number;
}

/**
* An object representing the changes made to the database since the last `run` or `exec` call.
*
* @since Bun v1.1.14
*/
interface Changes {
/**
* The number of rows changed by the last `run` or `exec` call.
*/
changes: number;

/**
* If `safeIntegers` is `true`, this is a `bigint`. Otherwise, it is a `number`.
*/
lastInsertRowid: number | bigint;
}
}
Loading

0 comments on commit 7719207

Please sign in to comment.