diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9cc2d04820..bdeb6c497a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -22,6 +22,9 @@
"BUN_DEBUG_QUIET_LOGS": "1",
"BUN_DEBUG_jest": "1",
"BUN_GARBAGE_COLLECTOR_LEVEL": "1",
+ // "BUN_JSC_validateExceptionChecks": "1",
+ // "BUN_JSC_dumpSimulatedThrows": "1",
+ // "BUN_JSC_unexpectedExceptionStackTraceLimit": "20",
},
"console": "internalConsole",
"sourceMap": {
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 717cb88fd5..c2c967c663 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -168,5 +168,5 @@
"WebKit/WebInspectorUI": true,
},
"git.detectSubmodules": false,
- "bun.test.customScript": "./build/debug/bun-debug test"
+ "bun.test.customScript": "./build/debug/bun-debug test",
}
diff --git a/bun.lock b/bun.lock
index cb7e8f42c2..a6281bfcfb 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,6 +6,7 @@
"devDependencies": {
"@lezer/common": "^1.2.3",
"@lezer/cpp": "^1.1.3",
+ "@types/bun": "workspace:*",
"bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8",
"esbuild": "^0.21.4",
"mitata": "^0.1.11",
diff --git a/cmake/sources/JavaScriptSources.txt b/cmake/sources/JavaScriptSources.txt
index f6a44973e0..1ae3a19d0e 100644
--- a/cmake/sources/JavaScriptSources.txt
+++ b/cmake/sources/JavaScriptSources.txt
@@ -65,6 +65,12 @@ src/js/internal/linkedlist.ts
src/js/internal/primordials.js
src/js/internal/promisify.ts
src/js/internal/shared.ts
+src/js/internal/sql/errors.ts
+src/js/internal/sql/postgres.ts
+src/js/internal/sql/query.ts
+src/js/internal/sql/shared.ts
+src/js/internal/sql/sqlite.ts
+src/js/internal/sql/utils.ts
src/js/internal/stream.promises.ts
src/js/internal/stream.ts
src/js/internal/streams/add-abort-signal.ts
diff --git a/docs/api/sql.md b/docs/api/sql.md
index 2b25208aa2..e033fe0916 100644
--- a/docs/api/sql.md
+++ b/docs/api/sql.md
@@ -1,20 +1,20 @@
-Bun provides native bindings for working with PostgreSQL databases with a modern, Promise-based API. The interface is designed to be simple and performant, using tagged template literals for queries and offering features like connection pooling, transactions, and prepared statements.
+Bun provides native bindings for working with SQL databases through a unified Promise-based API that supports both PostgreSQL and SQLite. The interface is designed to be simple and performant, using tagged template literals for queries and offering features like connection pooling, transactions, and prepared statements.
```ts
-import { sql } from "bun";
+import { sql, SQL } from "bun";
+// PostgreSQL (default)
const users = await sql`
SELECT * FROM users
WHERE active = ${true}
LIMIT ${10}
`;
-// Select with multiple conditions
-const activeUsers = await sql`
- SELECT *
- FROM users
- WHERE active = ${true}
- AND age >= ${18}
+// With a a SQLite db
+const sqlite = new SQL("sqlite://myapp.db");
+const results = await sqlite`
+ SELECT * FROM users
+ WHERE active = ${1}
`;
```
@@ -44,6 +44,115 @@ const activeUsers = await sql`
{% /features %}
+## Database Support
+
+Bun.SQL provides a unified API for multiple database systems:
+
+### PostgreSQL
+
+PostgreSQL is used when:
+
+- The connection string doesn't match SQLite patterns (it's the fallback adapter)
+- The connection string explicitly uses `postgres://` or `postgresql://` protocols
+- No connection string is provided and environment variables point to PostgreSQL
+
+```ts
+import { sql } from "bun";
+// Uses PostgreSQL if DATABASE_URL is not set or is a PostgreSQL URL
+await sql`SELECT ...`;
+
+import { SQL } from "bun";
+const pg = new SQL("postgres://user:pass@localhost:5432/mydb");
+await pg`SELECT ...`;
+```
+
+### SQLite
+
+SQLite support is now built into Bun.SQL, providing the same tagged template literal interface as PostgreSQL:
+
+```ts
+import { SQL } from "bun";
+
+// In-memory database
+const memory = new SQL(":memory:");
+const memory2 = new SQL("sqlite://:memory:");
+
+// File-based database
+const db = new SQL("sqlite://myapp.db");
+
+// Using options object
+const db2 = new SQL({
+ adapter: "sqlite",
+ filename: "./data/app.db",
+});
+
+// For simple filenames, specify adapter explicitly
+const db3 = new SQL("myapp.db", { adapter: "sqlite" });
+```
+
+
+SQLite Connection String Formats
+
+SQLite accepts various URL formats for connection strings:
+
+```ts
+// Standard sqlite:// protocol
+new SQL("sqlite://path/to/database.db");
+new SQL("sqlite:path/to/database.db"); // Without slashes
+
+// file:// protocol (also recognized as SQLite)
+new SQL("file://path/to/database.db");
+new SQL("file:path/to/database.db");
+
+// Special :memory: database
+new SQL(":memory:");
+new SQL("sqlite://:memory:");
+new SQL("file://:memory:");
+
+// Relative and absolute paths
+new SQL("sqlite://./local.db"); // Relative to current directory
+new SQL("sqlite://../parent/db.db"); // Parent directory
+new SQL("sqlite:///absolute/path.db"); // Absolute path
+
+// With query parameters
+new SQL("sqlite://data.db?mode=ro"); // Read-only mode
+new SQL("sqlite://data.db?mode=rw"); // Read-write mode (no create)
+new SQL("sqlite://data.db?mode=rwc"); // Read-write-create mode (default)
+```
+
+**Note:** Simple filenames without a protocol (like `"myapp.db"`) require explicitly specifying `{ adapter: "sqlite" }` to avoid ambiguity with PostgreSQL.
+
+
+
+
+SQLite-Specific Options
+
+SQLite databases support additional configuration options:
+
+```ts
+const db = new SQL({
+ adapter: "sqlite",
+ filename: "app.db",
+
+ // SQLite-specific options
+ readonly: false, // Open in read-only mode
+ create: true, // Create database if it doesn't exist
+ readwrite: true, // Open for reading and writing
+
+ // Additional Bun:sqlite options
+ strict: true, // Enable strict mode
+ safeIntegers: false, // Use JavaScript numbers for integers
+});
+```
+
+Query parameters in the URL are parsed to set these options:
+
+- `?mode=ro` → `readonly: true`
+- `?mode=rw` → `readonly: false, create: false`
+- `?mode=rwc` → `readonly: false, create: true` (default)
+
+
+
### Inserting data
You can pass JavaScript values directly to the SQL template literal and escaping will be handled for you.
@@ -251,14 +360,55 @@ await query;
## Database Environment Variables
-`sql` connection parameters can be configured using environment variables. The client checks these variables in a specific order of precedence.
+`sql` connection parameters can be configured using environment variables. The client checks these variables in a specific order of precedence and automatically detects the database type based on the connection string format.
-The following environment variables can be used to define the connection URL:
+### Automatic Database Detection
+
+When using `Bun.sql()` without arguments or `new SQL()` with a connection string, the adapter is automatically detected based on the URL format. SQLite becomes the default adapter in these cases:
+
+#### SQLite Auto-Detection
+
+SQLite is automatically selected when the connection string matches these patterns:
+
+- `:memory:` - In-memory database
+- `sqlite://...` - SQLite protocol URLs
+- `sqlite:...` - SQLite protocol without slashes
+- `file://...` - File protocol URLs
+- `file:...` - File protocol without slashes
+
+```ts
+// These all use SQLite automatically (no adapter needed)
+const sql1 = new SQL(":memory:");
+const sql2 = new SQL("sqlite://app.db");
+const sql3 = new SQL("file://./database.db");
+
+// Works with DATABASE_URL environment variable
+DATABASE_URL=":memory:" bun run app.js
+DATABASE_URL="sqlite://myapp.db" bun run app.js
+DATABASE_URL="file://./data/app.db" bun run app.js
+```
+
+#### PostgreSQL Auto-Detection
+
+PostgreSQL is the default for all other connection strings:
+
+```bash
+# PostgreSQL is detected for these patterns
+DATABASE_URL="postgres://user:pass@localhost:5432/mydb" bun run app.js
+DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" bun run app.js
+
+# Or any URL that doesn't match SQLite patterns
+DATABASE_URL="localhost:5432/mydb" bun run app.js
+```
+
+### PostgreSQL Environment Variables
+
+The following environment variables can be used to define the PostgreSQL connection:
| Environment Variable | Description |
| --------------------------- | ------------------------------------------ |
| `POSTGRES_URL` | Primary connection URL for PostgreSQL |
-| `DATABASE_URL` | Alternative connection URL |
+| `DATABASE_URL` | Alternative connection URL (auto-detected) |
| `PGURL` | Alternative connection URL |
| `PG_URL` | Alternative connection URL |
| `TLS_POSTGRES_DATABASE_URL` | SSL/TLS-enabled connection URL |
@@ -274,6 +424,19 @@ If no connection URL is provided, the system checks for the following individual
| `PGPASSWORD` | - | (empty) | Database password |
| `PGDATABASE` | - | username | Database name |
+### SQLite Environment Variables
+
+SQLite connections can be configured via `DATABASE_URL` when it contains a SQLite-compatible URL:
+
+```bash
+# These are all recognized as SQLite
+DATABASE_URL=":memory:"
+DATABASE_URL="sqlite://./app.db"
+DATABASE_URL="file:///absolute/path/to/db.sqlite"
+```
+
+**Note:** PostgreSQL-specific environment variables (`POSTGRES_URL`, `PGHOST`, etc.) are ignored when using SQLite.
+
## Runtime Preconnection
Bun can preconnect to PostgreSQL at startup to improve performance by establishing database connections before your application code runs. This is useful for reducing connection latency on the first database query.
@@ -293,16 +456,18 @@ The `--sql-preconnect` flag will automatically establish a PostgreSQL connection
## Connection Options
-You can configure your database connection manually by passing options to the SQL constructor:
+You can configure your database connection manually by passing options to the SQL constructor. Options vary depending on the database adapter:
+
+### PostgreSQL Options
```ts
import { SQL } from "bun";
const db = new SQL({
- // Required
+ // Connection details (adapter is auto-detected as PostgreSQL)
url: "postgres://user:pass@localhost:5432/dbname",
- // Optional configuration
+ // Alternative connection parameters
hostname: "localhost",
port: 5432,
database: "myapp",
@@ -330,14 +495,53 @@ const db = new SQL({
// Callbacks
onconnect: client => {
- console.log("Connected to database");
+ console.log("Connected to PostgreSQL");
},
onclose: client => {
- console.log("Connection closed");
+ console.log("PostgreSQL connection closed");
},
});
```
+### SQLite Options
+
+```ts
+import { SQL } from "bun";
+
+const db = new SQL({
+ // Required for SQLite
+ adapter: "sqlite",
+ filename: "./data/app.db", // or ":memory:" for in-memory database
+
+ // SQLite-specific access modes
+ readonly: false, // Open in read-only mode
+ create: true, // Create database if it doesn't exist
+ readwrite: true, // Allow read and write operations
+
+ // SQLite data handling
+ strict: true, // Enable strict mode for better type safety
+ safeIntegers: false, // Use BigInt for integers exceeding JS number range
+
+ // Callbacks
+ onconnect: client => {
+ console.log("SQLite database opened");
+ },
+ onclose: client => {
+ console.log("SQLite database closed");
+ },
+});
+```
+
+
+SQLite Connection Notes
+
+- **Connection Pooling**: SQLite doesn't use connection pooling as it's a file-based database. Each `SQL` instance represents a single connection.
+- **Transactions**: SQLite supports nested transactions through savepoints, similar to PostgreSQL.
+- **Concurrent Access**: SQLite handles concurrent access through file locking. Use WAL mode for better concurrency.
+- **Memory Databases**: Using `:memory:` creates a temporary database that exists only for the connection lifetime.
+
+
+
## Dynamic passwords
When clients need to use alternative authentication schemes such as access tokens or connections to databases with rotating passwords, provide either a synchronous or asynchronous function that will resolve the dynamic password value at connection time.
@@ -353,11 +557,66 @@ const sql = new SQL(url, {
});
```
+## SQLite-Specific Features
+
+### Query Execution
+
+SQLite executes queries synchronously, unlike PostgreSQL which uses asynchronous I/O. However, the API remains consistent using Promises:
+
+```ts
+const sqlite = new SQL("sqlite://app.db");
+
+// Works the same as PostgreSQL, but executes synchronously under the hood
+const users = await sqlite`SELECT * FROM users`;
+
+// Parameters work identically
+const user = await sqlite`SELECT * FROM users WHERE id = ${userId}`;
+```
+
+### SQLite Pragmas
+
+You can use PRAGMA statements to configure SQLite behavior:
+
+```ts
+const sqlite = new SQL("sqlite://app.db");
+
+// Enable foreign keys
+await sqlite`PRAGMA foreign_keys = ON`;
+
+// Set journal mode to WAL for better concurrency
+await sqlite`PRAGMA journal_mode = WAL`;
+
+// Check integrity
+const integrity = await sqlite`PRAGMA integrity_check`;
+```
+
+### Data Type Differences
+
+SQLite has a more flexible type system than PostgreSQL:
+
+```ts
+// SQLite stores data in 5 storage classes: NULL, INTEGER, REAL, TEXT, BLOB
+const sqlite = new SQL("sqlite://app.db");
+
+// SQLite is more lenient with types
+await sqlite`
+ CREATE TABLE flexible (
+ id INTEGER PRIMARY KEY,
+ data TEXT, -- Can store numbers as strings
+ value NUMERIC, -- Can store integers, reals, or text
+ blob BLOB -- Binary data
+ )
+`;
+
+// JavaScript values are automatically converted
+await sqlite`INSERT INTO flexible VALUES (${1}, ${"text"}, ${123.45}, ${Buffer.from("binary")})`;
+```
+
## Transactions
-To start a new transaction, use `sql.begin`. This method reserves a dedicated connection for the duration of the transaction and provides a scoped `sql` instance to use within the callback function. Once the callback completes, `sql.begin` resolves with the return value of the callback.
+To start a new transaction, use `sql.begin`. This method works for both PostgreSQL and SQLite. For PostgreSQL, it reserves a dedicated connection from the pool. For SQLite, it begins a transaction on the single connection.
-The `BEGIN` command is sent automatically, including any optional configurations you specify. If an error occurs during the transaction, a `ROLLBACK` is triggered to release the reserved connection and ensure the process continues smoothly.
+The `BEGIN` command is sent automatically, including any optional configurations you specify. If an error occurs during the transaction, a `ROLLBACK` is triggered to ensure the process continues smoothly.
### Basic Transactions
@@ -552,9 +811,34 @@ Note that disabling prepared statements may impact performance for queries that
## Error Handling
-The client provides typed errors for different failure scenarios:
+The client provides typed errors for different failure scenarios. Errors are database-specific and extend from base error classes:
-### Connection Errors
+### Error Classes
+
+```ts
+import { SQL } from "bun";
+
+try {
+ await sql`SELECT * FROM users`;
+} catch (error) {
+ if (error instanceof SQL.PostgresError) {
+ // PostgreSQL-specific error
+ console.log(error.code); // PostgreSQL error code
+ console.log(error.detail); // Detailed error message
+ console.log(error.hint); // Helpful hint from PostgreSQL
+ } else if (error instanceof SQL.SQLiteError) {
+ // SQLite-specific error
+ console.log(error.code); // SQLite error code (e.g., "SQLITE_CONSTRAINT")
+ console.log(error.errno); // SQLite error number
+ console.log(error.byteOffset); // Byte offset in SQL statement (if available)
+ } else if (error instanceof SQL.SQLError) {
+ // Generic SQL error (base class)
+ console.log(error.message);
+ }
+}
+```
+
+### PostgreSQL Connection Errors
| Connection Errors | Description |
| --------------------------------- | ---------------------------------------------------- |
@@ -619,6 +903,50 @@ The client provides typed errors for different failure scenarios:
| `ERR_POSTGRES_UNSAFE_TRANSACTION` | Unsafe transaction operation detected |
| `ERR_POSTGRES_INVALID_TRANSACTION_STATE` | Invalid transaction state |
+### SQLite-Specific Errors
+
+SQLite errors provide error codes and numbers that correspond to SQLite's standard error codes:
+
+
+Common SQLite Error Codes
+
+| Error Code | errno | Description |
+| ------------------- | ----- | ---------------------------------------------------- |
+| `SQLITE_CONSTRAINT` | 19 | Constraint violation (UNIQUE, CHECK, NOT NULL, etc.) |
+| `SQLITE_BUSY` | 5 | Database is locked |
+| `SQLITE_LOCKED` | 6 | Table in the database is locked |
+| `SQLITE_READONLY` | 8 | Attempt to write to a readonly database |
+| `SQLITE_IOERR` | 10 | Disk I/O error |
+| `SQLITE_CORRUPT` | 11 | Database disk image is malformed |
+| `SQLITE_FULL` | 13 | Database or disk is full |
+| `SQLITE_CANTOPEN` | 14 | Unable to open database file |
+| `SQLITE_PROTOCOL` | 15 | Database lock protocol error |
+| `SQLITE_SCHEMA` | 17 | Database schema has changed |
+| `SQLITE_TOOBIG` | 18 | String or BLOB exceeds size limit |
+| `SQLITE_MISMATCH` | 20 | Data type mismatch |
+| `SQLITE_MISUSE` | 21 | Library used incorrectly |
+| `SQLITE_AUTH` | 23 | Authorization denied |
+
+Example error handling:
+
+```ts
+const sqlite = new SQL("sqlite://app.db");
+
+try {
+ await sqlite`INSERT INTO users (id, name) VALUES (1, 'Alice')`;
+ await sqlite`INSERT INTO users (id, name) VALUES (1, 'Bob')`; // Duplicate ID
+} catch (error) {
+ if (error instanceof SQL.SQLiteError) {
+ if (error.code === "SQLITE_CONSTRAINT") {
+ console.log("Constraint violation:", error.message);
+ // Handle unique constraint violation
+ }
+ }
+}
+```
+
+
+
## Numbers and BigInt
Bun's SQL client includes special handling for large numbers that exceed the range of a 53-bit integer. Here's how it works:
@@ -652,7 +980,6 @@ There's still some things we haven't finished yet.
- Connection preloading via `--db-preconnect` Bun CLI flag
- MySQL support: [we're working on it](https://github.com/oven-sh/bun/pull/15274)
-- SQLite support: planned, but not started. Ideally, we implement it natively instead of wrapping `bun:sqlite`.
- Column name transforms (e.g. `snake_case` to `camelCase`). This is mostly blocked on a unicode-aware implementation of changing the case in C++ using WebKit's `WTF::String`.
- Column type transforms
diff --git a/package.json b/package.json
index 37679ba081..7ed45840be 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,10 @@
"./packages/@types/bun"
],
"devDependencies": {
- "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8",
"@lezer/common": "^1.2.3",
"@lezer/cpp": "^1.1.3",
+ "@types/bun": "workspace:*",
+ "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8",
"esbuild": "^0.21.4",
"mitata": "^0.1.11",
"peechy": "0.4.34",
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index bb24cbb32c..6e950b768f 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -14,7 +14,6 @@
* This module aliases `globalThis.Bun`.
*/
declare module "bun" {
- type DistributedOmit = T extends T ? Omit : never;
type PathLike = string | NodeJS.TypedArray | ArrayBufferLike | URL;
type ArrayBufferView =
| NodeJS.TypedArray
@@ -68,39 +67,31 @@ declare module "bun" {
? T
: Otherwise // Not defined in lib dom (or anywhere else), so no conflict. We can safely use our own definition
: Otherwise; // Lib dom not loaded anyway, so no conflict. We can safely use our own definition
+
+ /**
+ * Like Omit, but correctly distributes over unions. Most useful for removing
+ * properties from union options objects, like {@link Bun.SQL.Options}
+ *
+ * @example
+ * ```ts
+ * type X = Bun.DistributedOmit<{type?: 'a', url?: string} | {type?: 'b', flag?: boolean}, "url">
+ * // `{type?: 'a'} | {type?: 'b', flag?: boolean}` (Omit applied to each union item instead of entire type)
+ *
+ * type X = Omit<{type?: 'a', url?: string} | {type?: 'b', flag?: boolean}, "url">;
+ * // `{type?: "a" | "b" | undefined}` (Missing `flag` property and no longer a union)
+ * ```
+ */
+ type DistributedOmit = T extends T ? Omit : never;
+
+ type KeysInBoth = Extract;
+ type MergeInner = Omit> &
+ Omit> & {
+ [Key in KeysInBoth]: A[Key] | B[Key];
+ };
+ type Merge = MergeInner & MergeInner;
+ type DistributedMerge = T extends T ? Merge> : never;
}
- /** @deprecated This type is unused in Bun's types and might be removed in the near future */
- type Platform =
- | "aix"
- | "android"
- | "darwin"
- | "freebsd"
- | "haiku"
- | "linux"
- | "openbsd"
- | "sunos"
- | "win32"
- | "cygwin"
- | "netbsd";
-
- /** @deprecated This type is unused in Bun's types and might be removed in the near future */
- type Architecture = "arm" | "arm64" | "ia32" | "mips" | "mipsel" | "ppc" | "ppc64" | "s390" | "s390x" | "x64";
-
- /** @deprecated This type is unused in Bun's types and might be removed in the near future */
- type UncaughtExceptionListener = (error: Error, origin: UncaughtExceptionOrigin) => void;
-
- /**
- * Most of the time the unhandledRejection will be an Error, but this should not be relied upon
- * as *anything* can be thrown/rejected, it is therefore unsafe to assume that the value is an Error.
- *
- * @deprecated This type is unused in Bun's types and might be removed in the near future
- */
- type UnhandledRejectionListener = (reason: unknown, promise: Promise) => void;
-
- /** @deprecated This type is unused in Bun's types and might be removed in the near future */
- type MultipleResolveListener = (type: MultipleResolveType, promise: Promise, value: unknown) => void;
-
interface ErrorEventInit extends EventInit {
colno?: number;
error?: any;
@@ -1276,678 +1267,6 @@ declare module "bun" {
stat(): Promise;
}
- namespace SQL {
- type AwaitPromisesArray>> = {
- [K in keyof T]: Awaited;
- };
-
- type ContextCallbackResult = T extends Array> ? AwaitPromisesArray : Awaited;
- type ContextCallback = (sql: SQL) => Promise;
-
- /**
- * Configuration options for SQL client connection and behavior
- *
- * @example
- * ```ts
- * const config: Bun.SQL.Options = {
- * host: 'localhost',
- * port: 5432,
- * user: 'dbuser',
- * password: 'secretpass',
- * database: 'myapp',
- * idleTimeout: 30,
- * max: 20,
- * onconnect: (client) => {
- * console.log('Connected to database');
- * }
- * };
- * ```
- */
- interface Options {
- /**
- * Connection URL (can be string or URL object)
- */
- url?: URL | string | undefined;
-
- /**
- * Database server hostname
- * @default "localhost"
- */
- host?: string | undefined;
-
- /**
- * Database server hostname (alias for host)
- * @deprecated Prefer {@link host}
- * @default "localhost"
- */
- hostname?: string | undefined;
-
- /**
- * Database server port number
- * @default 5432
- */
- port?: number | string | undefined;
-
- /**
- * Database user for authentication
- * @default "postgres"
- */
- username?: string | undefined;
-
- /**
- * Database user for authentication (alias for username)
- * @deprecated Prefer {@link username}
- * @default "postgres"
- */
- user?: string | undefined;
-
- /**
- * Database password for authentication
- * @default ""
- */
- password?: string | (() => MaybePromise) | undefined;
-
- /**
- * Database password for authentication (alias for password)
- * @deprecated Prefer {@link password}
- * @default ""
- */
- pass?: string | (() => MaybePromise) | undefined;
-
- /**
- * Name of the database to connect to
- * @default The username value
- */
- database?: string | undefined;
-
- /**
- * Name of the database to connect to (alias for database)
- * @deprecated Prefer {@link database}
- * @default The username value
- */
- db?: string | undefined;
-
- /**
- * Database adapter/driver to use
- * @default "postgres"
- */
- adapter?: "postgres" /*| "sqlite" | "mysql"*/ | (string & {}) | undefined;
-
- /**
- * Maximum time in seconds to wait for connection to become available
- * @default 0 (no timeout)
- */
- idleTimeout?: number | undefined;
-
- /**
- * Maximum time in seconds to wait for connection to become available (alias for idleTimeout)
- * @deprecated Prefer {@link idleTimeout}
- * @default 0 (no timeout)
- */
- idle_timeout?: number | undefined;
-
- /**
- * Maximum time in seconds to wait when establishing a connection
- * @default 30
- */
- connectionTimeout?: number | undefined;
-
- /**
- * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
- * @deprecated Prefer {@link connectionTimeout}
- * @default 30
- */
- connection_timeout?: number | undefined;
-
- /**
- * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
- * @deprecated Prefer {@link connectionTimeout}
- * @default 30
- */
- connectTimeout?: number | undefined;
-
- /**
- * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
- * @deprecated Prefer {@link connectionTimeout}
- * @default 30
- */
- connect_timeout?: number | undefined;
-
- /**
- * Maximum lifetime in seconds of a connection
- * @default 0 (no maximum lifetime)
- */
- maxLifetime?: number | undefined;
-
- /**
- * Maximum lifetime in seconds of a connection (alias for maxLifetime)
- * @deprecated Prefer {@link maxLifetime}
- * @default 0 (no maximum lifetime)
- */
- max_lifetime?: number | undefined;
-
- /**
- * Whether to use TLS/SSL for the connection
- * @default false
- */
- tls?: TLSOptions | boolean | undefined;
-
- /**
- * Whether to use TLS/SSL for the connection (alias for tls)
- * @default false
- */
- ssl?: TLSOptions | boolean | undefined;
-
- // `.path` is currently unsupported in Bun, the implementation is incomplete.
- //
- // /**
- // * Unix domain socket path for connection
- // * @default ""
- // */
- // path?: string | undefined;
-
- /**
- * Callback function executed when a connection is established
- */
- onconnect?: ((client: SQL) => void) | undefined;
-
- /**
- * Callback function executed when a connection is closed
- */
- onclose?: ((client: SQL) => void) | undefined;
-
- /**
- * Postgres client runtime configuration options
- *
- * @see https://www.postgresql.org/docs/current/runtime-config-client.html
- */
- connection?: Record | undefined;
-
- /**
- * Maximum number of connections in the pool
- * @default 10
- */
- max?: number | undefined;
-
- /**
- * By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts.
- * @default false
- */
- bigint?: boolean | undefined;
-
- /**
- * Automatic creation of prepared statements
- * @default true
- */
- prepare?: boolean | undefined;
- }
-
- /**
- * Represents a SQL query that can be executed, with additional control methods
- * Extends Promise to allow for async/await usage
- */
- interface Query extends Promise {
- /**
- * Indicates if the query is currently executing
- */
- active: boolean;
-
- /**
- * Indicates if the query has been cancelled
- */
- cancelled: boolean;
-
- /**
- * Cancels the executing query
- */
- cancel(): Query;
-
- /**
- * Executes the query as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons
- */
- simple(): Query;
-
- /**
- * Executes the query
- */
- execute(): Query;
-
- /**
- * Returns the raw query result
- */
- raw(): Query;
-
- /**
- * Returns only the values from the query result
- */
- values(): Query;
- }
-
- /**
- * Callback function type for transaction contexts
- * @param sql Function to execute SQL queries within the transaction
- */
- type TransactionContextCallback = ContextCallback;
-
- /**
- * Callback function type for savepoint contexts
- * @param sql Function to execute SQL queries within the savepoint
- */
- type SavepointContextCallback = ContextCallback;
-
- /**
- * SQL.Helper represents a parameter or serializable
- * value inside of a query.
- *
- * @example
- * ```ts
- * const helper = sql(users, 'id');
- * await sql`insert into users ${helper}`;
- * ```
- */
- interface Helper {
- readonly value: T[];
- readonly columns: (keyof T)[];
- }
- }
-
- /**
- * Main SQL client interface providing connection and transaction management
- */
- interface SQL extends AsyncDisposable {
- /**
- * Executes a SQL query using template literals
- * @example
- * ```ts
- * const [user] = await sql`select * from users where id = ${1}`;
- * ```
- */
- (strings: TemplateStringsArray, ...values: unknown[]): SQL.Query;
-
- /**
- * Execute a SQL query using a string
- *
- * @example
- * ```ts
- * const users = await sql`SELECT * FROM users WHERE id = ${1}`;
- * ```
- */
- (string: string): SQL.Query;
-
- /**
- * Helper function for inserting an object into a query
- *
- * @example
- * ```ts
- * // Insert an object
- * const result = await sql`insert into users ${sql(users)} returning *`;
- *
- * // Or pick specific columns
- * const result = await sql`insert into users ${sql(users, "id", "name")} returning *`;
- *
- * // Or a single object
- * const result = await sql`insert into users ${sql(user)} returning *`;
- * ```
- */
- (obj: T | T[] | readonly T[]): SQL.Helper;
-
- /**
- * Helper function for inserting an object into a query, supporting specific columns
- *
- * @example
- * ```ts
- * // Insert an object
- * const result = await sql`insert into users ${sql(users)} returning *`;
- *
- * // Or pick specific columns
- * const result = await sql`insert into users ${sql(users, "id", "name")} returning *`;
- *
- * // Or a single object
- * const result = await sql`insert into users ${sql(user)} returning *`;
- * ```
- */
- (
- obj: T | T[] | readonly T[],
- ...columns: readonly Keys[]
- ): SQL.Helper>;
-
- /**
- * Helper function for inserting any serializable value into a query
- *
- * @example
- * ```ts
- * const result = await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`;
- * ```
- */
- (value: T): SQL.Helper;
-
- /**
- * Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
- *
- * @param name - The name of the distributed transaction
- *
- * @example
- * ```ts
- * await sql.commitDistributed("my_distributed_transaction");
- * ```
- */
- commitDistributed(name: string): Promise;
-
- /**
- * Rolls back a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
- *
- * @param name - The name of the distributed transaction
- *
- * @example
- * ```ts
- * await sql.rollbackDistributed("my_distributed_transaction");
- * ```
- */
- rollbackDistributed(name: string): Promise;
-
- /** Waits for the database connection to be established
- *
- * @example
- * ```ts
- * await sql.connect();
- * ```
- */
- connect(): Promise;
-
- /**
- * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing.
- *
- * @param options - The options for the close
- *
- * @example
- * ```ts
- * await sql.close({ timeout: 1 });
- * ```
- */
- close(options?: { timeout?: number }): Promise;
-
- /**
- * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing.
- * This is an alias of {@link SQL.close}
- *
- * @param options - The options for the close
- *
- * @example
- * ```ts
- * await sql.end({ timeout: 1 });
- * ```
- */
- end(options?: { timeout?: number }): Promise;
-
- /**
- * Flushes any pending operations
- *
- * @example
- * ```ts
- * sql.flush();
- * ```
- */
- flush(): void;
-
- /**
- * The reserve method pulls out a connection from the pool, and returns a client that wraps the single connection.
- *
- * This can be used for running queries on an isolated connection.
- * Calling reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package).
- *
- * @example
- * ```ts
- * const reserved = await sql.reserve();
- * await reserved`select * from users`;
- * await reserved.release();
- * // with in a production scenario would be something more like
- * const reserved = await sql.reserve();
- * try {
- * // ... queries
- * } finally {
- * await reserved.release();
- * }
- *
- * // Bun supports Symbol.dispose and Symbol.asyncDispose
- * {
- * // always release after context (safer)
- * using reserved = await sql.reserve()
- * await reserved`select * from users`
- * }
- * ```
- */
- reserve(): Promise;
-
- /**
- * Begins a new transaction.
- *
- * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
- * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
- * @example
- * const [user, account] = await sql.begin(async sql => {
- * const [user] = await sql`
- * insert into users (
- * name
- * ) values (
- * 'Murray'
- * )
- * returning *
- * `
- * const [account] = await sql`
- * insert into accounts (
- * user_id
- * ) values (
- * ${ user.user_id }
- * )
- * returning *
- * `
- * return [user, account]
- * })
- */
- begin(fn: SQL.TransactionContextCallback): Promise>;
-
- /**
- * Begins a new transaction with options.
- *
- * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
- * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
- * @example
- * const [user, account] = await sql.begin("read write", async sql => {
- * const [user] = await sql`
- * insert into users (
- * name
- * ) values (
- * 'Murray'
- * )
- * returning *
- * `
- * const [account] = await sql`
- * insert into accounts (
- * user_id
- * ) values (
- * ${ user.user_id }
- * )
- * returning *
- * `
- * return [user, account]
- * })
- */
- begin(options: string, fn: SQL.TransactionContextCallback): Promise>;
-
- /**
- * Alternative method to begin a transaction.
- *
- * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
- * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
- * @alias begin
- * @example
- * const [user, account] = await sql.transaction(async sql => {
- * const [user] = await sql`
- * insert into users (
- * name
- * ) values (
- * 'Murray'
- * )
- * returning *
- * `
- * const [account] = await sql`
- * insert into accounts (
- * user_id
- * ) values (
- * ${ user.user_id }
- * )
- * returning *
- * `
- * return [user, account]
- * })
- */
- transaction(fn: SQL.TransactionContextCallback): Promise>;
-
- /**
- * Alternative method to begin a transaction with options
- * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
- * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
- *
- * @alias {@link begin}
- *
- * @example
- * const [user, account] = await sql.transaction("read write", async sql => {
- * const [user] = await sql`
- * insert into users (
- * name
- * ) values (
- * 'Murray'
- * )
- * returning *
- * `
- * const [account] = await sql`
- * insert into accounts (
- * user_id
- * ) values (
- * ${ user.user_id }
- * )
- * returning *
- * `
- * return [user, account]
- * });
- */
- transaction(options: string, fn: SQL.TransactionContextCallback): Promise>;
-
- /**
- * Begins a distributed transaction
- * Also know as Two-Phase Commit, in a distributed transaction, Phase 1 involves the coordinator preparing nodes by ensuring data is written and ready to commit, while Phase 2 finalizes with nodes committing or rolling back based on the coordinator's decision, ensuring durability and releasing locks.
- * In PostgreSQL and MySQL distributed transactions persist beyond the original session, allowing privileged users or coordinators to commit/rollback them, ensuring support for distributed transactions, recovery, and administrative tasks.
- * beginDistributed will automatic rollback if any exception are not caught, and you can commit and rollback later if everything goes well.
- * PostgreSQL natively supports distributed transactions using PREPARE TRANSACTION, while MySQL uses XA Transactions, and MSSQL also supports distributed/XA transactions. However, in MSSQL, distributed transactions are tied to the original session, the DTC coordinator, and the specific connection.
- * These transactions are automatically committed or rolled back following the same rules as regular transactions, with no option for manual intervention from other sessions, in MSSQL distributed transactions are used to coordinate transactions using Linked Servers.
- *
- * @example
- * await sql.beginDistributed("numbers", async sql => {
- * await sql`create table if not exists numbers (a int)`;
- * await sql`insert into numbers values(1)`;
- * });
- * // later you can call
- * await sql.commitDistributed("numbers");
- * // or await sql.rollbackDistributed("numbers");
- */
- beginDistributed(
- name: string,
- fn: SQL.TransactionContextCallback,
- ): Promise>;
-
- /** Alternative method to begin a distributed transaction
- * @alias {@link beginDistributed}
- */
- distributed(name: string, fn: SQL.TransactionContextCallback): Promise>;
-
- /**If you know what you're doing, you can use unsafe to pass any string you'd like.
- * Please note that this can lead to SQL injection if you're not careful.
- * You can also nest sql.unsafe within a safe sql expression. This is useful if only part of your fraction has unsafe elements.
- * @example
- * const result = await sql.unsafe(`select ${danger} from users where id = ${dragons}`)
- */
- unsafe(string: string, values?: any[]): SQL.Query;
-
- /**
- * Reads a file and uses the contents as a query.
- * Optional parameters can be used if the file includes $1, $2, etc
- * @example
- * const result = await sql.file("query.sql", [1, 2, 3]);
- */
- file(filename: string, values?: any[]): SQL.Query;
-
- /**
- * Current client options
- */
- options: SQL.Options;
- }
-
- const SQL: {
- /**
- * Creates a new SQL client instance
- *
- * @param connectionString - The connection string for the SQL client
- *
- * @example
- * ```ts
- * const sql = new SQL("postgres://localhost:5432/mydb");
- * const sql = new SQL(new URL("postgres://localhost:5432/mydb"));
- * ```
- */
- new (connectionString: string | URL): SQL;
-
- /**
- * Creates a new SQL client instance with options
- *
- * @param connectionString - The connection string for the SQL client
- * @param options - The options for the SQL client
- *
- * @example
- * ```ts
- * const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 });
- * ```
- */
- new (connectionString: string | URL, options: Omit): SQL;
-
- /**
- * Creates a new SQL client instance with options
- *
- * @param options - The options for the SQL client
- *
- * @example
- * ```ts
- * const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 });
- * ```
- */
- new (options?: SQL.Options): SQL;
- };
-
- /**
- * Represents a reserved connection from the connection pool
- * Extends SQL with additional release functionality
- */
- interface ReservedSQL extends SQL, Disposable {
- /**
- * Releases the client back to the connection pool
- */
- release(): void;
- }
-
- /**
- * Represents a client within a transaction context
- * Extends SQL with savepoint functionality
- */
- interface TransactionSQL extends SQL {
- /** Creates a savepoint within the current transaction */
- savepoint(name: string, fn: SQLSavepointContextCallback): Promise;
- savepoint(fn: SQLSavepointContextCallback): Promise;
- }
-
- /**
- * Represents a savepoint within a transaction
- */
- interface SavepointSQL extends SQL {}
-
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";
interface CSRFGenerateOptions {
@@ -1995,16 +1314,6 @@ declare module "bun" {
maxAge?: number;
}
- /**
- * SQL client
- */
- const sql: SQL;
-
- /**
- * SQL client for PostgreSQL
- */
- const postgres: SQL;
-
/**
* Generate and verify CSRF tokens
*
@@ -4383,11 +3692,11 @@ declare module "bun" {
* The type of options that can be passed to {@link serve}, with support for `routes` and a safer requirement for `fetch`
*/
type ServeFunctionOptions> }> =
- | (DistributedOmit, WebSocketServeOptions>, "fetch"> & {
+ | (__internal.DistributedOmit, WebSocketServeOptions>, "fetch"> & {
routes: R;
fetch?: (this: Server, request: Request, server: Server) => Response | Promise;
})
- | (DistributedOmit, WebSocketServeOptions>, "routes"> & {
+ | (__internal.DistributedOmit, WebSocketServeOptions>, "routes"> & {
routes?: never;
fetch: (this: Server, request: Request, server: Server) => Response | Promise;
})
diff --git a/packages/bun-types/deprecated.d.ts b/packages/bun-types/deprecated.d.ts
index 0b8cee7818..543661c473 100644
--- a/packages/bun-types/deprecated.d.ts
+++ b/packages/bun-types/deprecated.d.ts
@@ -1,4 +1,35 @@
declare module "bun" {
+ /** @deprecated This type is unused in Bun's types and might be removed in the near future */
+ type Platform =
+ | "aix"
+ | "android"
+ | "darwin"
+ | "freebsd"
+ | "haiku"
+ | "linux"
+ | "openbsd"
+ | "sunos"
+ | "win32"
+ | "cygwin"
+ | "netbsd";
+
+ /** @deprecated This type is unused in Bun's types and might be removed in the near future */
+ type Architecture = "arm" | "arm64" | "ia32" | "mips" | "mipsel" | "ppc" | "ppc64" | "s390" | "s390x" | "x64";
+
+ /** @deprecated This type is unused in Bun's types and might be removed in the near future */
+ type UncaughtExceptionListener = (error: Error, origin: UncaughtExceptionOrigin) => void;
+
+ /**
+ * Most of the time the unhandledRejection will be an Error, but this should not be relied upon
+ * as *anything* can be thrown/rejected, it is therefore unsafe to assume that the value is an Error.
+ *
+ * @deprecated This type is unused in Bun's types and might be removed in the near future
+ */
+ type UnhandledRejectionListener = (reason: unknown, promise: Promise) => void;
+
+ /** @deprecated This type is unused in Bun's types and might be removed in the near future */
+ type MultipleResolveListener = (type: MultipleResolveType, promise: Promise, value: unknown) => void;
+
/**
* Consume all data from a {@link ReadableStream} until it closes or errors.
*
diff --git a/packages/bun-types/index.d.ts b/packages/bun-types/index.d.ts
index c5b488ba22..870e2ae463 100644
--- a/packages/bun-types/index.d.ts
+++ b/packages/bun-types/index.d.ts
@@ -21,6 +21,7 @@
///
///
///
+///
///
diff --git a/packages/bun-types/overrides.d.ts b/packages/bun-types/overrides.d.ts
index f52de8acbf..b4f9f97ad1 100644
--- a/packages/bun-types/overrides.d.ts
+++ b/packages/bun-types/overrides.d.ts
@@ -24,6 +24,12 @@ declare module "stream/web" {
}
}
+declare module "url" {
+ interface URLSearchParams {
+ toJSON(): Record;
+ }
+}
+
declare global {
namespace NodeJS {
interface ProcessEnv extends Bun.Env {}
diff --git a/packages/bun-types/sql.d.ts b/packages/bun-types/sql.d.ts
new file mode 100644
index 0000000000..a85278b8c5
--- /dev/null
+++ b/packages/bun-types/sql.d.ts
@@ -0,0 +1,805 @@
+import type * as BunSQLite from "bun:sqlite";
+
+declare module "bun" {
+ /**
+ * Represents a reserved connection from the connection pool Extends SQL with
+ * additional release functionality
+ */
+ interface ReservedSQL extends SQL, Disposable {
+ /**
+ * Releases the client back to the connection pool
+ */
+ release(): void;
+ }
+
+ /**
+ * Represents a client within a transaction context Extends SQL with savepoint
+ * functionality
+ */
+ interface TransactionSQL extends SQL {
+ /**
+ * Creates a savepoint within the current transaction
+ */
+ savepoint(name: string, fn: SQL.SavepointContextCallback): Promise;
+ savepoint(fn: SQL.SavepointContextCallback): Promise;
+
+ /**
+ * The reserve method pulls out a connection from the pool, and returns a
+ * client that wraps the single connection.
+ *
+ * Using reserve() inside of a transaction will return a brand new
+ * connection, not one related to the transaction. This matches the
+ * behaviour of the `postgres` package.
+ */
+ reserve(): Promise;
+ }
+
+ namespace SQL {
+ class SQLError extends Error {
+ constructor(message: string);
+ }
+
+ class PostgresError extends SQLError {
+ public readonly code: string;
+ public readonly errno: string | undefined;
+ public readonly detail: string | undefined;
+ public readonly hint: string | undefined;
+ public readonly severity: string | undefined;
+ public readonly position: string | undefined;
+ public readonly internalPosition: string | undefined;
+ public readonly internalQuery: string | undefined;
+ public readonly where: string | undefined;
+ public readonly schema: string | undefined;
+ public readonly table: string | undefined;
+ public readonly column: string | undefined;
+ public readonly dataType: string | undefined;
+ public readonly constraint: string | undefined;
+ public readonly file: string | undefined;
+ public readonly line: string | undefined;
+ public readonly routine: string | undefined;
+
+ constructor(
+ message: string,
+ options: {
+ code: string;
+ errno?: string | undefined;
+ detail?: string;
+ hint?: string | undefined;
+ severity?: string | undefined;
+ position?: string | undefined;
+ internalPosition?: string;
+ internalQuery?: string;
+ where?: string | undefined;
+ schema?: string;
+ table?: string | undefined;
+ column?: string | undefined;
+ dataType?: string | undefined;
+ constraint?: string;
+ file?: string | undefined;
+ line?: string | undefined;
+ routine?: string | undefined;
+ },
+ );
+ }
+
+ class SQLiteError extends SQLError {
+ public readonly code: string;
+ public readonly errno: number;
+ public readonly byteOffset?: number | undefined;
+
+ constructor(message: string, options: { code: string; errno: number; byteOffset?: number | undefined });
+ }
+
+ type AwaitPromisesArray>> = {
+ [K in keyof T]: Awaited;
+ };
+
+ type ContextCallbackResult = T extends Array> ? AwaitPromisesArray : Awaited;
+ type ContextCallback = (sql: SQL) => Bun.MaybePromise;
+
+ interface SQLiteOptions extends BunSQLite.DatabaseOptions {
+ adapter?: "sqlite";
+
+ /**
+ * Specify the path to the database file
+ *
+ * Examples:
+ *
+ * - `sqlite://:memory:`
+ * - `sqlite://./path/to/database.db`
+ * - `sqlite:///Users/bun/projects/my-app/database.db`
+ * - `./dev.db`
+ * - `:memory:`
+ *
+ * @default ":memory:"
+ */
+ filename?: URL | ":memory:" | (string & {}) | undefined;
+
+ /**
+ * Callback executed when a connection attempt completes (SQLite)
+ * Receives an Error on failure, or null on success.
+ */
+ onconnect?: ((err: Error | null) => void) | undefined;
+
+ /**
+ * Callback executed when a connection is closed (SQLite)
+ * Receives the closing Error or null.
+ */
+ onclose?: ((err: Error | null) => void) | undefined;
+ }
+
+ interface PostgresOptions {
+ /**
+ * Connection URL (can be string or URL object)
+ */
+ url?: URL | string | undefined;
+
+ /**
+ * Database server hostname
+ * @default "localhost"
+ */
+ host?: string | undefined;
+
+ /**
+ * Database server hostname (alias for host)
+ * @deprecated Prefer {@link host}
+ * @default "localhost"
+ */
+ hostname?: string | undefined;
+
+ /**
+ * Database server port number
+ * @default 5432
+ */
+ port?: number | string | undefined;
+
+ /**
+ * Database user for authentication
+ * @default "postgres"
+ */
+ username?: string | undefined;
+
+ /**
+ * Database user for authentication (alias for username)
+ * @deprecated Prefer {@link username}
+ * @default "postgres"
+ */
+ user?: string | undefined;
+
+ /**
+ * Database password for authentication
+ * @default ""
+ */
+ password?: string | (() => MaybePromise) | undefined;
+
+ /**
+ * Database password for authentication (alias for password)
+ * @deprecated Prefer {@link password}
+ * @default ""
+ */
+ pass?: string | (() => MaybePromise) | undefined;
+
+ /**
+ * Name of the database to connect to
+ * @default The username value
+ */
+ database?: string | undefined;
+
+ /**
+ * Name of the database to connect to (alias for database)
+ * @deprecated Prefer {@link database}
+ * @default The username value
+ */
+ db?: string | undefined;
+
+ /**
+ * Database adapter/driver to use
+ * @default "postgres"
+ */
+ adapter?: "postgres";
+
+ /**
+ * Maximum time in seconds to wait for connection to become available
+ * @default 0 (no timeout)
+ */
+ idleTimeout?: number | undefined;
+
+ /**
+ * Maximum time in seconds to wait for connection to become available (alias for idleTimeout)
+ * @deprecated Prefer {@link idleTimeout}
+ * @default 0 (no timeout)
+ */
+ idle_timeout?: number | undefined;
+
+ /**
+ * Maximum time in seconds to wait when establishing a connection
+ * @default 30
+ */
+ connectionTimeout?: number | undefined;
+
+ /**
+ * Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
+ * @deprecated Prefer {@link connectionTimeout}
+ * @default 30
+ */
+ connection_timeout?: number | undefined;
+
+ /**
+ * Maximum time in seconds to wait when establishing a connection (alias
+ * for connectionTimeout)
+ * @deprecated Prefer {@link connectionTimeout}
+ * @default 30
+ */
+ connectTimeout?: number | undefined;
+
+ /**
+ * Maximum time in seconds to wait when establishing a connection (alias
+ * for connectionTimeout)
+ * @deprecated Prefer {@link connectionTimeout}
+ * @default 30
+ */
+ connect_timeout?: number | undefined;
+
+ /**
+ * Maximum lifetime in seconds of a connection
+ * @default 0 (no maximum lifetime)
+ */
+ maxLifetime?: number | undefined;
+
+ /**
+ * Maximum lifetime in seconds of a connection (alias for maxLifetime)
+ * @deprecated Prefer {@link maxLifetime}
+ * @default 0 (no maximum lifetime)
+ */
+ max_lifetime?: number | undefined;
+
+ /**
+ * Whether to use TLS/SSL for the connection
+ * @default false
+ */
+ tls?: TLSOptions | boolean | undefined;
+
+ /**
+ * Whether to use TLS/SSL for the connection (alias for tls)
+ * @default false
+ */
+ ssl?: TLSOptions | boolean | undefined;
+
+ // `.path` is currently unsupported in Bun, the implementation is
+ // incomplete.
+ //
+ // /**
+ // * Unix domain socket path for connection
+ // * @default ""
+ // */
+ // path?: string | undefined;
+
+ /**
+ * Callback executed when a connection attempt completes
+ * Receives an Error on failure, or null on success.
+ */
+ onconnect?: ((err: Error | null) => void) | undefined;
+
+ /**
+ * Callback executed when a connection is closed
+ * Receives the closing Error or null.
+ */
+ onclose?: ((err: Error | null) => void) | undefined;
+
+ /**
+ * Postgres client runtime configuration options
+ *
+ * @see https://www.postgresql.org/docs/current/runtime-config-client.html
+ */
+ connection?: Record | undefined;
+
+ /**
+ * Maximum number of connections in the pool
+ * @default 10
+ */
+ max?: number | undefined;
+
+ /**
+ * By default values outside i32 range are returned as strings. If this is
+ * true, values outside i32 range are returned as BigInts.
+ * @default false
+ */
+ bigint?: boolean | undefined;
+
+ /**
+ * Automatic creation of prepared statements
+ * @default true
+ */
+ prepare?: boolean | undefined;
+ }
+
+ /**
+ * Configuration options for SQL client connection and behavior
+ *
+ * @example
+ * ```ts
+ * const config: Bun.SQL.Options = {
+ * host: 'localhost',
+ * port: 5432,
+ * user: 'dbuser',
+ * password: 'secretpass',
+ * database: 'myapp',
+ * idleTimeout: 30,
+ * max: 20,
+ * onconnect: (client) => {
+ * console.log('Connected to database');
+ * }
+ * };
+ * ```
+ */
+ type Options = SQLiteOptions | PostgresOptions;
+
+ /**
+ * Represents a SQL query that can be executed, with additional control
+ * methods Extends Promise to allow for async/await usage
+ */
+ interface Query extends Promise {
+ /**
+ * Indicates if the query is currently executing
+ */
+ active: boolean;
+
+ /**
+ * Indicates if the query has been cancelled
+ */
+ cancelled: boolean;
+
+ /**
+ * Cancels the executing query
+ */
+ cancel(): Query;
+
+ /**
+ * Executes the query as a simple query, no parameters are allowed but can
+ * execute multiple commands separated by semicolons
+ */
+ simple(): Query;
+
+ /**
+ * Executes the query
+ */
+ execute(): Query;
+
+ /**
+ * Returns the raw query result
+ */
+ raw(): Query;
+
+ /**
+ * Returns only the values from the query result
+ */
+ values(): Query;
+ }
+
+ /**
+ * Callback function type for transaction contexts
+ * @param sql Function to execute SQL queries within the transaction
+ */
+ type TransactionContextCallback = ContextCallback;
+
+ /**
+ * Callback function type for savepoint contexts
+ * @param sql Function to execute SQL queries within the savepoint
+ */
+ type SavepointContextCallback = ContextCallback;
+
+ /**
+ * SQL.Helper represents a parameter or serializable
+ * value inside of a query.
+ *
+ * @example
+ * ```ts
+ * const helper = sql(users, 'id');
+ * await sql`insert into users ${helper}`;
+ * ```
+ */
+ interface Helper {
+ readonly value: T[];
+ readonly columns: (keyof T)[];
+ }
+ }
+
+ interface SQL extends AsyncDisposable {
+ /**
+ * Executes a SQL query using template literals
+ * @example
+ * ```ts
+ * const [user] = await sql`select * from users where id = ${1}`;
+ * ```
+ */
+ (strings: TemplateStringsArray, ...values: unknown[]): SQL.Query;
+
+ /**
+ * Execute a SQL query using a string
+ *
+ * @example
+ * ```ts
+ * const users = await sql`SELECT * FROM users WHERE id = ${1}`;
+ * ```
+ */
+ (string: string): SQL.Query;
+
+ /**
+ * Helper function for inserting an object into a query
+ *
+ * @example
+ * ```ts
+ * // Insert an object
+ * const result = await sql`insert into users ${sql(users)} returning *`;
+ *
+ * // Or pick specific columns
+ * const result = await sql`insert into users ${sql(users, "id", "name")} returning *`;
+ *
+ * // Or a single object
+ * const result = await sql`insert into users ${sql(user)} returning *`;
+ * ```
+ */
+ (obj: T | T[] | readonly T[]): SQL.Helper; // Contributor note: This is the same as the signature below with the exception of the columns and the Pick
+
+ /**
+ * Helper function for inserting an object into a query, supporting specific columns
+ *
+ * @example
+ * ```ts
+ * // Insert an object
+ * const result = await sql`insert into users ${sql(users)} returning *`;
+ *
+ * // Or pick specific columns
+ * const result = await sql`insert into users ${sql(users, "id", "name")} returning *`;
+ *
+ * // Or a single object
+ * const result = await sql`insert into users ${sql(user)} returning *`;
+ * ```
+ */
+ (
+ obj: T | T[] | readonly T[],
+ ...columns: readonly Keys[]
+ ): SQL.Helper>; // Contributor note: This is the same as the signature above with the exception of this signature tracking keys
+
+ /**
+ * Helper function for inserting any serializable value into a query
+ *
+ * @example
+ * ```ts
+ * const result = await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`;
+ * ```
+ */
+ (value: T): SQL.Helper;
+ }
+
+ /**
+ * Main SQL client interface providing connection and transaction management
+ */
+ class SQL {
+ /**
+ * Creates a new SQL client instance
+ *
+ * @param connectionString - The connection string for the SQL client
+ *
+ * @example
+ * ```ts
+ * const sql = new SQL("postgres://localhost:5432/mydb");
+ * const sql = new SQL(new URL("postgres://localhost:5432/mydb"));
+ * ```
+ */
+ constructor(connectionString: string | URL);
+
+ /**
+ * Creates a new SQL client instance with options
+ *
+ * @param connectionString - The connection string for the SQL client
+ * @param options - The options for the SQL client
+ *
+ * @example
+ * ```ts
+ * const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 });
+ * ```
+ */
+ constructor(
+ connectionString: string | URL,
+ options: Bun.__internal.DistributedOmit,
+ );
+
+ /**
+ * Creates a new SQL client instance with options
+ *
+ * @param options - The options for the SQL client
+ *
+ * @example
+ * ```ts
+ * const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 });
+ * ```
+ */
+ constructor(options?: SQL.Options);
+
+ /**
+ * Current client options
+ */
+ options: Bun.__internal.DistributedMerge;
+
+ /**
+ * Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
+ *
+ * @param name - The name of the distributed transaction
+ *
+ * @throws {Error} If the adapter does not support distributed transactions (e.g., SQLite)
+ *
+ * @example
+ * ```ts
+ * await sql.commitDistributed("my_distributed_transaction");
+ * ```
+ */
+ commitDistributed(name: string): Promise;
+
+ /**
+ * Rolls back a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
+ *
+ * @param name - The name of the distributed transaction
+ *
+ * @throws {Error} If the adapter does not support distributed transactions (e.g., SQLite)
+ *
+ * @example
+ * ```ts
+ * await sql.rollbackDistributed("my_distributed_transaction");
+ * ```
+ */
+ rollbackDistributed(name: string): Promise;
+
+ /** Waits for the database connection to be established
+ *
+ * @example
+ * ```ts
+ * await sql.connect();
+ * ```
+ */
+ connect(): Promise;
+
+ /**
+ * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing.
+ *
+ * @param options - The options for the close
+ *
+ * @example
+ * ```ts
+ * await sql.close({ timeout: 1 });
+ * ```
+ */
+ close(options?: { timeout?: number }): Promise;
+
+ /**
+ * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing.
+ * This is an alias of {@link SQL.close}
+ *
+ * @param options - The options for the close
+ *
+ * @example
+ * ```ts
+ * await sql.end({ timeout: 1 });
+ * ```
+ */
+ end(options?: { timeout?: number }): Promise;
+
+ /**
+ * Flushes any pending operations
+ *
+ * @throws {Error} If the adapter does not support flushing (e.g., SQLite)
+ *
+ * @example
+ * ```ts
+ * sql.flush();
+ * ```
+ */
+ flush(): void;
+
+ /**
+ * The reserve method pulls out a connection from the pool, and returns a client that wraps the single connection.
+ *
+ * This can be used for running queries on an isolated connection.
+ * Calling reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package).
+ *
+ * @throws {Error} If the adapter does not support connection pooling (e.g., SQLite)s
+ *
+ * @example
+ * ```ts
+ * const reserved = await sql.reserve();
+ * await reserved`select * from users`;
+ * await reserved.release();
+ * // with in a production scenario would be something more like
+ * const reserved = await sql.reserve();
+ * try {
+ * // ... queries
+ * } finally {
+ * await reserved.release();
+ * }
+ *
+ * // Bun supports Symbol.dispose and Symbol.asyncDispose
+ * // always release after context (safer)
+ * using reserved = await sql.reserve()
+ * await reserved`select * from users`
+ * ```
+ */
+ reserve(): Promise;
+
+ /**
+ * Begins a new transaction.
+ *
+ * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
+ * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
+ * @example
+ * const [user, account] = await sql.begin(async sql => {
+ * const [user] = await sql`
+ * insert into users (
+ * name
+ * ) values (
+ * 'Murray'
+ * )
+ * returning *
+ * `
+ * const [account] = await sql`
+ * insert into accounts (
+ * user_id
+ * ) values (
+ * ${ user.user_id }
+ * )
+ * returning *
+ * `
+ * return [user, account]
+ * })
+ */
+ begin(fn: SQL.TransactionContextCallback): Promise>;
+
+ /**
+ * Begins a new transaction with options.
+ *
+ * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
+ * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
+ * @example
+ * const [user, account] = await sql.begin("read write", async sql => {
+ * const [user] = await sql`
+ * insert into users (
+ * name
+ * ) values (
+ * 'Murray'
+ * )
+ * returning *
+ * `
+ * const [account] = await sql`
+ * insert into accounts (
+ * user_id
+ * ) values (
+ * ${ user.user_id }
+ * )
+ * returning *
+ * `
+ * return [user, account]
+ * })
+ */
+ begin(options: string, fn: SQL.TransactionContextCallback): Promise>;
+
+ /**
+ * Alternative method to begin a transaction.
+ *
+ * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
+ * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
+ * @alias begin
+ * @example
+ * const [user, account] = await sql.transaction(async sql => {
+ * const [user] = await sql`
+ * insert into users (
+ * name
+ * ) values (
+ * 'Murray'
+ * )
+ * returning *
+ * `
+ * const [account] = await sql`
+ * insert into accounts (
+ * user_id
+ * ) values (
+ * ${ user.user_id }
+ * )
+ * returning *
+ * `
+ * return [user, account]
+ * })
+ */
+ transaction(fn: SQL.TransactionContextCallback): Promise>;
+
+ /**
+ * Alternative method to begin a transaction with options
+ * Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
+ * BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
+ *
+ * @alias {@link begin}
+ *
+ * @example
+ * const [user, account] = await sql.transaction("read write", async sql => {
+ * const [user] = await sql`
+ * insert into users (
+ * name
+ * ) values (
+ * 'Murray'
+ * )
+ * returning *
+ * `
+ * const [account] = await sql`
+ * insert into accounts (
+ * user_id
+ * ) values (
+ * ${ user.user_id }
+ * )
+ * returning *
+ * `
+ * return [user, account]
+ * });
+ */
+ transaction(options: string, fn: SQL.TransactionContextCallback): Promise>;
+
+ /**
+ * Begins a distributed transaction
+ * Also know as Two-Phase Commit, in a distributed transaction, Phase 1 involves the coordinator preparing nodes by ensuring data is written and ready to commit, while Phase 2 finalizes with nodes committing or rolling back based on the coordinator's decision, ensuring durability and releasing locks.
+ * In PostgreSQL and MySQL distributed transactions persist beyond the original session, allowing privileged users or coordinators to commit/rollback them, ensuring support for distributed transactions, recovery, and administrative tasks.
+ * beginDistributed will automatic rollback if any exception are not caught, and you can commit and rollback later if everything goes well.
+ * PostgreSQL natively supports distributed transactions using PREPARE TRANSACTION, while MySQL uses XA Transactions, and MSSQL also supports distributed/XA transactions. However, in MSSQL, distributed transactions are tied to the original session, the DTC coordinator, and the specific connection.
+ * These transactions are automatically committed or rolled back following the same rules as regular transactions, with no option for manual intervention from other sessions, in MSSQL distributed transactions are used to coordinate transactions using Linked Servers.
+ *
+ * @throws {Error} If the adapter does not support distributed transactions (e.g., SQLite)
+ *
+ * @example
+ * await sql.beginDistributed("numbers", async sql => {
+ * await sql`create table if not exists numbers (a int)`;
+ * await sql`insert into numbers values(1)`;
+ * });
+ * // later you can call
+ * await sql.commitDistributed("numbers");
+ * // or await sql.rollbackDistributed("numbers");
+ */
+ beginDistributed(
+ name: string,
+ fn: SQL.TransactionContextCallback,
+ ): Promise>;
+
+ /** Alternative method to begin a distributed transaction
+ * @alias {@link beginDistributed}
+ */
+ distributed(name: string, fn: SQL.TransactionContextCallback): Promise>;
+
+ /**If you know what you're doing, you can use unsafe to pass any string you'd like.
+ * Please note that this can lead to SQL injection if you're not careful.
+ * You can also nest sql.unsafe within a safe sql expression. This is useful if only part of your fraction has unsafe elements.
+ * @example
+ * const result = await sql.unsafe(`select ${danger} from users where id = ${dragons}`)
+ */
+ unsafe(string: string, values?: any[]): SQL.Query;
+
+ /**
+ * Reads a file and uses the contents as a query.
+ * Optional parameters can be used if the file includes $1, $2, etc
+ * @example
+ * const result = await sql.file("query.sql", [1, 2, 3]);
+ */
+ file(filename: string, values?: any[]): SQL.Query;
+ }
+
+ /**
+ * SQL client
+ */
+ const sql: SQL;
+
+ /**
+ * SQL client for PostgreSQL
+ *
+ * @deprecated Prefer {@link Bun.sql}
+ */
+ const postgres: SQL;
+
+ /**
+ * Represents a savepoint within a transaction
+ */
+ interface SavepointSQL extends SQL {}
+}
diff --git a/packages/bun-types/sqlite.d.ts b/packages/bun-types/sqlite.d.ts
index 0c79d22779..47ef366629 100644
--- a/packages/bun-types/sqlite.d.ts
+++ b/packages/bun-types/sqlite.d.ts
@@ -24,6 +24,66 @@
* | `null` | `NULL` |
*/
declare module "bun:sqlite" {
+ /**
+ * Options for {@link Database}
+ */
+ export interface DatabaseOptions {
+ /**
+ * Open the database as read-only (no write operations, no create).
+ *
+ * Equivalent to {@link constants.SQLITE_OPEN_READONLY}
+ */
+ readonly?: boolean;
+
+ /**
+ * Allow creating a new database
+ *
+ * Equivalent to {@link constants.SQLITE_OPEN_CREATE}
+ */
+ create?: boolean;
+
+ /**
+ * Open the database as read-write
+ *
+ * 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
+ */
+ safeIntegers?: 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;
+ }
+
/**
* A SQLite3 database
*
@@ -53,8 +113,6 @@ declare module "bun:sqlite" {
* ```ts
* const db = new Database("mydb.sqlite", {readonly: true});
* ```
- *
- * @category Database
*/
export class Database implements Disposable {
/**
@@ -63,96 +121,19 @@ declare module "bun:sqlite" {
* @param filename The filename of the database to open. Pass an empty string (`""`) or `":memory:"` or undefined for an in-memory database.
* @param options defaults to `{readwrite: true, create: true}`. If a number, then it's treated as `SQLITE_OPEN_*` constant flags.
*/
- constructor(
- filename?: string,
- options?:
- | number
- | {
- /**
- * Open the database as read-only (no write operations, no create).
- *
- * Equivalent to {@link constants.SQLITE_OPEN_READONLY}
- */
- readonly?: boolean;
- /**
- * Allow creating a new database
- *
- * Equivalent to {@link constants.SQLITE_OPEN_CREATE}
- */
- create?: boolean;
- /**
- * Open the database as read-write
- *
- * 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
- */
- safeIntegers?: 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;
- },
- );
+ constructor(filename?: string, options?: number | DatabaseOptions);
/**
+ * Open or create a SQLite3 databases
+ *
+ * @param filename The filename of the database to open. Pass an empty string (`""`) or `":memory:"` or undefined for an in-memory database.
+ * @param options defaults to `{readwrite: true, create: true}`. If a number, then it's treated as `SQLITE_OPEN_*` constant flags.
+ *
* This is an alias of `new Database()`
*
* See {@link Database}
*/
- static open(
- filename: string,
- options?:
- | number
- | {
- /**
- * Open the database as read-only (no write operations, no create).
- *
- * Equivalent to {@link constants.SQLITE_OPEN_READONLY}
- */
- readonly?: boolean;
- /**
- * Allow creating a new database
- *
- * Equivalent to {@link constants.SQLITE_OPEN_CREATE}
- */
- create?: boolean;
- /**
- * Open the database as read-write
- *
- * Equivalent to {@link constants.SQLITE_OPEN_READWRITE}
- */
- readwrite?: boolean;
- },
- ): Database;
+ static open(filename: string, options?: number | DatabaseOptions): Database;
/**
* Execute a SQL query **without returning any results**.
@@ -203,8 +184,11 @@ declare module "bun:sqlite" {
* @returns `Database` instance
*/
run(sql: string, ...bindings: ParamsType[]): Changes;
+
/**
* This is an alias of {@link Database.run}
+ *
+ * @deprecated Prefer {@link Database.run}
*/
exec(sql: string, ...bindings: ParamsType[]): Changes;
@@ -351,6 +335,16 @@ declare module "bun:sqlite" {
*/
static setCustomSQLite(path: string): boolean;
+ /**
+ * Closes the database when using the async resource proposal
+ *
+ * @example
+ * ```
+ * using db = new Database("myapp.db");
+ * doSomethingWithDatabase(db);
+ * // Automatically closed when `db` goes out of scope
+ * ```
+ */
[Symbol.dispose](): void;
/**
@@ -744,6 +738,30 @@ declare module "bun:sqlite" {
*/
values(...params: ParamsType): Array>;
+ /**
+ * Execute the prepared statement and return all results as arrays of
+ * `Uint8Array`s.
+ *
+ * This is similar to `values()` but returns all values as Uint8Array
+ * objects, regardless of their original SQLite type.
+ *
+ * @param params optional values to bind to the statement. If omitted, the
+ * statement is run with the last bound values or no parameters if there are
+ * none.
+ *
+ * @example
+ * ```ts
+ * const stmt = db.prepare("SELECT * FROM foo WHERE bar = ?");
+ *
+ * stmt.raw("baz");
+ * // => [[Uint8Array(24)]]
+ *
+ * stmt.raw();
+ * // => [[Uint8Array(24)]]
+ * ```
+ */
+ raw(...params: ParamsType): Array>;
+
/**
* The names of the columns returned by the prepared statement.
* @example
diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp
index 57c9f15d8a..de3a1f06bb 100644
--- a/src/bun.js/bindings/BunObject.cpp
+++ b/src/bun.js/bindings/BunObject.cpp
@@ -307,6 +307,9 @@ static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject)
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
+#if BUN_DEBUG
+ if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception());
+#endif
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, vm.propertyNames->defaultKeyword));
}
@@ -316,6 +319,9 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject)
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
+#if BUN_DEBUG
+ if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception());
+#endif
RETURN_IF_EXCEPTION(scope, {});
auto clientData = WebCore::clientData(vm);
RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName()));
diff --git a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp
index 66d37d5895..521a5946c0 100644
--- a/src/bun.js/bindings/sqlite/JSSQLStatement.cpp
+++ b/src/bun.js/bindings/sqlite/JSSQLStatement.cpp
@@ -280,6 +280,7 @@ JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionGet);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionAll);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionIterate);
JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows);
+JSC_DECLARE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRawRows);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnNames);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnCount);
@@ -294,6 +295,7 @@ JSC_DECLARE_HOST_FUNCTION(jsSQLStatementToStringFunction);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnNames);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnCount);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetParamCount);
+JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetHasMultipleStatements);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnTypes);
JSC_DECLARE_CUSTOM_GETTER(jsSqlStatementGetColumnDeclaredTypes);
@@ -488,9 +490,64 @@ protected:
void finishCreation(JSC::VM& vm);
};
+static JSValue toJSAsBuffer(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt* stmt, int i)
+{
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ switch (sqlite3_column_type(stmt, i)) {
+ case SQLITE_INTEGER: {
+ int64_t value = sqlite3_column_int64(stmt, i);
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 8);
+ RETURN_IF_EXCEPTION(scope, {});
+ uint8_t* data = array->typedVector();
+ for (int j = 0; j < 8; j++) {
+ data[j] = (value >> (j * 8)) & 0xFF;
+ }
+ return array;
+ }
+ case SQLITE_FLOAT: {
+ double value = sqlite3_column_double(stmt, i);
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 8);
+ RETURN_IF_EXCEPTION(scope, {});
+ memcpy(array->typedVector(), &value, 8);
+ return array;
+ }
+ case SQLITE3_TEXT: {
+ size_t len = sqlite3_column_bytes(stmt, i);
+ const unsigned char* text = len > 0 ? sqlite3_column_text(stmt, i) : nullptr;
+ if (text == nullptr || len == 0) [[unlikely]] {
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
+ RETURN_IF_EXCEPTION(scope, {});
+ return array;
+ }
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
+ RETURN_IF_EXCEPTION(scope, {});
+ memcpy(array->typedVector(), text, len);
+ return array;
+ }
+ case SQLITE_BLOB: {
+ size_t len = sqlite3_column_bytes(stmt, i);
+ const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr;
+ if (len > 0 && blob != nullptr) [[likely]] {
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
+ RETURN_IF_EXCEPTION(scope, {});
+ memcpy(array->vector(), blob, len);
+ return array;
+ }
+ JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
+ RETURN_IF_EXCEPTION(scope, {});
+ return array;
+ }
+ case SQLITE_NULL:
+ default:
+ return jsNull();
+ }
+}
+
template
static JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt* stmt, int i)
{
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
switch (sqlite3_column_type(stmt, i)) {
case SQLITE_INTEGER: {
if constexpr (!useBigInt64) {
@@ -498,7 +555,9 @@ static JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt
return jsNumberFromSQLite(stmt, i);
} else {
// https://github.com/oven-sh/bun/issues/1536
- return jsBigIntFromSQLite(globalObject, stmt, i);
+ auto bint = jsBigIntFromSQLite(globalObject, stmt, i);
+ RETURN_IF_EXCEPTION(throwScope, {});
+ return bint;
}
}
case SQLITE_FLOAT: {
@@ -515,20 +574,27 @@ static JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, sqlite3_stmt
return jsEmptyString(vm);
}
- return len < 64 ? jsString(vm, WTF::String::fromUTF8({ text, len })) : JSC::JSValue::decode(Bun__encoding__toStringUTF8(text, len, globalObject));
+ if (len < 64) {
+ return jsString(vm, WTF::String::fromUTF8({ text, len }));
+ }
+
+ auto encoded = Bun__encoding__toStringUTF8(text, len, globalObject);
+ RETURN_IF_EXCEPTION(throwScope, {});
+ return JSC::JSValue::decode(encoded);
}
case SQLITE_BLOB: {
size_t len = sqlite3_column_bytes(stmt, i);
const void* blob = len > 0 ? sqlite3_column_blob(stmt, i) : nullptr;
if (len > 0 && blob != nullptr) [[likely]] {
- auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len);
- RETURN_IF_EXCEPTION(scope, {});
+ RETURN_IF_EXCEPTION(throwScope, {});
memcpy(array->vector(), blob, len);
return array;
}
- return JSC::JSUint8Array::create(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
+ auto array = JSC::JSUint8Array::create(globalObject, globalObject->m_typedArrayUint8.get(globalObject), 0);
+ RETURN_IF_EXCEPTION(throwScope, {});
+ return array;
}
default: {
break;
@@ -545,6 +611,7 @@ static const HashTableValue JSSQLStatementPrototypeTableValues[] = {
{ "iterate"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionIterate, 1 } },
{ "as"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementSetPrototypeFunction, 1 } },
{ "values"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionRows, 1 } },
+ { "raw"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementExecuteStatementFunctionRawRows, 1 } },
{ "finalize"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementFunctionFinalize, 0 } },
{ "toString"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsSQLStatementToStringFunction, 0 } },
{ "columns"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnNames, 0 } },
@@ -553,7 +620,6 @@ static const HashTableValue JSSQLStatementPrototypeTableValues[] = {
{ "columnTypes"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnTypes, 0 } },
{ "declaredTypes"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetColumnDeclaredTypes, 0 } },
{ "safeIntegers"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsSqlStatementGetSafeIntegers, jsSqlStatementSetSafeIntegers } },
-
};
class JSSQLStatementPrototype final : public JSC::JSNonFinalObject {
@@ -712,6 +778,7 @@ static void initializeColumnNames(JSC::JSGlobalObject* lexicalGlobalObject, JSSQ
break;
const auto key = Identifier::fromString(vm, WTF::String::fromUTF8({ name, len }));
+
JSC::JSValue primitive = JSC::jsUndefined();
auto decl = sqlite3_column_decltype(stmt, i);
if (decl != nullptr) {
@@ -1857,6 +1924,22 @@ static inline JSC::JSValue constructResultObject(JSC::JSGlobalObject* lexicalGlo
RELEASE_AND_RETURN(scope, result);
}
+static inline JSC::JSArray* constructResultRowRaw(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, size_t columnCount)
+{
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ auto* stmt = castedThis->stmt;
+ MarkedArgumentBuffer arguments;
+ arguments.ensureCapacity(columnCount);
+
+ for (size_t i = 0; i < columnCount; i++) {
+ JSValue value = toJSAsBuffer(vm, lexicalGlobalObject, stmt, i);
+ RETURN_IF_EXCEPTION(throwScope, nullptr);
+ arguments.append(value);
+ }
+
+ RELEASE_AND_RETURN(throwScope, JSC::constructArray(lexicalGlobalObject, static_cast(nullptr), arguments));
+}
+
static inline JSC::JSArray* constructResultRow(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSSQLStatement* castedThis, size_t columnCount)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
@@ -2181,7 +2264,9 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows, (JSC::JSGlo
if (!castedThis->hasExecuted || castedThis->need_update()) {
initializeColumnNames(lexicalGlobalObject, castedThis);
+
if (scope.exception()) [[unlikely]] {
+ // Don't forget to reset before releasing the exception.
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
@@ -2212,10 +2297,97 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRows, (JSC::JSGlo
RELEASE_AND_RETURN(scope, {});
}
resultArray->push(lexicalGlobalObject, row);
+ RETURN_IF_EXCEPTION(scope, {});
+ status = sqlite3_step(stmt);
+ } while (status == SQLITE_ROW);
+ }
+
+ result = resultArray;
+ }
+ } else if (status == SQLITE_DONE && columnCount != 0) {
+ // breaking change in Bun v0.6.8
+ result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0);
+ RETURN_IF_EXCEPTION(scope, {});
+ }
+
+ if (status != SQLITE_DONE && status != SQLITE_OK) [[unlikely]] {
+ throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
+ sqlite3_reset(stmt);
+ return {};
+ }
+
+ // sqlite3_reset(stmt);
+ RELEASE_AND_RETURN(scope, JSC::JSValue::encode(result));
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRawRows, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
+{
+ auto& vm = JSC::getVM(lexicalGlobalObject);
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ auto castedThis = jsDynamicCast(callFrame->thisValue());
+
+ CHECK_THIS;
+
+ auto* stmt = castedThis->stmt;
+ CHECK_PREPARED
+
+ int statusCode = sqlite3_reset(stmt);
+ if (statusCode != SQLITE_OK) [[unlikely]] {
+ throwException(lexicalGlobalObject, scope, createSQLiteError(lexicalGlobalObject, castedThis->version_db->db));
+ sqlite3_reset(stmt);
+ return {};
+ }
+
+ int count = callFrame->argumentCount();
+ if (count > 0) {
+ auto arg0 = callFrame->argument(0);
+ DO_REBIND(arg0);
+ }
+
+ int status = sqlite3_step(stmt);
+ if (!sqlite3_stmt_readonly(stmt)) {
+ castedThis->version_db->version++;
+ }
+
+ if (!castedThis->hasExecuted || castedThis->need_update()) {
+ initializeColumnNames(lexicalGlobalObject, castedThis);
+ if (scope.exception()) [[unlikely]] {
+ sqlite3_reset(stmt);
+ RELEASE_AND_RETURN(scope, {});
+ }
+ }
+
+ size_t columnCount = castedThis->columnNames->size();
+ JSValue result = jsNull();
+ if (status == SQLITE_ROW) {
+ // this is a count from UPDATE or another query like that
+ if (columnCount == 0) {
+ while (status == SQLITE_ROW) {
+ status = sqlite3_step(stmt);
+ }
+
+ result = jsNumber(sqlite3_column_count(stmt));
+
+ } else {
+
+ JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0);
+ RETURN_IF_EXCEPTION(scope, {});
+ {
+ size_t columnCount = sqlite3_column_count(stmt);
+
+ do {
+ JSC::JSArray* row = constructResultRowRaw(vm, lexicalGlobalObject, castedThis, columnCount);
+ if (!row || scope.exception()) [[unlikely]] {
+ sqlite3_reset(stmt);
+ RELEASE_AND_RETURN(scope, {});
+ }
+ resultArray->push(lexicalGlobalObject, row);
+
if (scope.exception()) [[unlikely]] {
sqlite3_reset(stmt);
RELEASE_AND_RETURN(scope, {});
}
+
status = sqlite3_step(stmt);
} while (status == SQLITE_ROW);
}
@@ -2299,7 +2471,9 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementExecuteStatementFunctionRun, (JSC::JSGlob
int64_t last_insert_rowid = sqlite3_last_insert_rowid(db);
diff->putInternalField(vm, 0, JSC::jsNumber(total_changes_after - total_changes_before));
if (castedThis->useBigInt64) {
- diff->putInternalField(vm, 1, JSBigInt::createFrom(lexicalGlobalObject, last_insert_rowid));
+ JSValue lastRowIdBigInt = JSBigInt::createFrom(lexicalGlobalObject, last_insert_rowid);
+ RETURN_IF_EXCEPTION(scope, {});
+ diff->putInternalField(vm, 1, lastRowIdBigInt);
} else {
diff->putInternalField(vm, 1, JSC::jsNumber(last_insert_rowid));
}
diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts
index 2998f6a78c..bec07d0e16 100644
--- a/src/codegen/bundle-modules.ts
+++ b/src/codegen/bundle-modules.ts
@@ -75,11 +75,14 @@ async function retry(n, fn) {
throw err;
}
+const bunRepoRoot = path.join(CMAKE_BUILD_ROOT, "..", "..");
+
// Preprocess builtins
const bundledEntryPoints: string[] = [];
for (let i = 0; i < nativeStartIndex; i++) {
try {
- let input = fs.readFileSync(path.join(BASE, moduleList[i]), "utf8");
+ const file = path.join(BASE, moduleList[i]);
+ let input = fs.readFileSync(file, "utf8");
if (!/\bexport\s+(?:function|class|const|default|{)/.test(input)) {
if (input.includes("module.exports")) {
@@ -87,7 +90,9 @@ for (let i = 0; i < nativeStartIndex; i++) {
"Do not use CommonJS module.exports in ESM modules. Use `export default { ... }` instead. See src/js/README.md",
);
} else {
- throw new Error("Internal modules must have at least one ESM export statement. See src/js/README.md");
+ throw new Error(
+ `Internal modules must have at least one ESM export statement in '${path.relative(bunRepoRoot, file)}' — see src/js/README.md`,
+ );
}
}
diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts
index 24f0407ad5..ba27f233cc 100644
--- a/src/js/builtins.d.ts
+++ b/src/js/builtins.d.ts
@@ -172,6 +172,7 @@ declare function $idWithProfile(): TODO;
* @see [JIT implementation](https://github.com/oven-sh/WebKit/blob/433f7598bf3537a295d0af5ffd83b9a307abec4e/Source/JavaScriptCore/jit/JITOpcodes.cpp#L311)
*/
declare function $isObject(obj: unknown): obj is object;
+declare function $isArray(obj: T): obj is Extract | Extract;
declare function $isArray(obj: unknown): obj is any[];
declare function $isCallable(fn: unknown): fn is CallableFunction;
declare function $isConstructor(fn: unknown): fn is { new (...args: any[]): any };
@@ -842,7 +843,7 @@ interface ObjectConstructor {
declare const $Object: ObjectConstructor;
/** gets a property on an object */
-declare function $getByIdDirect(obj: any, key: string): T;
+declare function $getByIdDirect(obj: T, key: K): T[K];
/**
* Gets a private property on an object.
diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts
index 5cb7316330..f505268cc8 100644
--- a/src/js/builtins/ReadableStreamInternals.ts
+++ b/src/js/builtins/ReadableStreamInternals.ts
@@ -2050,7 +2050,6 @@ export function createLazyLoadedStreamPrototype(): typeof ReadableStreamDefaultC
throw $ERR_INVALID_STATE("Internal error: invalid result from pull. This is a bug in Bun. Please report it.");
}
- // eslint-disable-next-line no-unused-private-class-members
#pull(controller) {
var handle = $getByIdDirectPrivate(this, "stream");
@@ -2103,7 +2102,6 @@ export function createLazyLoadedStreamPrototype(): typeof ReadableStreamDefaultC
}
}
- // eslint-disable-next-line no-unused-private-class-members
#cancel(reason) {
var handle = $getByIdDirectPrivate(this, "stream");
this.$data = undefined;
diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts
index f4f92050cb..ffc317bad1 100644
--- a/src/js/bun/sql.ts
+++ b/src/js/bun/sql.ts
@@ -1,1574 +1,73 @@
-import type * as BunTypes from "bun";
+import type { PostgresAdapter } from "internal/sql/postgres";
+import type { BaseQueryHandle, Query } from "internal/sql/query";
+import type { SQLHelper } from "internal/sql/shared";
-const enum QueryStatus {
- active = 1 << 1,
- cancelled = 1 << 2,
- error = 1 << 3,
- executed = 1 << 4,
- invalidHandle = 1 << 5,
-}
-const cmds = ["", "INSERT", "DELETE", "UPDATE", "MERGE", "SELECT", "MOVE", "FETCH", "COPY"];
+const { Query, SQLQueryFlags } = require("internal/sql/query");
+const { PostgresAdapter } = require("internal/sql/postgres");
+const { SQLiteAdapter } = require("internal/sql/sqlite");
+const { SQLHelper, parseOptions } = require("internal/sql/shared");
+const { connectionClosedError } = require("internal/sql/utils");
+const { SQLError, PostgresError, SQLiteError } = require("internal/sql/errors");
-const PublicArray = globalThis.Array;
-const enum SSLMode {
- disable = 0,
- prefer = 1,
- require = 2,
- verify_ca = 3,
- verify_full = 4,
-}
-
-const { hideFromStack } = require("internal/shared");
const defineProperties = Object.defineProperties;
-function connectionClosedError() {
- return $ERR_POSTGRES_CONNECTION_CLOSED("Connection closed");
-}
-function notTaggedCallError() {
- return $ERR_POSTGRES_NOT_TAGGED_CALL("Query not called as a tagged template literal");
-}
-hideFromStack(connectionClosedError);
-hideFromStack(notTaggedCallError);
-
-enum SQLQueryResultMode {
- objects = 0,
- values = 1,
- raw = 2,
-}
-const escapeIdentifier = function escape(str) {
- return '"' + str.replaceAll('"', '""').replaceAll(".", '"."') + '"';
-};
-class SQLResultArray extends PublicArray {
- static [Symbol.toStringTag] = "SQLResults";
-
- constructor() {
- super();
- // match postgres's result array, in this way for in will not list the properties and .map will not return undefined command and count
- Object.defineProperties(this, {
- count: { value: null, writable: true },
- command: { value: null, writable: true },
- });
- }
- static get [Symbol.species]() {
- return Array;
- }
-}
-
-const _resolve = Symbol("resolve");
-const _reject = Symbol("reject");
-const _handle = Symbol("handle");
-const _run = Symbol("run");
-const _queryStatus = Symbol("status");
-const _handler = Symbol("handler");
-const _strings = Symbol("strings");
-const _values = Symbol("values");
-const _poolSize = Symbol("poolSize");
-const _flags = Symbol("flags");
-const _results = Symbol("results");
-const PublicPromise = Promise;
-type TransactionCallback = (sql: (strings: string, ...values: any[]) => Query) => Promise;
-
-const { createConnection: _createConnection, createQuery, init } = $zig("postgres.zig", "createBinding");
-
-function normalizeSSLMode(value: string): SSLMode {
- if (!value) {
- return SSLMode.disable;
- }
-
- value = (value + "").toLowerCase();
- switch (value) {
- case "disable":
- return SSLMode.disable;
- case "prefer":
- return SSLMode.prefer;
- case "require":
- case "required":
- return SSLMode.require;
- case "verify-ca":
- case "verify_ca":
- return SSLMode.verify_ca;
- case "verify-full":
- case "verify_full":
- return SSLMode.verify_full;
- default: {
- break;
- }
- }
-
- throw $ERR_INVALID_ARG_VALUE("sslmode", value);
-}
-
-enum SQLQueryFlags {
- none = 0,
- allowUnsafeTransaction = 1 << 0,
- unsafe = 1 << 1,
- bigint = 1 << 2,
- simple = 1 << 3,
- notTagged = 1 << 4,
-}
-
-function getQueryHandle(query) {
- let handle = query[_handle];
- if (!handle) {
- try {
- query[_handle] = handle = doCreateQuery(
- query[_strings],
- query[_values],
- query[_flags] & SQLQueryFlags.allowUnsafeTransaction,
- query[_poolSize],
- query[_flags] & SQLQueryFlags.bigint,
- query[_flags] & SQLQueryFlags.simple,
- );
- } catch (err) {
- query[_queryStatus] |= QueryStatus.error | QueryStatus.invalidHandle;
- query.reject(err);
- }
- }
- return handle;
-}
-
-enum SQLCommand {
- insert = 0,
- update = 1,
- updateSet = 2,
- where = 3,
- whereIn = 4,
- none = -1,
-}
-
-function commandToString(command: SQLCommand): string {
- switch (command) {
- case SQLCommand.insert:
- return "INSERT";
- case SQLCommand.updateSet:
- case SQLCommand.update:
- return "UPDATE";
- case SQLCommand.whereIn:
- case SQLCommand.where:
- return "WHERE";
- default:
- return "";
- }
-}
-
-function detectCommand(query: string): SQLCommand {
- const text = query.toLowerCase().trim();
- const text_len = text.length;
-
- let token = "";
- let command = SQLCommand.none;
- let quoted = false;
- for (let i = 0; i < text_len; i++) {
- const char = text[i];
- switch (char) {
- case " ": // Space
- case "\n": // Line feed
- case "\t": // Tab character
- case "\r": // Carriage return
- case "\f": // Form feed
- case "\v": {
- switch (token) {
- case "insert": {
- if (command === SQLCommand.none) {
- return SQLCommand.insert;
- }
- return command;
- }
- case "update": {
- if (command === SQLCommand.none) {
- command = SQLCommand.update;
- token = "";
- continue; // try to find SET
- }
- return command;
- }
- case "where": {
- command = SQLCommand.where;
- token = "";
- continue; // try to find IN
- }
- case "set": {
- if (command === SQLCommand.update) {
- command = SQLCommand.updateSet;
- token = "";
- continue; // try to find WHERE
- }
- return command;
- }
- case "in": {
- if (command === SQLCommand.where) {
- return SQLCommand.whereIn;
- }
- return command;
- }
- default: {
- token = "";
- continue;
- }
- }
- }
- default: {
- // skip quoted commands
- if (char === '"') {
- quoted = !quoted;
- continue;
- }
- if (!quoted) {
- token += char;
- }
- }
- }
- }
- if (token) {
- switch (command) {
- case SQLCommand.none: {
- switch (token) {
- case "insert":
- return SQLCommand.insert;
- case "update":
- return SQLCommand.update;
- case "where":
- return SQLCommand.where;
- default:
- return SQLCommand.none;
- }
- }
- case SQLCommand.update: {
- if (token === "set") {
- return SQLCommand.updateSet;
- }
- return SQLCommand.update;
- }
- case SQLCommand.where: {
- if (token === "in") {
- return SQLCommand.whereIn;
- }
- return SQLCommand.where;
- }
- }
- }
-
- return command;
-}
-
-function normalizeQuery(strings, values, binding_idx = 1) {
- if (typeof strings === "string") {
- // identifier or unsafe query
- return [strings, values || []];
- }
- if (!$isArray(strings)) {
- // we should not hit this path
- throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
- }
- const str_len = strings.length;
- if (str_len === 0) {
- return ["", []];
- }
- let binding_values: any[] = [];
- let query = "";
- for (let i = 0; i < str_len; i++) {
- const string = strings[i];
-
- if (typeof string === "string") {
- query += string;
- if (values.length > i) {
- const value = values[i];
- if (value instanceof Query) {
- const [sub_query, sub_values] = normalizeQuery(value[_strings], value[_values], binding_idx);
- query += sub_query;
- for (let j = 0; j < sub_values.length; j++) {
- binding_values.push(sub_values[j]);
- }
- binding_idx += sub_values.length;
- } else if (value instanceof SQLHelper) {
- const command = detectCommand(query);
- // only selectIn, insert, update, updateSet are allowed
- if (command === SQLCommand.none || command === SQLCommand.where) {
- throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands");
- }
- const { columns, value: items } = value as SQLHelper;
- const columnCount = columns.length;
- if (columnCount === 0 && command !== SQLCommand.whereIn) {
- throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`);
- }
- const lastColumnIndex = columns.length - 1;
-
- if (command === SQLCommand.insert) {
- //
- // insert into users ${sql(users)} or insert into users ${sql(user)}
- //
-
- query += "(";
- for (let j = 0; j < columnCount; j++) {
- query += escapeIdentifier(columns[j]);
- if (j < lastColumnIndex) {
- query += ", ";
- }
- }
- query += ") VALUES";
- if ($isArray(items)) {
- const itemsCount = items.length;
- const lastItemIndex = itemsCount - 1;
- for (let j = 0; j < itemsCount; j++) {
- query += "(";
- const item = items[j];
- for (let k = 0; k < columnCount; k++) {
- const column = columns[k];
- const columnValue = item[column];
- query += `$${binding_idx++}${k < lastColumnIndex ? ", " : ""}`;
- if (typeof columnValue === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(columnValue);
- }
- }
- if (j < lastItemIndex) {
- query += "),";
- } else {
- query += ") "; // the user can add RETURNING * or RETURNING id
- }
- }
- } else {
- query += "(";
- const item = items;
- for (let j = 0; j < columnCount; j++) {
- const column = columns[j];
- const columnValue = item[column];
- query += `$${binding_idx++}${j < lastColumnIndex ? ", " : ""}`;
- if (typeof columnValue === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(columnValue);
- }
- }
- query += ") "; // the user can add RETURNING * or RETURNING id
- }
- } else if (command === SQLCommand.whereIn) {
- // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])})
- if (!$isArray(items)) {
- throw new SyntaxError("An array of values is required for WHERE IN helper");
- }
- const itemsCount = items.length;
- const lastItemIndex = itemsCount - 1;
- query += "(";
- for (let j = 0; j < itemsCount; j++) {
- query += `$${binding_idx++}${j < lastItemIndex ? ", " : ""}`;
- if (columnCount > 0) {
- // we must use a key from a object
- if (columnCount > 1) {
- // we should not pass multiple columns here
- throw new SyntaxError("Cannot use WHERE IN helper with multiple columns");
- }
- // SELECT * FROM users WHERE id IN (${sql(users, "id")})
- const value = items[j];
- if (typeof value === "undefined") {
- binding_values.push(null);
- } else {
- const value_from_key = value[columns[0]];
-
- if (typeof value_from_key === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(value_from_key);
- }
- }
- } else {
- const value = items[j];
- if (typeof value === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(value);
- }
- }
- }
- query += ") "; // more conditions can be added after this
- } else {
- // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1
- let item;
- if ($isArray(items)) {
- if (items.length > 1) {
- throw new SyntaxError("Cannot use array of objects for UPDATE");
- }
- item = items[0];
- } else {
- item = items;
- }
- // no need to include if is updateSet
- if (command === SQLCommand.update) {
- query += " SET ";
- }
- for (let i = 0; i < columnCount; i++) {
- const column = columns[i];
- const columnValue = item[column];
- query += `${escapeIdentifier(column)} = $${binding_idx++}${i < lastColumnIndex ? ", " : ""}`;
- if (typeof columnValue === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(columnValue);
- }
- }
- query += " "; // the user can add where clause after this
- }
- } else {
- //TODO: handle sql.array parameters
- query += `$${binding_idx++} `;
- if (typeof value === "undefined") {
- binding_values.push(null);
- } else {
- binding_values.push(value);
- }
- }
- }
- } else {
- throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
- }
- }
-
- return [query, binding_values];
-}
-
-class Query extends PublicPromise {
- [_resolve];
- [_reject];
- [_handle];
- [_handler];
- [_queryStatus] = 0;
- [_strings];
- [_values];
-
- [Symbol.for("nodejs.util.inspect.custom")]() {
- const status = this[_queryStatus];
- const active = (status & QueryStatus.active) != 0;
- const cancelled = (status & QueryStatus.cancelled) != 0;
- const executed = (status & QueryStatus.executed) != 0;
- const error = (status & QueryStatus.error) != 0;
- return `PostgresQuery { ${active ? "active" : ""} ${cancelled ? "cancelled" : ""} ${executed ? "executed" : ""} ${error ? "error" : ""} }`;
- }
-
- constructor(strings, values, flags, poolSize, handler) {
- var resolve_, reject_;
- super((resolve, reject) => {
- resolve_ = resolve;
- reject_ = reject;
- });
- if (typeof strings === "string") {
- if (!(flags & SQLQueryFlags.unsafe)) {
- // identifier (cannot be executed in safe mode)
- flags |= SQLQueryFlags.notTagged;
- strings = escapeIdentifier(strings);
- }
- }
- this[_resolve] = resolve_;
- this[_reject] = reject_;
- this[_handle] = null;
- this[_handler] = handler;
- this[_queryStatus] = 0;
- this[_poolSize] = poolSize;
- this[_strings] = strings;
- this[_values] = values;
- this[_flags] = flags;
-
- this[_results] = null;
- }
-
- async [_run](async: boolean) {
- const { [_handler]: handler, [_queryStatus]: status } = this;
-
- if (status & (QueryStatus.executed | QueryStatus.error | QueryStatus.cancelled | QueryStatus.invalidHandle)) {
- return;
- }
- if (this[_flags] & SQLQueryFlags.notTagged) {
- this.reject(notTaggedCallError());
- return;
- }
- this[_queryStatus] |= QueryStatus.executed;
-
- const handle = getQueryHandle(this);
- if (!handle) return this;
-
- if (async) {
- // Ensure it's actually async
- // eslint-disable-next-line
- await 1;
- }
-
- try {
- return handler(this, handle);
- } catch (err) {
- this[_queryStatus] |= QueryStatus.error;
- this.reject(err);
- }
- }
- get active() {
- return (this[_queryStatus] & QueryStatus.active) != 0;
- }
-
- set active(value) {
- const status = this[_queryStatus];
- if (status & (QueryStatus.cancelled | QueryStatus.error)) {
- return;
- }
-
- if (value) {
- this[_queryStatus] |= QueryStatus.active;
- } else {
- this[_queryStatus] &= ~QueryStatus.active;
- }
- }
-
- get cancelled() {
- return (this[_queryStatus] & QueryStatus.cancelled) !== 0;
- }
-
- resolve(x) {
- this[_queryStatus] &= ~QueryStatus.active;
- const handle = getQueryHandle(this);
- if (!handle) return this;
- handle.done();
- return this[_resolve](x);
- }
-
- reject(x) {
- this[_queryStatus] &= ~QueryStatus.active;
- this[_queryStatus] |= QueryStatus.error;
- if (!(this[_queryStatus] & QueryStatus.invalidHandle)) {
- const handle = getQueryHandle(this);
- if (!handle) return this[_reject](x);
- handle.done();
- }
-
- return this[_reject](x);
- }
-
- cancel() {
- var status = this[_queryStatus];
- if (status & QueryStatus.cancelled) {
- return this;
- }
- this[_queryStatus] |= QueryStatus.cancelled;
-
- if (status & QueryStatus.executed) {
- const handle = getQueryHandle(this);
- handle.cancel();
- }
-
- return this;
- }
-
- execute() {
- this[_run](false);
- return this;
- }
-
- raw() {
- const handle = getQueryHandle(this);
- if (!handle) return this;
- handle.setMode(SQLQueryResultMode.raw);
- return this;
- }
-
- simple() {
- this[_flags] |= SQLQueryFlags.simple;
- return this;
- }
-
- values() {
- const handle = getQueryHandle(this);
- if (!handle) return this;
- handle.setMode(SQLQueryResultMode.values);
- return this;
- }
-
- then() {
- if (this[_flags] & SQLQueryFlags.notTagged) {
- throw notTaggedCallError();
- }
- this[_run](true);
- const result = super.$then.$apply(this, arguments);
- $markPromiseAsHandled(result);
- return result;
- }
-
- catch() {
- if (this[_flags] & SQLQueryFlags.notTagged) {
- throw notTaggedCallError();
- }
- this[_run](true);
- const result = super.catch.$apply(this, arguments);
- $markPromiseAsHandled(result);
- return result;
- }
-
- finally() {
- if (this[_flags] & SQLQueryFlags.notTagged) {
- throw notTaggedCallError();
- }
- this[_run](true);
- return super.finally.$apply(this, arguments);
- }
-}
-Object.defineProperty(Query, Symbol.species, { value: PublicPromise });
-Object.defineProperty(Query, Symbol.toStringTag, { value: "Query" });
-init(
- function onResolvePostgresQuery(query, result, commandTag, count, queries, is_last) {
- /// simple queries
- if (query[_flags] & SQLQueryFlags.simple) {
- // simple can have multiple results or a single result
- if (is_last) {
- if (queries) {
- const queriesIndex = queries.indexOf(query);
- if (queriesIndex !== -1) {
- queries.splice(queriesIndex, 1);
- }
- }
- try {
- query.resolve(query[_results]);
- } catch {}
- return;
- }
- $assert(result instanceof SQLResultArray, "Invalid result array");
- // prepare for next query
- query[_handle].setPendingValue(new SQLResultArray());
-
- if (typeof commandTag === "string") {
- if (commandTag.length > 0) {
- result.command = commandTag;
- }
- } else {
- result.command = cmds[commandTag];
- }
-
- result.count = count || 0;
- const last_result = query[_results];
-
- if (!last_result) {
- query[_results] = result;
- } else {
- if (last_result instanceof SQLResultArray) {
- // multiple results
- query[_results] = [last_result, result];
- } else {
- // 3 or more results
- last_result.push(result);
- }
- }
- return;
- }
- /// prepared statements
- $assert(result instanceof SQLResultArray, "Invalid result array");
- if (typeof commandTag === "string") {
- if (commandTag.length > 0) {
- result.command = commandTag;
- }
- } else {
- result.command = cmds[commandTag];
- }
-
- result.count = count || 0;
- if (queries) {
- const queriesIndex = queries.indexOf(query);
- if (queriesIndex !== -1) {
- queries.splice(queriesIndex, 1);
- }
- }
- try {
- query.resolve(result);
- } catch {}
- },
- function onRejectPostgresQuery(query, reject, queries) {
- if (queries) {
- const queriesIndex = queries.indexOf(query);
- if (queriesIndex !== -1) {
- queries.splice(queriesIndex, 1);
- }
- }
-
- try {
- query.reject(reject);
- } catch {}
- },
-);
-
-function onQueryFinish(onClose) {
- this.queries.delete(onClose);
- this.pool.release(this);
-}
-
-enum PooledConnectionState {
- pending = 0,
- connected = 1,
- closed = 2,
-}
-enum PooledConnectionFlags {
- /// canBeConnected is used to indicate that at least one time we were able to connect to the database
- canBeConnected = 1 << 0,
- /// reserved is used to indicate that the connection is currently reserved
- reserved = 1 << 1,
- /// preReserved is used to indicate that the connection will be reserved in the future when queryCount drops to 0
- preReserved = 1 << 2,
-}
-
-class PooledConnection {
- pool: ConnectionPool;
- connection: $ZigGeneratedClasses.PostgresSQLConnection | null = null;
- state: PooledConnectionState = PooledConnectionState.pending;
- storedError: Error | null = null;
- queries: Set<(err: Error) => void> = new Set();
- onFinish: ((err: Error | null) => void) | null = null;
- connectionInfo: any;
- flags: number = 0;
- /// queryCount is used to indicate the number of queries using the connection, if a connection is reserved or if its a transaction queryCount will be 1 independently of the number of queries
- queryCount: number = 0;
- #onConnected(err, _) {
- const connectionInfo = this.connectionInfo;
- if (connectionInfo?.onconnect) {
- connectionInfo.onconnect(err);
- }
- this.storedError = err;
- if (!err) {
- this.flags |= PooledConnectionFlags.canBeConnected;
- }
- this.state = err ? PooledConnectionState.closed : PooledConnectionState.connected;
- const onFinish = this.onFinish;
- if (onFinish) {
- this.queryCount = 0;
- this.flags &= ~PooledConnectionFlags.reserved;
- this.flags &= ~PooledConnectionFlags.preReserved;
-
- // pool is closed, lets finish the connection
- // pool is closed, lets finish the connection
- if (err) {
- onFinish(err);
- } else {
- this.connection?.close();
- }
- return;
- }
- this.pool.release(this, true);
- }
- #onClose(err) {
- const connectionInfo = this.connectionInfo;
- if (connectionInfo?.onclose) {
- connectionInfo.onclose(err);
- }
- this.state = PooledConnectionState.closed;
- this.connection = null;
- this.storedError = err;
-
- // remove from ready connections if its there
- this.pool.readyConnections.delete(this);
- const queries = new Set(this.queries);
- this.queries.clear();
- this.queryCount = 0;
- this.flags &= ~PooledConnectionFlags.reserved;
-
- // notify all queries that the connection is closed
- for (const onClose of queries) {
- onClose(err);
- }
- const onFinish = this.onFinish;
- if (onFinish) {
- onFinish(err);
- }
-
- this.pool.release(this, true);
- }
- constructor(connectionInfo, pool: ConnectionPool) {
- this.state = PooledConnectionState.pending;
- this.pool = pool;
- this.connectionInfo = connectionInfo;
- this.#startConnection();
- }
- async #startConnection() {
- this.connection = (await createConnection(
- this.connectionInfo,
- this.#onConnected.bind(this),
- this.#onClose.bind(this),
- )) as $ZigGeneratedClasses.PostgresSQLConnection;
- }
- onClose(onClose: (err: Error) => void) {
- this.queries.add(onClose);
- }
- bindQuery(query: Query, onClose: (err: Error) => void) {
- this.queries.add(onClose);
- // @ts-ignore
- query.finally(onQueryFinish.bind(this, onClose));
- }
-
- #doRetry() {
- if (this.pool.closed) {
- return;
- }
- // reset error and state
- this.storedError = null;
- this.state = PooledConnectionState.pending;
- // retry connection
- this.#startConnection();
- }
- close() {
- try {
- if (this.state === PooledConnectionState.connected) {
- this.connection?.close();
- }
- } catch {}
- }
- flush() {
- this.connection?.flush();
- }
- retry() {
- // if pool is closed, we can't retry
- if (this.pool.closed) {
- return false;
- }
- // we need to reconnect
- // lets use a retry strategy
-
- // we can only retry if one day we are able to connect
- if (this.flags & PooledConnectionFlags.canBeConnected) {
- this.#doRetry();
- } else {
- // analyse type of error to see if we can retry
- switch (this.storedError?.code) {
- case "ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD":
- case "ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD":
- case "ERR_POSTGRES_TLS_NOT_AVAILABLE":
- case "ERR_POSTGRES_TLS_UPGRADE_FAILED":
- case "ERR_POSTGRES_INVALID_SERVER_SIGNATURE":
- case "ERR_POSTGRES_INVALID_SERVER_KEY":
- case "ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2":
- // we can't retry these are authentication errors
- return false;
- default:
- // we can retry
- this.#doRetry();
- }
- }
- return true;
- }
-}
-class ConnectionPool {
- connectionInfo: any;
-
- connections: PooledConnection[];
- readyConnections: Set;
- waitingQueue: Array<(err: Error | null, result: any) => void> = [];
- reservedQueue: Array<(err: Error | null, result: any) => void> = [];
-
- poolStarted: boolean = false;
- closed: boolean = false;
- totalQueries: number = 0;
- onAllQueriesFinished: (() => void) | null = null;
- constructor(connectionInfo) {
- this.connectionInfo = connectionInfo;
- this.connections = new Array(connectionInfo.max);
- this.readyConnections = new Set();
- }
-
- maxDistribution() {
- if (!this.waitingQueue.length) return 0;
- const result = Math.ceil((this.waitingQueue.length + this.totalQueries) / this.connections.length);
- return result ? result : 1;
- }
-
- flushConcurrentQueries() {
- const maxDistribution = this.maxDistribution();
- if (maxDistribution === 0) {
- return;
- }
-
- while (true) {
- const nonReservedConnections = Array.from(this.readyConnections).filter(
- c => !(c.flags & PooledConnectionFlags.preReserved) && c.queryCount < maxDistribution,
- );
- if (nonReservedConnections.length === 0) {
- return;
- }
- const orderedConnections = nonReservedConnections.sort((a, b) => a.queryCount - b.queryCount);
- for (const connection of orderedConnections) {
- const pending = this.waitingQueue.shift();
- if (!pending) {
- return;
- }
- connection.queryCount++;
- this.totalQueries++;
- pending(null, connection);
- }
- }
- }
-
- release(connection: PooledConnection, connectingEvent: boolean = false) {
- if (!connectingEvent) {
- connection.queryCount--;
- this.totalQueries--;
- }
- const currentQueryCount = connection.queryCount;
- if (currentQueryCount == 0) {
- connection.flags &= ~PooledConnectionFlags.reserved;
- connection.flags &= ~PooledConnectionFlags.preReserved;
- }
- if (this.onAllQueriesFinished) {
- // we are waiting for all queries to finish, lets check if we can call it
- if (!this.hasPendingQueries()) {
- this.onAllQueriesFinished();
- }
- }
-
- if (connection.state !== PooledConnectionState.connected) {
- // connection is not ready
- if (connection.storedError) {
- // this connection got a error but maybe we can wait for another
-
- if (this.hasConnectionsAvailable()) {
- return;
- }
-
- const waitingQueue = this.waitingQueue;
- const reservedQueue = this.reservedQueue;
-
- this.waitingQueue = [];
- this.reservedQueue = [];
- // we have no connections available so lets fails
- for (const pending of waitingQueue) {
- pending(connection.storedError, connection);
- }
- for (const pending of reservedQueue) {
- pending(connection.storedError, connection);
- }
- }
- return;
- }
-
- if (currentQueryCount == 0) {
- // ok we can actually bind reserved queries to it
- const pendingReserved = this.reservedQueue.shift();
- if (pendingReserved) {
- connection.flags |= PooledConnectionFlags.reserved;
- connection.queryCount++;
- this.totalQueries++;
- // we have a connection waiting for a reserved connection lets prioritize it
- pendingReserved(connection.storedError, connection);
- return;
- }
- }
- this.readyConnections.add(connection);
- this.flushConcurrentQueries();
- }
-
- hasConnectionsAvailable() {
- if (this.readyConnections.size > 0) return true;
- if (this.poolStarted) {
- const pollSize = this.connections.length;
- for (let i = 0; i < pollSize; i++) {
- const connection = this.connections[i];
- if (connection.state !== PooledConnectionState.closed) {
- // some connection is connecting or connected
- return true;
- }
- }
- }
- return false;
- }
-
- hasPendingQueries() {
- if (this.waitingQueue.length > 0 || this.reservedQueue.length > 0) return true;
- if (this.poolStarted) {
- return this.totalQueries > 0;
- }
- return false;
- }
- isConnected() {
- if (this.readyConnections.size > 0) {
- return true;
- }
- if (this.poolStarted) {
- const pollSize = this.connections.length;
- for (let i = 0; i < pollSize; i++) {
- const connection = this.connections[i];
- if (connection.state === PooledConnectionState.connected) {
- return true;
- }
- }
- }
- return false;
- }
- flush() {
- if (this.closed) {
- return;
- }
- if (this.poolStarted) {
- const pollSize = this.connections.length;
- for (let i = 0; i < pollSize; i++) {
- const connection = this.connections[i];
- if (connection.state === PooledConnectionState.connected) {
- connection.connection?.flush();
- }
- }
- }
- }
-
- async #close() {
- let pending;
- while ((pending = this.waitingQueue.shift())) {
- pending(connectionClosedError(), null);
- }
- while (this.reservedQueue.length > 0) {
- const pendingReserved = this.reservedQueue.shift();
- if (pendingReserved) {
- pendingReserved(connectionClosedError(), null);
- }
- }
- const promises: Array> = [];
- if (this.poolStarted) {
- this.poolStarted = false;
- const pollSize = this.connections.length;
- for (let i = 0; i < pollSize; i++) {
- const connection = this.connections[i];
- switch (connection.state) {
- case PooledConnectionState.pending:
- {
- const { promise, resolve } = Promise.withResolvers();
- connection.onFinish = resolve;
- promises.push(promise);
- connection.connection?.close();
- }
- break;
- case PooledConnectionState.connected:
- {
- const { promise, resolve } = Promise.withResolvers();
- connection.onFinish = resolve;
- promises.push(promise);
- connection.connection?.close();
- }
- break;
- }
- // clean connection reference
- // @ts-ignore
- this.connections[i] = null;
- }
- }
- this.readyConnections.clear();
- this.waitingQueue.length = 0;
- return Promise.all(promises);
- }
- async close(options?: { timeout?: number }) {
- if (this.closed) {
- return;
- }
- let timeout = options?.timeout;
- if (timeout) {
- timeout = Number(timeout);
- if (timeout > 2 ** 31 || timeout < 0 || timeout !== timeout) {
- throw $ERR_INVALID_ARG_VALUE("options.timeout", timeout, "must be a non-negative integer less than 2^31");
- }
- this.closed = true;
- if (timeout === 0 || !this.hasPendingQueries()) {
- // close immediately
- await this.#close();
- return;
- }
-
- const { promise, resolve } = Promise.withResolvers();
- const timer = setTimeout(() => {
- // timeout is reached, lets close and probably fail some queries
- this.#close().finally(resolve);
- }, timeout * 1000);
- timer.unref(); // dont block the event loop
- this.onAllQueriesFinished = () => {
- clearTimeout(timer);
- // everything is closed, lets close the pool
- this.#close().finally(resolve);
- };
-
- return promise;
- } else {
- this.closed = true;
- if (!this.hasPendingQueries()) {
- // close immediately
- await this.#close();
- return;
- }
- // gracefully close the pool
- const { promise, resolve } = Promise.withResolvers();
- this.onAllQueriesFinished = () => {
- // everything is closed, lets close the pool
- this.#close().finally(resolve);
- };
- return promise;
- }
- }
-
- /**
- * @param {function} onConnected - The callback function to be called when the connection is established.
- * @param {boolean} reserved - Whether the connection is reserved, if is reserved the connection will not be released until release is called, if not release will only decrement the queryCount counter
- */
- connect(onConnected: (err: Error | null, result: any) => void, reserved: boolean = false) {
- if (this.closed) {
- return onConnected(connectionClosedError(), null);
- }
-
- if (this.readyConnections.size === 0) {
- // no connection ready lets make some
- let retry_in_progress = false;
- let all_closed = true;
- let storedError: Error | null = null;
-
- if (this.poolStarted) {
- // we already started the pool
- // lets check if some connection is available to retry
- const pollSize = this.connections.length;
- for (let i = 0; i < pollSize; i++) {
- const connection = this.connections[i];
- // we need a new connection and we have some connections that can retry
- if (connection.state === PooledConnectionState.closed) {
- if (connection.retry()) {
- // lets wait for connection to be released
- if (!retry_in_progress) {
- // avoid adding to the queue twice, we wanna to retry every available pool connection
- retry_in_progress = true;
- if (reserved) {
- // we are not sure what connection will be available so we dont pre reserve
- this.reservedQueue.push(onConnected);
- } else {
- this.waitingQueue.push(onConnected);
- }
- }
- } else {
- // we have some error, lets grab it and fail if unable to start a connection
- storedError = connection.storedError;
- }
- } else {
- // we have some pending or open connections
- all_closed = false;
- }
- }
- if (!all_closed && !retry_in_progress) {
- // is possible to connect because we have some working connections, or we are just without network for some reason
- // wait for connection to be released or fail
- if (reserved) {
- // we are not sure what connection will be available so we dont pre reserve
- this.reservedQueue.push(onConnected);
- } else {
- this.waitingQueue.push(onConnected);
- }
- } else if (!retry_in_progress) {
- // impossible to connect or retry
- onConnected(storedError ?? connectionClosedError(), null);
- }
- return;
- }
- // we never started the pool, lets start it
- if (reserved) {
- this.reservedQueue.push(onConnected);
- } else {
- this.waitingQueue.push(onConnected);
- }
- this.poolStarted = true;
- const pollSize = this.connections.length;
- // pool is always at least 1 connection
- const firstConnection = new PooledConnection(this.connectionInfo, this);
- this.connections[0] = firstConnection;
- if (reserved) {
- firstConnection.flags |= PooledConnectionFlags.preReserved; // lets pre reserve the first connection
- }
- for (let i = 1; i < pollSize; i++) {
- this.connections[i] = new PooledConnection(this.connectionInfo, this);
- }
- return;
- }
- if (reserved) {
- let connectionWithLeastQueries: PooledConnection | null = null;
- let leastQueries = Infinity;
- for (const connection of this.readyConnections) {
- if (connection.flags & PooledConnectionFlags.preReserved || connection.flags & PooledConnectionFlags.reserved)
- continue;
- const queryCount = connection.queryCount;
- if (queryCount > 0) {
- if (queryCount < leastQueries) {
- leastQueries = queryCount;
- connectionWithLeastQueries = connection;
- }
- continue;
- }
- connection.flags |= PooledConnectionFlags.reserved;
- connection.queryCount++;
- this.totalQueries++;
- this.readyConnections.delete(connection);
- onConnected(null, connection);
- return;
- }
- if (connectionWithLeastQueries) {
- // lets mark the connection with the least queries as preReserved if any
- connectionWithLeastQueries.flags |= PooledConnectionFlags.preReserved;
- }
- // no connection available to be reserved lets wait for a connection to be released
- this.reservedQueue.push(onConnected);
- } else {
- this.waitingQueue.push(onConnected);
- this.flushConcurrentQueries();
- }
- }
-}
-
-async function createConnection(options, onConnected, onClose) {
- const {
- hostname,
- port,
- username,
- tls,
- query,
- database,
- sslMode,
- idleTimeout = 0,
- connectionTimeout = 30 * 1000,
- maxLifetime = 0,
- prepare = true,
- path,
- } = options;
-
- let password = options.password;
- try {
- if (typeof password === "function") {
- password = password();
- if (password && $isPromise(password)) {
- password = await password;
- }
- }
- return _createConnection(
- hostname,
- Number(port),
- username || "",
- password || "",
- database || "",
- // > The default value for sslmode is prefer. As is shown in the table, this
- // makes no sense from a security point of view, and it only promises
- // performance overhead if possible. It is only provided as the default for
- // backward compatibility, and is not recommended in secure deployments.
- sslMode || SSLMode.disable,
- tls || null,
- query || "",
- path || "",
- onConnected,
- onClose,
- idleTimeout,
- connectionTimeout,
- maxLifetime,
- !prepare,
- ) as $ZigGeneratedClasses.PostgresSQLConnection;
- } catch (e) {
- onClose(e);
- }
-}
-
-function doCreateQuery(strings, values, allowUnsafeTransaction, poolSize, bigint, simple) {
- const [sqlString, final_values] = normalizeQuery(strings, values);
- if (!allowUnsafeTransaction) {
- if (poolSize !== 1) {
- const upperCaseSqlString = sqlString.toUpperCase().trim();
- if (upperCaseSqlString.startsWith("BEGIN") || upperCaseSqlString.startsWith("START TRANSACTION")) {
- throw $ERR_POSTGRES_UNSAFE_TRANSACTION("Only use sql.begin, sql.reserved or max: 1");
- }
- }
- }
- return createQuery(sqlString, final_values, new SQLResultArray(), undefined, !!bigint, !!simple);
-}
-
-class SQLHelper {
- value: any;
- columns: string[];
- constructor(value, keys) {
- if (keys?.length === 0) {
- keys = Object.keys(value[0]);
- }
-
- for (let key of keys) {
- if (typeof key === "string") {
- const asNumber = Number(key);
- if (Number.isNaN(asNumber)) {
- continue;
- }
- key = asNumber;
- }
-
- if (typeof key !== "string") {
- if (Number.isSafeInteger(key)) {
- if (key >= 0 && key <= 64 * 1024) {
- continue;
- }
- }
-
- throw new Error(`Keys must be strings or numbers: ${key}`);
- }
- }
-
- this.value = value;
- this.columns = keys;
- }
-}
-
-function decodeIfValid(value) {
- if (value) {
- return decodeURIComponent(value);
- }
- return null;
-}
-function loadOptions(o: Bun.SQL.Options) {
- var hostname,
- port,
- username,
- password,
- database,
- tls,
- url,
- query,
- adapter,
- idleTimeout,
- connectionTimeout,
- maxLifetime,
- onconnect,
- onclose,
- max,
- bigint,
- path;
- let prepare = true;
- const env = Bun.env || {};
- var sslMode: SSLMode = SSLMode.disable;
-
- if (o === undefined || (typeof o === "string" && o.length === 0)) {
- let urlString = env.POSTGRES_URL || env.DATABASE_URL || env.PGURL || env.PG_URL;
- if (!urlString) {
- urlString = env.TLS_POSTGRES_DATABASE_URL || env.TLS_DATABASE_URL;
- if (urlString) {
- sslMode = SSLMode.require;
- }
- }
-
- if (urlString) {
- url = new URL(urlString);
- o = {};
- }
- } else if (o && typeof o === "object") {
- if (o instanceof URL) {
- url = o;
- } else if (o?.url) {
- const _url = o.url;
- if (typeof _url === "string") {
- url = new URL(_url);
- } else if (_url && typeof _url === "object" && _url instanceof URL) {
- url = _url;
- }
- }
- if (o?.tls) {
- sslMode = SSLMode.require;
- tls = o.tls;
- }
- } else if (typeof o === "string") {
- url = new URL(o);
- }
- o ||= {};
- query = "";
-
- if (url) {
- ({ hostname, port, username, password, adapter } = o);
- // object overrides url
- hostname ||= url.hostname;
- port ||= url.port;
- username ||= decodeIfValid(url.username);
- password ||= decodeIfValid(url.password);
- adapter ||= url.protocol;
-
- if (adapter[adapter.length - 1] === ":") {
- adapter = adapter.slice(0, -1);
- }
-
- const queryObject = url.searchParams.toJSON();
- for (const key in queryObject) {
- if (key.toLowerCase() === "sslmode") {
- sslMode = normalizeSSLMode(queryObject[key]);
- } else if (key.toLowerCase() === "path") {
- path = queryObject[key];
- } else {
- // this is valid for postgres for other databases it might not be valid
- // check adapter then implement for other databases
- // encode string with \0 as finalizer
- // must be key\0value\0
- query += `${key}\0${queryObject[key]}\0`;
- }
- }
- query = query.trim();
- }
- hostname ||= o.hostname || o.host || env.PGHOST || "localhost";
-
- port ||= Number(o.port || env.PGPORT || 5432);
-
- path ||= o.path || "";
- // add /.s.PGSQL.${port} if it doesn't exist
- if (path && path?.indexOf("/.s.PGSQL.") === -1) {
- path = `${path}/.s.PGSQL.${port}`;
- }
-
- username ||= o.username || o.user || env.PGUSERNAME || env.PGUSER || env.USER || env.USERNAME || "postgres";
- database ||= o.database || o.db || decodeIfValid((url?.pathname ?? "").slice(1)) || env.PGDATABASE || username;
- password ||= o.password || o.pass || env.PGPASSWORD || "";
- const connection = o.connection;
- if (connection && $isObject(connection)) {
- for (const key in connection) {
- if (connection[key] !== undefined) {
- query += `${key}\0${connection[key]}\0`;
- }
- }
- }
- tls ||= o.tls || o.ssl;
- adapter ||= o.adapter || "postgres";
- max = o.max;
-
- idleTimeout ??= o.idleTimeout;
- idleTimeout ??= o.idle_timeout;
- connectionTimeout ??= o.connectionTimeout;
- connectionTimeout ??= o.connection_timeout;
- connectionTimeout ??= o.connectTimeout;
- connectionTimeout ??= o.connect_timeout;
- maxLifetime ??= o.maxLifetime;
- maxLifetime ??= o.max_lifetime;
- bigint ??= o.bigint;
- // we need to explicitly set prepare to false if it is false
- if (o.prepare === false) {
- prepare = false;
- }
-
- onconnect ??= o.onconnect;
- onclose ??= o.onclose;
- if (onconnect !== undefined) {
- if (!$isCallable(onconnect)) {
- throw $ERR_INVALID_ARG_TYPE("onconnect", "function", onconnect);
- }
- }
-
- if (onclose !== undefined) {
- if (!$isCallable(onclose)) {
- throw $ERR_INVALID_ARG_TYPE("onclose", "function", onclose);
- }
- }
-
- if (idleTimeout != null) {
- idleTimeout = Number(idleTimeout);
- if (idleTimeout > 2 ** 31 || idleTimeout < 0 || idleTimeout !== idleTimeout) {
- throw $ERR_INVALID_ARG_VALUE(
- "options.idle_timeout",
- idleTimeout,
- "must be a non-negative integer less than 2^31",
- );
- }
- idleTimeout *= 1000;
- }
-
- if (connectionTimeout != null) {
- connectionTimeout = Number(connectionTimeout);
- if (connectionTimeout > 2 ** 31 || connectionTimeout < 0 || connectionTimeout !== connectionTimeout) {
- throw $ERR_INVALID_ARG_VALUE(
- "options.connection_timeout",
- connectionTimeout,
- "must be a non-negative integer less than 2^31",
- );
- }
- connectionTimeout *= 1000;
- }
-
- if (maxLifetime != null) {
- maxLifetime = Number(maxLifetime);
- if (maxLifetime > 2 ** 31 || maxLifetime < 0 || maxLifetime !== maxLifetime) {
- throw $ERR_INVALID_ARG_VALUE(
- "options.max_lifetime",
- maxLifetime,
- "must be a non-negative integer less than 2^31",
- );
- }
- maxLifetime *= 1000;
- }
-
- if (max != null) {
- max = Number(max);
- if (max > 2 ** 31 || max < 1 || max !== max) {
- throw $ERR_INVALID_ARG_VALUE("options.max", max, "must be a non-negative integer between 1 and 2^31");
- }
- }
-
- if (sslMode !== SSLMode.disable && !tls?.serverName) {
- if (hostname) {
- tls = { ...tls, serverName: hostname };
- } else if (tls) {
- tls = true;
- }
- }
-
- if (tls && sslMode === SSLMode.disable) {
- sslMode = SSLMode.prefer;
- }
- port = Number(port);
-
- if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
- throw $ERR_INVALID_ARG_VALUE("port", port, "must be a non-negative integer between 1 and 65535");
- }
-
- switch (adapter) {
- case "postgres":
- case "postgresql":
- adapter = "postgres";
- break;
- default:
- throw new Error(`Unsupported adapter: ${adapter}. Only \"postgres\" is supported for now`);
- }
- const ret: any = { hostname, port, username, password, database, tls, query, sslMode, adapter, prepare, bigint };
- if (idleTimeout != null) {
- ret.idleTimeout = idleTimeout;
- }
- if (connectionTimeout != null) {
- ret.connectionTimeout = connectionTimeout;
- }
- if (maxLifetime != null) {
- ret.maxLifetime = maxLifetime;
- }
- if (onconnect !== undefined) {
- ret.onconnect = onconnect;
- }
- if (onclose !== undefined) {
- ret.onclose = onclose;
- }
- ret.max = max || 10;
-
- return ret;
-}
+type TransactionCallback = (sql: (strings: string, ...values: any[]) => Query) => Promise;
enum ReservedConnectionState {
acceptQueries = 1 << 0,
closed = 1 << 1,
}
-function assertValidTransactionName(name: string) {
- if (name.indexOf("'") !== -1) {
- throw Error(`Distributed transaction name cannot contain single quotes.`);
+interface TransactionState {
+ connectionState: ReservedConnectionState;
+ reject: (err: Error) => void;
+ storedError?: Error | null | undefined;
+ queries: Set>;
+}
+
+function adapterFromOptions(options: Bun.SQL.__internal.DefinedOptions) {
+ switch (options.adapter) {
+ case "postgres":
+ return new PostgresAdapter(options);
+ case "sqlite":
+ return new SQLiteAdapter(options);
+ default:
+ throw new Error(`Unsupported adapter: ${(options as { adapter?: string }).adapter}.`);
}
}
-function SQL(o, e = {}) {
- if (typeof o === "string" || o instanceof URL) {
- o = { ...e, url: o };
- }
- var connectionInfo = loadOptions(o);
- var pool = new ConnectionPool(connectionInfo);
+const SQL: typeof Bun.SQL = function SQL(
+ stringOrUrlOrOptions: Bun.SQL.Options | string | undefined = undefined,
+ definitelyOptionsButMaybeEmpty: Bun.SQL.Options = {},
+): Bun.SQL {
+ const connectionInfo = parseOptions(stringOrUrlOrOptions, definitelyOptionsButMaybeEmpty);
- function onQueryDisconnected(err) {
+ const pool = adapterFromOptions(connectionInfo);
+
+ function onQueryDisconnected(this: Query, err: Error) {
// connection closed mid query this will not be called if the query finishes first
const query = this;
+
if (err) {
return query.reject(err);
}
+
// query is cancelled when waiting for a connection from the pool
if (query.cancelled) {
- return query.reject($ERR_POSTGRES_QUERY_CANCELLED("Query cancelled"));
+ return query.reject(
+ new PostgresError("Query cancelled", {
+ code: "ERR_POSTGRES_QUERY_CANCELLED",
+ }),
+ );
}
}
- function onQueryConnected(handle, err, pooledConnection) {
+ function onQueryConnected(
+ this: Query,
+ handle: BaseQueryHandle,
+ err,
+ connectionHandle: ConnectionHandle,
+ ) {
const query = this;
if (err) {
// fail to aquire a connection from the pool
@@ -1576,72 +75,121 @@ function SQL(o, e = {}) {
}
// query is cancelled when waiting for a connection from the pool
if (query.cancelled) {
- pool.release(pooledConnection); // release the connection back to the pool
- return query.reject($ERR_POSTGRES_QUERY_CANCELLED("Query cancelled"));
+ pool.release(connectionHandle); // release the connection back to the pool
+ return query.reject(
+ new PostgresError("Query cancelled", {
+ code: "ERR_POSTGRES_QUERY_CANCELLED",
+ }),
+ );
}
- // bind close event to the query (will unbind and auto release the connection when the query is finished)
- pooledConnection.bindQuery(query, onQueryDisconnected.bind(query));
- handle.run(pooledConnection.connection, query);
+ if (connectionHandle.bindQuery) {
+ connectionHandle.bindQuery(query, onQueryDisconnected.bind(query));
+ }
+
+ try {
+ const connection = pool.getConnectionForQuery ? pool.getConnectionForQuery(connectionHandle) : connectionHandle;
+ const result = handle.run(connection, query);
+
+ if (result && $isPromise(result)) {
+ result.catch(err => query.reject(err));
+ }
+ } catch (err) {
+ query.reject(err);
+ }
}
function queryFromPoolHandler(query, handle, err) {
if (err) {
// fail to create query
return query.reject(err);
}
+
// query is cancelled
if (!handle || query.cancelled) {
- return query.reject($ERR_POSTGRES_QUERY_CANCELLED("Query cancelled"));
+ return query.reject(
+ new PostgresError("Query cancelled", {
+ code: "ERR_POSTGRES_QUERY_CANCELLED",
+ }),
+ );
}
pool.connect(onQueryConnected.bind(query, handle));
}
- function queryFromPool(strings, values) {
+
+ function queryFromPool(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ values: any[],
+ ) {
try {
return new Query(
strings,
values,
connectionInfo.bigint ? SQLQueryFlags.bigint : SQLQueryFlags.none,
- connectionInfo.max,
queryFromPoolHandler,
+ pool,
);
} catch (err) {
return Promise.reject(err);
}
}
- function unsafeQuery(strings, values) {
+ function unsafeQuery(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ values: any[],
+ ) {
try {
let flags = connectionInfo.bigint ? SQLQueryFlags.bigint | SQLQueryFlags.unsafe : SQLQueryFlags.unsafe;
if ((values?.length ?? 0) === 0) {
flags |= SQLQueryFlags.simple;
}
- return new Query(strings, values, flags, connectionInfo.max, queryFromPoolHandler);
+ return new Query(strings, values, flags, queryFromPoolHandler, pool);
} catch (err) {
return Promise.reject(err);
}
}
- function onTransactionQueryDisconnected(query) {
+ function onTransactionQueryDisconnected(query: Query) {
const transactionQueries = this;
transactionQueries.delete(query);
}
+
function queryFromTransactionHandler(transactionQueries, query, handle, err) {
const pooledConnection = this;
if (err) {
transactionQueries.delete(query);
return query.reject(err);
}
+
// query is cancelled
if (query.cancelled) {
transactionQueries.delete(query);
- return query.reject($ERR_POSTGRES_QUERY_CANCELLED("Query cancelled"));
+ return query.reject(
+ new PostgresError("Query cancelled", {
+ code: "ERR_POSTGRES_QUERY_CANCELLED",
+ }),
+ );
}
query.finally(onTransactionQueryDisconnected.bind(transactionQueries, query));
- handle.run(pooledConnection.connection, query);
+
+ try {
+ // Use adapter method to get the actual connection
+ const connection = pool.getConnectionForQuery ? pool.getConnectionForQuery(pooledConnection) : pooledConnection;
+ const result = handle.run(connection, query);
+ if (result && $isPromise(result)) {
+ result.catch(err => query.reject(err));
+ }
+ } catch (err) {
+ query.reject(err);
+ }
}
- function queryFromTransaction(strings, values, pooledConnection, transactionQueries) {
+
+ function queryFromTransaction(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ values: any[],
+ pooledConnection: PooledPostgresConnection,
+ transactionQueries: Set>,
+ ) {
try {
const query = new Query(
strings,
@@ -1649,16 +197,23 @@ function SQL(o, e = {}) {
connectionInfo.bigint
? SQLQueryFlags.allowUnsafeTransaction | SQLQueryFlags.bigint
: SQLQueryFlags.allowUnsafeTransaction,
- connectionInfo.max,
queryFromTransactionHandler.bind(pooledConnection, transactionQueries),
+ pool,
);
+
transactionQueries.add(query);
return query;
} catch (err) {
return Promise.reject(err);
}
}
- function unsafeQueryFromTransaction(strings, values, pooledConnection, transactionQueries) {
+
+ function unsafeQueryFromTransaction(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ values: any[],
+ pooledConnection: PooledPostgresConnection,
+ transactionQueries: Set>,
+ ) {
try {
let flags = connectionInfo.bigint
? SQLQueryFlags.allowUnsafeTransaction | SQLQueryFlags.unsafe | SQLQueryFlags.bigint
@@ -1671,8 +226,8 @@ function SQL(o, e = {}) {
strings,
values,
flags,
- connectionInfo.max,
queryFromTransactionHandler.bind(pooledConnection, transactionQueries),
+ pool,
);
transactionQueries.add(query);
return query;
@@ -1681,36 +236,41 @@ function SQL(o, e = {}) {
}
}
- function onTransactionDisconnected(err) {
+ function onTransactionDisconnected(this: TransactionState, err: Error) {
const reject = this.reject;
this.connectionState |= ReservedConnectionState.closed;
for (const query of this.queries) {
- (query as Query).reject(err);
+ query.reject(err);
}
+
if (err) {
return reject(err);
}
}
- function onReserveConnected(err, pooledConnection) {
+ function onReserveConnected(this: Query, err: Error | null, pooledConnection) {
const { resolve, reject } = this;
+
if (err) {
return reject(err);
}
let reservedTransaction = new Set();
- const state = {
+ const state: TransactionState = {
connectionState: ReservedConnectionState.acceptQueries,
reject,
storedError: null,
queries: new Set(),
};
- const onClose = onTransactionDisconnected.bind(state);
- pooledConnection.onClose(onClose);
- function reserved_sql(strings, ...values) {
+ const onClose = onTransactionDisconnected.bind(state);
+ if (pooledConnection.onClose) {
+ pooledConnection.onClose(onClose);
+ }
+
+ function reserved_sql(strings: string | TemplateStringsArray | SQLHelper | Query, ...values: any[]) {
if (
state.connectionState & ReservedConnectionState.closed ||
!(state.connectionState & ReservedConnectionState.acceptQueries)
@@ -1719,7 +279,7 @@ function SQL(o, e = {}) {
}
if ($isArray(strings)) {
// detect if is tagged template
- if (!$isArray((strings as unknown as TemplateStringsArray).raw)) {
+ if (!$isArray(strings.raw)) {
return new SQLHelper(strings, values);
}
} else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) {
@@ -1728,9 +288,11 @@ function SQL(o, e = {}) {
// we use the same code path as the transaction sql
return queryFromTransaction(strings, values, pooledConnection, state.queries);
}
+
reserved_sql.unsafe = (string, args = []) => {
return unsafeQueryFromTransaction(string, args, pooledConnection, state.queries);
};
+
reserved_sql.file = async (path: string, args = []) => {
return await Bun.file(path)
.text()
@@ -1738,6 +300,7 @@ function SQL(o, e = {}) {
return unsafeQueryFromTransaction(text, args, pooledConnection, state.queries);
});
};
+
reserved_sql.connect = () => {
if (state.connectionState & ReservedConnectionState.closed) {
return Promise.reject(connectionClosedError());
@@ -1746,36 +309,20 @@ function SQL(o, e = {}) {
};
reserved_sql.commitDistributed = async function (name: string) {
- const adapter = connectionInfo.adapter;
- assertValidTransactionName(name);
- switch (adapter) {
- case "postgres":
- return await reserved_sql.unsafe(`COMMIT PREPARED '${name}'`);
- case "mysql":
- return await reserved_sql.unsafe(`XA COMMIT '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically committed.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+ if (!pool.getCommitDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sql = pool.getCommitDistributedSQL(name);
+ return await reserved_sql.unsafe(sql);
};
reserved_sql.rollbackDistributed = async function (name: string) {
- assertValidTransactionName(name);
- const adapter = connectionInfo.adapter;
- switch (adapter) {
- case "postgres":
- return await reserved_sql.unsafe(`ROLLBACK PREPARED '${name}'`);
- case "mysql":
- return await reserved_sql.unsafe(`XA ROLLBACK '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically rolled back.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+ if (!pool.getRollbackDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sql = pool.getRollbackDistributedSQL(name);
+ return await reserved_sql.unsafe(sql);
};
// reserve is allowed to be called inside reserved connection but will return a new reserved connection from the pool
@@ -1836,7 +383,11 @@ function SQL(o, e = {}) {
if (state.connectionState & ReservedConnectionState.closed) {
throw connectionClosedError();
}
- return pooledConnection.flush();
+ // Use pooled connection's flush if available, otherwise use adapter's flush
+ if (pooledConnection.flush) {
+ return pooledConnection.flush();
+ }
+ return pool.flush();
};
reserved_sql.close = async (options?: { timeout?: number }) => {
const reserveQueries = state.queries;
@@ -1861,7 +412,7 @@ function SQL(o, e = {}) {
const timer = setTimeout(() => {
state.connectionState |= ReservedConnectionState.closed;
for (const query of reserveQueries) {
- (query as Query).cancel();
+ (query as Query).cancel();
}
state.connectionState |= ReservedConnectionState.closed;
pooledConnection.close();
@@ -1878,7 +429,7 @@ function SQL(o, e = {}) {
}
state.connectionState |= ReservedConnectionState.closed;
for (const query of reserveQueries) {
- (query as Query).cancel();
+ (query as Query).cancel();
}
pooledConnection.close();
@@ -1895,7 +446,10 @@ function SQL(o, e = {}) {
// just release the connection back to the pool
state.connectionState |= ReservedConnectionState.closed;
state.connectionState &= ~ReservedConnectionState.acceptQueries;
- pooledConnection.queries.delete(onClose);
+ // Use adapter method to detach connection close handler
+ if (pool.detachConnectionCloseHandler) {
+ pool.detachConnectionCloseHandler(pooledConnection, onClose);
+ }
pool.release(pooledConnection);
return Promise.resolve(undefined);
};
@@ -1944,7 +498,8 @@ function SQL(o, e = {}) {
if (err) {
return reject(err);
}
- const state = {
+
+ const state: TransactionState = {
connectionState: ReservedConnectionState.acceptQueries,
reject,
queries: new Set(),
@@ -1952,86 +507,60 @@ function SQL(o, e = {}) {
let savepoints = 0;
let transactionSavepoints = new Set();
- const adapter = connectionInfo.adapter;
- let BEGIN_COMMAND: string = "BEGIN";
- let ROLLBACK_COMMAND: string = "ROLLBACK";
- let COMMIT_COMMAND: string = "COMMIT";
- let SAVEPOINT_COMMAND: string = "SAVEPOINT";
- let RELEASE_SAVEPOINT_COMMAND: string | null = "RELEASE SAVEPOINT";
- let ROLLBACK_TO_SAVEPOINT_COMMAND: string = "ROLLBACK TO SAVEPOINT";
- // MySQL and maybe other adapters need to call XA END or some other command before commit or rollback in a distributed transaction
+
+ let BEGIN_COMMAND: string;
+ let ROLLBACK_COMMAND: string;
+ let COMMIT_COMMAND: string;
+ let SAVEPOINT_COMMAND: string;
+ let RELEASE_SAVEPOINT_COMMAND: string | null;
+ let ROLLBACK_TO_SAVEPOINT_COMMAND: string;
let BEFORE_COMMIT_OR_ROLLBACK_COMMAND: string | null = null;
+
if (distributed) {
- if (options.indexOf("'") !== -1) {
+ // Get distributed transaction commands from adapter
+ const commands = pool.getDistributedTransactionCommands?.(options);
+ if (!commands) {
pool.release(pooledConnection);
- return reject(new Error(`Distributed transaction name cannot contain single quotes.`));
+ return reject(new Error(`This adapter doesn't support distributed transactions.`));
}
- // distributed transaction
- // in distributed transaction options is the name/id of the transaction
- switch (adapter) {
- case "postgres":
- // in postgres we only need to call prepare transaction instead of commit
- COMMIT_COMMAND = `PREPARE TRANSACTION '${options}'`;
- break;
- case "mysql":
- // MySQL we use XA transactions
- // START TRANSACTION is autocommit false
- BEGIN_COMMAND = `XA START '${options}'`;
- BEFORE_COMMIT_OR_ROLLBACK_COMMAND = `XA END '${options}'`;
- COMMIT_COMMAND = `XA PREPARE '${options}'`;
- ROLLBACK_COMMAND = `XA ROLLBACK '${options}'`;
- break;
- case "sqlite":
- pool.release(pooledConnection);
- // do not support options just use defaults
- return reject(new Error(`SQLite dont support distributed transactions.`));
- case "mssql":
- BEGIN_COMMAND = ` BEGIN DISTRIBUTED TRANSACTION ${options}`;
- ROLLBACK_COMMAND = `ROLLBACK TRANSACTION ${options}`;
- COMMIT_COMMAND = `COMMIT TRANSACTION ${options}`;
- break;
- default:
- pool.release(pooledConnection);
-
- // TODO: use ERR_
- return reject(new Error(`Unsupported adapter: ${adapter}.`));
- }
+ BEGIN_COMMAND = commands.BEGIN;
+ COMMIT_COMMAND = commands.COMMIT;
+ ROLLBACK_COMMAND = commands.ROLLBACK;
+ SAVEPOINT_COMMAND = commands.SAVEPOINT;
+ RELEASE_SAVEPOINT_COMMAND = commands.RELEASE_SAVEPOINT;
+ ROLLBACK_TO_SAVEPOINT_COMMAND = commands.ROLLBACK_TO_SAVEPOINT;
+ BEFORE_COMMIT_OR_ROLLBACK_COMMAND = commands.BEFORE_COMMIT_OR_ROLLBACK || null;
} else {
- // normal transaction
- switch (adapter) {
- case "postgres":
- if (options) {
- BEGIN_COMMAND = `BEGIN ${options}`;
- }
- break;
- case "mysql":
- // START TRANSACTION is autocommit false
- BEGIN_COMMAND = options ? `START TRANSACTION ${options}` : "START TRANSACTION";
- break;
-
- case "sqlite":
- if (options) {
- // sqlite supports DEFERRED, IMMEDIATE, EXCLUSIVE
- BEGIN_COMMAND = `BEGIN ${options}`;
- }
- break;
- case "mssql":
- BEGIN_COMMAND = options ? `START TRANSACTION ${options}` : "START TRANSACTION";
- ROLLBACK_COMMAND = "ROLLBACK TRANSACTION";
- COMMIT_COMMAND = "COMMIT TRANSACTION";
- SAVEPOINT_COMMAND = "SAVE";
- RELEASE_SAVEPOINT_COMMAND = null; // mssql dont have release savepoint
- ROLLBACK_TO_SAVEPOINT_COMMAND = "ROLLBACK TRANSACTION";
- break;
- default:
+ // Validate transaction options if provided
+ if (options && pool.validateTransactionOptions) {
+ const validation = pool.validateTransactionOptions(options);
+ if (!validation.valid) {
pool.release(pooledConnection);
- // TODO: use ERR_
- return reject(new Error(`Unsupported adapter: ${adapter}.`));
+ return reject(new Error(validation.error));
+ }
+ }
+
+ try {
+ const commands = pool.getTransactionCommands(options);
+ BEGIN_COMMAND = commands.BEGIN;
+ COMMIT_COMMAND = commands.COMMIT;
+ ROLLBACK_COMMAND = commands.ROLLBACK;
+ SAVEPOINT_COMMAND = commands.SAVEPOINT;
+ RELEASE_SAVEPOINT_COMMAND = commands.RELEASE_SAVEPOINT;
+ ROLLBACK_TO_SAVEPOINT_COMMAND = commands.ROLLBACK_TO_SAVEPOINT;
+ BEFORE_COMMIT_OR_ROLLBACK_COMMAND = commands.BEFORE_COMMIT_OR_ROLLBACK || null;
+ } catch (err) {
+ pool.release(pooledConnection);
+ return reject(err);
}
}
+
const onClose = onTransactionDisconnected.bind(state);
- pooledConnection.onClose(onClose);
+ // Use adapter method to attach connection close handler
+ if (pool.attachConnectionCloseHandler) {
+ pool.attachConnectionCloseHandler(pooledConnection, onClose);
+ }
function run_internal_transaction_sql(string) {
if (state.connectionState & ReservedConnectionState.closed) {
@@ -2039,7 +568,10 @@ function SQL(o, e = {}) {
}
return unsafeQueryFromTransaction(string, [], pooledConnection, state.queries);
}
- function transaction_sql(strings, ...values) {
+ function transaction_sql(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ ...values: any[]
+ ) {
if (
state.connectionState & ReservedConnectionState.closed ||
!(state.connectionState & ReservedConnectionState.acceptQueries)
@@ -2079,57 +611,53 @@ function SQL(o, e = {}) {
return Promise.resolve(transaction_sql);
};
transaction_sql.commitDistributed = async function (name: string) {
- assertValidTransactionName(name);
- switch (adapter) {
- case "postgres":
- return await run_internal_transaction_sql(`COMMIT PREPARED '${name}'`);
- case "mysql":
- return await run_internal_transaction_sql(`XA COMMIT '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically committed.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+ if (!pool.getCommitDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sql = pool.getCommitDistributedSQL(name);
+ return await run_internal_transaction_sql(sql);
};
transaction_sql.rollbackDistributed = async function (name: string) {
- assertValidTransactionName(name);
- switch (adapter) {
- case "postgres":
- return await run_internal_transaction_sql(`ROLLBACK PREPARED '${name}'`);
- case "mysql":
- return await run_internal_transaction_sql(`XA ROLLBACK '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically rolled back.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+ if (!pool.getRollbackDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sql = pool.getRollbackDistributedSQL(name);
+ return await run_internal_transaction_sql(sql);
};
// begin is not allowed on a transaction we need to use savepoint() instead
transaction_sql.begin = function () {
if (distributed) {
- throw $ERR_POSTGRES_INVALID_TRANSACTION_STATE("cannot call begin inside a distributed transaction");
+ throw new PostgresError("cannot call begin inside a distributed transaction", {
+ code: "ERR_POSTGRES_INVALID_TRANSACTION_STATE",
+ });
}
- throw $ERR_POSTGRES_INVALID_TRANSACTION_STATE("cannot call begin inside a transaction use savepoint() instead");
+ throw new PostgresError("cannot call begin inside a transaction use savepoint() instead", {
+ code: "POSTGRES_INVALID_TRANSACTION_STATE",
+ });
};
transaction_sql.beginDistributed = function () {
if (distributed) {
- throw $ERR_POSTGRES_INVALID_TRANSACTION_STATE("cannot call beginDistributed inside a distributed transaction");
+ throw new PostgresError("cannot call beginDistributed inside a distributed transaction", {
+ code: "ERR_POSTGRES_INVALID_TRANSACTION_STATE",
+ });
}
- throw $ERR_POSTGRES_INVALID_TRANSACTION_STATE(
- "cannot call beginDistributed inside a transaction use savepoint() instead",
- );
+ throw new PostgresError("cannot call beginDistributed inside a transaction use savepoint() instead", {
+ code: "POSTGRES_INVALID_TRANSACTION_STATE",
+ });
};
transaction_sql.flush = function () {
if (state.connectionState & ReservedConnectionState.closed) {
throw connectionClosedError();
}
- return pooledConnection.flush();
+ // Use pooled connection's flush if available, otherwise use adapter's flush
+ if (pooledConnection.flush) {
+ return pooledConnection.flush();
+ }
+ return pool.flush();
};
transaction_sql.close = async function (options?: { timeout?: number }) {
// we dont actually close the connection here, we just set the state to closed and rollback the transaction
@@ -2155,7 +683,7 @@ function SQL(o, e = {}) {
const pending_savepoints = Array.from(transactionSavepoints);
const timer = setTimeout(async () => {
for (const query of transactionQueries) {
- (query as Query).cancel();
+ (query as Query).cancel();
}
if (BEFORE_COMMIT_OR_ROLLBACK_COMMAND) {
await run_internal_transaction_sql(BEFORE_COMMIT_OR_ROLLBACK_COMMAND);
@@ -2173,7 +701,7 @@ function SQL(o, e = {}) {
}
}
for (const query of transactionQueries) {
- (query as Query).cancel();
+ (query as Query).cancel();
}
if (BEFORE_COMMIT_OR_ROLLBACK_COMMAND) {
await run_internal_transaction_sql(BEFORE_COMMIT_OR_ROLLBACK_COMMAND);
@@ -2212,7 +740,9 @@ function SQL(o, e = {}) {
}
if (distributed) {
transaction_sql.savepoint = async (_fn: TransactionCallback, _name?: string): Promise => {
- throw $ERR_POSTGRES_INVALID_TRANSACTION_STATE("cannot call savepoint inside a distributed transaction");
+ throw new PostgresError("cannot call savepoint inside a distributed transaction", {
+ code: "ERR_POSTGRES_INVALID_TRANSACTION_STATE",
+ });
};
} else {
transaction_sql.savepoint = async (fn: TransactionCallback, name?: string): Promise => {
@@ -2269,13 +799,19 @@ function SQL(o, e = {}) {
return reject(err);
} finally {
state.connectionState |= ReservedConnectionState.closed;
- pooledConnection.queries.delete(onClose);
+ // Use adapter method to detach connection close handler
+ if (pool.detachConnectionCloseHandler) {
+ pool.detachConnectionCloseHandler(pooledConnection, onClose);
+ }
if (!dontRelease) {
pool.release(pooledConnection);
}
}
}
- function sql(strings, ...values) {
+ function sql(
+ strings: string | TemplateStringsArray | import("internal/sql/shared.ts").SQLHelper | Query,
+ ...values: any[]
+ ) {
if ($isArray(strings)) {
// detect if is tagged template
if (!$isArray((strings as unknown as TemplateStringsArray).raw)) {
@@ -2298,11 +834,18 @@ function SQL(o, e = {}) {
return unsafeQuery(text, args);
});
};
+
sql.reserve = () => {
if (pool.closed) {
return Promise.reject(connectionClosedError());
}
+ // Check if adapter supports reserved connections
+ if (pool.supportsReservedConnections && !pool.supportsReservedConnections()) {
+ return Promise.reject(new Error("This adapter doesn't support connection reservation"));
+ }
+
+ // Try to reserve a connection - adapters that support it will handle appropriately
const promiseWithResolvers = Promise.withResolvers();
pool.connect(onReserveConnected.bind(promiseWithResolvers), true);
return promiseWithResolvers.promise;
@@ -2311,40 +854,26 @@ function SQL(o, e = {}) {
if (pool.closed) {
throw connectionClosedError();
}
- assertValidTransactionName(name);
- const adapter = connectionInfo.adapter;
- switch (adapter) {
- case "postgres":
- return await sql.unsafe(`ROLLBACK PREPARED '${name}'`);
- case "mysql":
- return await sql.unsafe(`XA ROLLBACK '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically rolled back.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+
+ if (!pool.getRollbackDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sqlQuery = pool.getRollbackDistributedSQL(name);
+ return await sql.unsafe(sqlQuery);
};
sql.commitDistributed = async function (name: string) {
if (pool.closed) {
throw connectionClosedError();
}
- assertValidTransactionName(name);
- const adapter = connectionInfo.adapter;
- switch (adapter) {
- case "postgres":
- return await sql.unsafe(`COMMIT PREPARED '${name}'`);
- case "mysql":
- return await sql.unsafe(`XA COMMIT '${name}'`);
- case "mssql":
- throw Error(`MSSQL distributed transaction is automatically committed.`);
- case "sqlite":
- throw Error(`SQLite dont support distributed transactions.`);
- default:
- throw Error(`Unsupported adapter: ${adapter}.`);
+
+ if (!pool.getCommitDistributedSQL) {
+ throw Error(`This adapter doesn't support distributed transactions.`);
}
+
+ const sqlQuery = pool.getCommitDistributedSQL(name);
+ return await sql.unsafe(sqlQuery);
};
sql.beginDistributed = (name: string, fn: TransactionCallback) => {
@@ -2361,8 +890,8 @@ function SQL(o, e = {}) {
return Promise.reject($ERR_INVALID_ARG_VALUE("fn", callback, "must be a function"));
}
const { promise, resolve, reject } = Promise.withResolvers();
- // lets just reuse the same code path as the transaction begin
- pool.connect(onTransactionConnected.bind(null, callback, name, resolve, reject, false, true), true);
+ const useReserved = pool.supportsReservedConnections?.() ?? true;
+ pool.connect(onTransactionConnected.bind(null, callback, name, resolve, reject, false, true), useReserved);
return promise;
};
@@ -2382,7 +911,8 @@ function SQL(o, e = {}) {
return Promise.reject($ERR_INVALID_ARG_VALUE("fn", callback, "must be a function"));
}
const { promise, resolve, reject } = Promise.withResolvers();
- pool.connect(onTransactionConnected.bind(null, callback, options, resolve, reject, false, false), true);
+ const useReserved = pool.supportsReservedConnections?.() ?? true;
+ pool.connect(onTransactionConnected.bind(null, callback, options, resolve, reject, false, false), useReserved);
return promise;
};
sql.connect = () => {
@@ -2422,9 +952,9 @@ function SQL(o, e = {}) {
sql.distributed = sql.beginDistributed;
sql.end = sql.close;
return sql;
-}
+};
-var lazyDefaultSQL: InstanceType;
+var lazyDefaultSQL: Bun.SQL;
function resetDefaultSQL(sql) {
lazyDefaultSQL = sql;
@@ -2439,15 +969,17 @@ function ensureDefaultSQL() {
}
}
-var defaultSQLObject: InstanceType = function sql(strings, ...values) {
+var defaultSQLObject: Bun.SQL = function sql(strings, ...values) {
if (new.target) {
return SQL(strings);
}
+
if (!lazyDefaultSQL) {
resetDefaultSQL(SQL(undefined));
}
+
return lazyDefaultSQL(strings, ...values);
-} as typeof BunTypes.SQL;
+} as Bun.SQL;
defaultSQLObject.reserve = (...args) => {
ensureDefaultSQL();
@@ -2484,7 +1016,7 @@ defaultSQLObject.file = (filename: string, ...args) => {
defaultSQLObject.transaction = defaultSQLObject.begin = function (...args: Parameters) {
ensureDefaultSQL();
return lazyDefaultSQL.begin(...args);
-} as (typeof BunTypes.SQL)["begin"];
+} as Bun.SQL["begin"];
defaultSQLObject.end = defaultSQLObject.close = (...args: Parameters) => {
ensureDefaultSQL();
@@ -2510,12 +1042,45 @@ defineProperties(defaultSQLObject, {
},
});
-var exportsObject = {
+SQL.SQLError = SQLError;
+SQL.PostgresError = PostgresError;
+SQL.SQLiteError = SQLiteError;
+
+// // Helper functions for native code to create error instances
+// // These are internal functions used by Zig/C++ code
+// export function $createPostgresError(
+// message: string,
+// code: string,
+// detail: string,
+// hint: string,
+// severity: string,
+// additionalFields?: Record,
+// ) {
+// const options = {
+// code,
+// detail,
+// hint,
+// severity,
+// ...additionalFields,
+// };
+// return new PostgresError(message, options);
+// }
+
+// export function $createSQLiteError(message: string, code: string, errno: number) {
+// return new SQLiteError(message, { code, errno });
+// }
+
+// export function $createSQLError(message: string) {
+// return new SQLError(message);
+// }
+
+export default {
sql: defaultSQLObject,
default: defaultSQLObject,
SQL,
Query,
postgres: SQL,
+ SQLError,
+ PostgresError,
+ SQLiteError,
};
-
-export default exportsObject;
diff --git a/src/js/bun/sqlite.ts b/src/js/bun/sqlite.ts
index 3fbc4a61a3..395c0cd70b 100644
--- a/src/js/bun/sqlite.ts
+++ b/src/js/bun/sqlite.ts
@@ -4,10 +4,10 @@ import type * as SqliteTypes from "bun:sqlite";
const kSafeIntegersFlag = 1 << 1;
const kStrictFlag = 1 << 2;
-var defineProperties = Object.defineProperties;
-var toStringTag = Symbol.toStringTag;
-var isArray = Array.isArray;
-var isTypedArray = ArrayBuffer.isView;
+const defineProperties = Object.defineProperties;
+const toStringTag = Symbol.toStringTag;
+const isArray = Array.isArray;
+const isTypedArray = ArrayBuffer.isView;
let internalFieldTuple;
@@ -94,12 +94,41 @@ const constants = {
SQLITE_FCNTL_RESET_CACHE: 42,
};
-var SQL;
+// This is interface is the JS equivalent of what JSSQLStatement.cpp defines
+interface CppSQLStatement {
+ run: (...args: TODO[]) => TODO;
+ get: (...args: TODO[]) => TODO;
+ all: (...args: TODO[]) => TODO;
+ iterate: (...args: TODO[]) => TODO;
+ as: (...args: TODO[]) => TODO;
+ values: (...args: TODO[]) => TODO;
+ raw: (...args: TODO[]) => TODO;
+ finalize: (...args: TODO[]) => TODO;
+ toString: (...args: TODO[]) => TODO;
+ columns: string[];
+ columnsCount: number;
+ paramsCount: number;
+ columnTypes: string[];
+ declaredTypes: (string | null)[];
+ safeIntegers: boolean;
+}
-var controllers;
+interface CppSQL {
+ open(filename: string, flags: number, db: Database): TODO;
+ isInTransaction(handle: TODO): boolean;
+ loadExtension(handle: TODO, name: string, entryPoint: string): void;
+ serialize(handle: TODO, name: string): Buffer;
+ deserialize(serialized: NodeJS.TypedArray | ArrayBufferLike, openFlags: number, deserializeFlags: number): TODO;
+ fcntl(handle: TODO, ...args: TODO[]): TODO;
+ close(handle: TODO, throwOnError: boolean): void;
+ setCustomSQLite(path: string): void;
+}
+
+let SQL: CppSQL;
+let controllers: WeakMap | undefined;
class Statement {
- constructor(raw) {
+ constructor(raw: CppSQLStatement) {
this.#raw = raw;
switch (raw.paramsCount) {
@@ -108,6 +137,7 @@ class Statement {
this.all = this.#allNoArgs;
this.iterate = this.#iterateNoArgs;
this.values = this.#valuesNoArgs;
+ this.raw = this.#rawNoArgs;
this.run = this.#runNoArgs;
break;
}
@@ -116,18 +146,20 @@ class Statement {
this.all = this.#all;
this.iterate = this.#iterate;
this.values = this.#values;
+ this.raw = this.#rawValues;
this.run = this.#run;
break;
}
}
}
- #raw;
+ #raw: CppSQLStatement;
get: SqliteTypes.Statement["get"];
all: SqliteTypes.Statement["all"];
iterate: SqliteTypes.Statement["iterate"];
values: SqliteTypes.Statement["values"];
+ raw: SqliteTypes.Statement["raw"];
run: SqliteTypes.Statement["run"];
isFinalized = false;
@@ -170,6 +202,10 @@ class Statement {
return this.#raw.values();
}
+ #rawNoArgs() {
+ return this.#raw.raw();
+ }
+
#runNoArgs() {
this.#raw.run(internalFieldTuple);
@@ -191,7 +227,6 @@ class Statement {
return this;
}
- // eslint-disable-next-line no-unused-private-class-members
#get(...args) {
if (args.length === 0) return this.#getNoArgs();
var arg0 = args[0];
@@ -204,7 +239,6 @@ class Statement {
: this.#raw.get(...args);
}
- // eslint-disable-next-line no-unused-private-class-members
#all(...args) {
if (args.length === 0) return this.#allNoArgs();
var arg0 = args[0];
@@ -217,7 +251,6 @@ class Statement {
: this.#raw.all(...args);
}
- // eslint-disable-next-line no-unused-private-class-members
*#iterate(...args) {
if (args.length === 0) return yield* this.#iterateNoArgs();
var arg0 = args[0];
@@ -234,7 +267,6 @@ class Statement {
}
}
- // eslint-disable-next-line no-unused-private-class-members
#values(...args) {
if (args.length === 0) return this.#valuesNoArgs();
var arg0 = args[0];
@@ -247,7 +279,18 @@ class Statement {
: this.#raw.values(...args);
}
- // eslint-disable-next-line no-unused-private-class-members
+ #rawValues(...args) {
+ if (args.length === 0) return this.#rawNoArgs();
+ var arg0 = args[0];
+ // ["foo"] => ["foo"]
+ // ("foo") => ["foo"]
+ // (Uint8Array(1024)) => [Uint8Array]
+ // (123) => [123]
+ return !isArray(arg0) && (!arg0 || typeof arg0 !== "object" || isTypedArray(arg0))
+ ? this.#raw.raw(args)
+ : this.#raw.raw(...args);
+ }
+
#run(...args) {
if (args.length === 0) {
this.#runNoArgs();
@@ -295,9 +338,13 @@ class Statement {
}
}
-var cachedCount = Symbol.for("Bun.Database.cache.count");
-class Database {
- constructor(filenameGiven, options) {
+const cachedCount = Symbol.for("Bun.Database.cache.count");
+
+class Database implements SqliteTypes.Database {
+ constructor(
+ filenameGiven: string | undefined | NodeJS.TypedArray | Buffer,
+ options?: SqliteTypes.DatabaseOptions | number,
+ ) {
if (typeof filenameGiven === "undefined") {
} else if (typeof filenameGiven !== "string") {
if (isTypedArray(filenameGiven)) {
@@ -398,11 +445,11 @@ class Database {
return SQL.loadExtension(this.#handle, name, entryPoint);
}
- serialize(optionalName) {
+ serialize(optionalName?: string) {
return SQL.serialize(this.#handle, optionalName || "main");
}
- static #deserialize(serialized, openFlags, deserializeFlags) {
+ static #deserialize(serialized: NodeJS.TypedArray | ArrayBufferLike, openFlags: number, deserializeFlags: number) {
if (!SQL) {
initializeSQL();
}
@@ -411,7 +458,7 @@ class Database {
}
static deserialize(
- serialized,
+ serialized: NodeJS.TypedArray | ArrayBufferLike,
options: boolean | { readonly?: boolean; strict?: boolean; safeIntegers?: boolean } = false,
) {
if (typeof options === "boolean") {
@@ -476,7 +523,7 @@ class Database {
return createChangesObject();
}
- prepare(query, params, flags) {
+ prepare(query: string, params: any[] | undefined, flags: number = 0) {
return new Statement(SQL.prepare(this.#handle, query, params, flags || 0, this.#internalFlags));
}
diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts
index 5827c78288..984885fb4b 100644
--- a/src/js/internal/shared.ts
+++ b/src/js/internal/shared.ts
@@ -26,7 +26,7 @@ function throwNotImplemented(feature: string, issue?: number, extra?: string): n
throw new NotImplementedError(feature, issue, extra);
}
-function hideFromStack(...fns) {
+function hideFromStack(...fns: Function[]) {
for (const fn of fns) {
Object.defineProperty(fn, "name", {
value: "::bunternal::",
@@ -34,7 +34,7 @@ function hideFromStack(...fns) {
}
}
-let warned;
+let warned: Set;
function warnNotImplementedOnce(feature: string, issue?: number) {
if (!warned) {
warned = new Set();
@@ -47,16 +47,14 @@ function warnNotImplementedOnce(feature: string, issue?: number) {
console.warn(new NotImplementedError(feature, issue));
}
-//
-
let util: typeof import("node:util");
class ExceptionWithHostPort extends Error {
errno: number;
syscall: string;
port?: number;
- address;
+ address: string;
- constructor(err, syscall, address, port) {
+ constructor(err: number, syscall: string, address: string, port?: number) {
// TODO(joyeecheung): We have to use the type-checked
// getSystemErrorName(err) to guard against invalid arguments from users.
// This can be replaced with [ code ] = errmap.get(err) when this method
@@ -94,6 +92,9 @@ class NodeAggregateError extends AggregateError {
}
class ErrnoException extends Error {
+ errno: number;
+ syscall: string;
+
constructor(err, syscall, original) {
util ??= require("node:util");
const code = util.getSystemErrorName(err);
diff --git a/src/js/internal/sql/errors.ts b/src/js/internal/sql/errors.ts
new file mode 100644
index 0000000000..a2f5d5a98a
--- /dev/null
+++ b/src/js/internal/sql/errors.ts
@@ -0,0 +1,95 @@
+class SQLError extends Error implements Bun.SQL.SQLError {
+ constructor(message: string) {
+ super(message);
+ this.name = "SQLError";
+ }
+}
+
+export interface PostgresErrorOptions {
+ code: string;
+
+ detail?: string | undefined;
+ hint?: string | undefined;
+ severity?: string | undefined;
+ errno?: string | undefined;
+ position?: string | undefined;
+ internalPosition?: string | undefined;
+ internalQuery?: string | undefined;
+ where?: string | undefined;
+ schema?: string | undefined;
+ table?: string | undefined;
+ column?: string | undefined;
+ dataType?: string | undefined;
+ constraint?: string | undefined;
+ file?: string | undefined;
+ line?: string | undefined;
+ routine?: string | undefined;
+}
+
+class PostgresError extends SQLError implements Bun.SQL.PostgresError {
+ public readonly code: string;
+ public readonly detail: string | undefined;
+ public readonly hint: string | undefined;
+ public readonly severity: string | undefined;
+ public readonly errno: string | undefined;
+ public readonly position: string | undefined;
+ public readonly internalPosition: string | undefined;
+ public readonly internalQuery: string | undefined;
+ public readonly where: string | undefined;
+ public readonly schema: string | undefined;
+ public readonly table: string | undefined;
+ public readonly column: string | undefined;
+ public readonly dataType: string | undefined;
+ public readonly constraint: string | undefined;
+ public readonly file: string | undefined;
+ public readonly line: string | undefined;
+ public readonly routine: string | undefined;
+
+ constructor(message: string, options: PostgresErrorOptions) {
+ super(message);
+
+ this.name = "PostgresError";
+ this.code = options.code;
+
+ if (options.detail !== undefined) this.detail = options.detail;
+ if (options.hint !== undefined) this.hint = options.hint;
+ if (options.severity !== undefined) this.severity = options.severity;
+ if (options.errno !== undefined) this.errno = options.errno;
+ if (options.position !== undefined) this.position = options.position;
+ if (options.internalPosition !== undefined) this.internalPosition = options.internalPosition;
+ if (options.internalQuery !== undefined) this.internalQuery = options.internalQuery;
+ if (options.where !== undefined) this.where = options.where;
+ if (options.schema !== undefined) this.schema = options.schema;
+ if (options.table !== undefined) this.table = options.table;
+ if (options.column !== undefined) this.column = options.column;
+ if (options.dataType !== undefined) this.dataType = options.dataType;
+ if (options.constraint !== undefined) this.constraint = options.constraint;
+ if (options.file !== undefined) this.file = options.file;
+ if (options.line !== undefined) this.line = options.line;
+ if (options.routine !== undefined) this.routine = options.routine;
+ }
+}
+
+export interface SQLiteErrorOptions {
+ code: string;
+ errno: number;
+ byteOffset?: number | undefined;
+}
+
+class SQLiteError extends SQLError implements Bun.SQL.SQLiteError {
+ public readonly code: string;
+ public readonly errno: number;
+ public readonly byteOffset: number | undefined;
+
+ constructor(message: string, options: SQLiteErrorOptions) {
+ super(message);
+
+ this.name = "SQLiteError";
+ this.code = options.code;
+ this.errno = options.errno;
+
+ if (options.byteOffset !== undefined) this.byteOffset = options.byteOffset;
+ }
+}
+
+export default { PostgresError, SQLError, SQLiteError };
diff --git a/src/js/internal/sql/postgres.ts b/src/js/internal/sql/postgres.ts
new file mode 100644
index 0000000000..24f44e8cae
--- /dev/null
+++ b/src/js/internal/sql/postgres.ts
@@ -0,0 +1,1171 @@
+import type { Query } from "./query";
+import type { DatabaseAdapter, SQLHelper, SQLResultArray, SSLMode } from "./shared";
+
+const { SQLHelper, SSLMode, SQLResultArray } = require("internal/sql/shared");
+const {
+ Query,
+ SQLQueryFlags,
+ symbols: { _strings, _values, _flags, _results, _handle },
+} = require("internal/sql/query");
+const { escapeIdentifier, connectionClosedError } = require("internal/sql/utils");
+const { PostgresError } = require("internal/sql/errors");
+
+const {
+ createConnection: createPostgresConnection,
+ createQuery: createPostgresQuery,
+ init: initPostgres,
+} = $zig("postgres.zig", "createBinding") as PostgresDotZig;
+
+const cmds = ["", "INSERT", "DELETE", "UPDATE", "MERGE", "SELECT", "MOVE", "FETCH", "COPY"];
+
+initPostgres(
+ function onResolvePostgresQuery(query, result, commandTag, count, queries, is_last) {
+ /// simple queries
+ if (query[_flags] & SQLQueryFlags.simple) {
+ // simple can have multiple results or a single result
+ if (is_last) {
+ if (queries) {
+ const queriesIndex = queries.indexOf(query);
+ if (queriesIndex !== -1) {
+ queries.splice(queriesIndex, 1);
+ }
+ }
+ try {
+ query.resolve(query[_results]);
+ } catch {}
+ return;
+ }
+ $assert(result instanceof SQLResultArray, "Invalid result array");
+ // prepare for next query
+ query[_handle].setPendingValue(new SQLResultArray());
+
+ if (typeof commandTag === "string") {
+ if (commandTag.length > 0) {
+ result.command = commandTag;
+ }
+ } else {
+ result.command = cmds[commandTag];
+ }
+
+ result.count = count || 0;
+ const last_result = query[_results];
+
+ if (!last_result) {
+ query[_results] = result;
+ } else {
+ if (last_result instanceof SQLResultArray) {
+ // multiple results
+ query[_results] = [last_result, result];
+ } else {
+ // 3 or more results
+ last_result.push(result);
+ }
+ }
+ return;
+ }
+ /// prepared statements
+ $assert(result instanceof SQLResultArray, "Invalid result array");
+ if (typeof commandTag === "string") {
+ if (commandTag.length > 0) {
+ result.command = commandTag;
+ }
+ } else {
+ result.command = cmds[commandTag];
+ }
+
+ result.count = count || 0;
+ if (queries) {
+ const queriesIndex = queries.indexOf(query);
+ if (queriesIndex !== -1) {
+ queries.splice(queriesIndex, 1);
+ }
+ }
+ try {
+ query.resolve(result);
+ } catch {}
+ },
+
+ function onRejectPostgresQuery(query: Query, reject: Error, queries: Query[]) {
+ if (queries) {
+ const queriesIndex = queries.indexOf(query);
+ if (queriesIndex !== -1) {
+ queries.splice(queriesIndex, 1);
+ }
+ }
+
+ try {
+ query.reject(reject);
+ } catch {}
+ },
+);
+
+export interface PostgresDotZig {
+ init: (
+ onResolveQuery: (
+ query: Query,
+ result: SQLResultArray,
+ commandTag: string,
+ count: number,
+ queries: any,
+ is_last: boolean,
+ ) => void,
+ onRejectQuery: (query: Query, err: Error, queries) => void,
+ ) => void;
+ createConnection: (
+ hostname: string | undefined,
+ port: number,
+ username: string,
+ password: string,
+ databae: string,
+ sslmode: SSLMode,
+ tls: Bun.TLSOptions | boolean | null, // boolean true => empty TLSOptions object `{}`, boolean false or null => nothing
+ query: string,
+ path: string,
+ onConnected: (err: Error | null, connection: $ZigGeneratedClasses.PostgresSQLConnection) => void,
+ onDisconnected: (err: Error | null, connection: $ZigGeneratedClasses.PostgresSQLConnection) => void,
+ idleTimeout: number,
+ connectionTimeout: number,
+ maxLifetime: number,
+ useUnnamedPreparedStatements: boolean,
+ ) => $ZigGeneratedClasses.PostgresSQLConnection;
+ createQuery: (
+ sql: string,
+ values: unknown[],
+ pendingValue: SQLResultArray,
+ columns: string[] | undefined,
+ bigint: boolean,
+ simple: boolean,
+ ) => $ZigGeneratedClasses.PostgresSQLQuery;
+}
+
+const enum SQLCommand {
+ insert = 0,
+ update = 1,
+ updateSet = 2,
+ where = 3,
+ whereIn = 4,
+ none = -1,
+}
+export type { SQLCommand };
+
+function commandToString(command: SQLCommand): string {
+ switch (command) {
+ case SQLCommand.insert:
+ return "INSERT";
+ case SQLCommand.updateSet:
+ case SQLCommand.update:
+ return "UPDATE";
+ case SQLCommand.whereIn:
+ case SQLCommand.where:
+ return "WHERE";
+ default:
+ return "";
+ }
+}
+
+function detectCommand(query: string): SQLCommand {
+ const text = query.toLowerCase().trim();
+ const text_len = text.length;
+
+ let token = "";
+ let command = SQLCommand.none;
+ let quoted = false;
+ for (let i = 0; i < text_len; i++) {
+ const char = text[i];
+ switch (char) {
+ case " ": // Space
+ case "\n": // Line feed
+ case "\t": // Tab character
+ case "\r": // Carriage return
+ case "\f": // Form feed
+ case "\v": {
+ switch (token) {
+ case "insert": {
+ if (command === SQLCommand.none) {
+ return SQLCommand.insert;
+ }
+ return command;
+ }
+ case "update": {
+ if (command === SQLCommand.none) {
+ command = SQLCommand.update;
+ token = "";
+ continue; // try to find SET
+ }
+ return command;
+ }
+ case "where": {
+ command = SQLCommand.where;
+ token = "";
+ continue; // try to find IN
+ }
+ case "set": {
+ if (command === SQLCommand.update) {
+ command = SQLCommand.updateSet;
+ token = "";
+ continue; // try to find WHERE
+ }
+ return command;
+ }
+ case "in": {
+ if (command === SQLCommand.where) {
+ return SQLCommand.whereIn;
+ }
+ return command;
+ }
+ default: {
+ token = "";
+ continue;
+ }
+ }
+ }
+ default: {
+ // skip quoted commands
+ if (char === '"') {
+ quoted = !quoted;
+ continue;
+ }
+ if (!quoted) {
+ token += char;
+ }
+ }
+ }
+ }
+ if (token) {
+ switch (command) {
+ case SQLCommand.none: {
+ switch (token) {
+ case "insert":
+ return SQLCommand.insert;
+ case "update":
+ return SQLCommand.update;
+ case "where":
+ return SQLCommand.where;
+ default:
+ return SQLCommand.none;
+ }
+ }
+ case SQLCommand.update: {
+ if (token === "set") {
+ return SQLCommand.updateSet;
+ }
+ return SQLCommand.update;
+ }
+ case SQLCommand.where: {
+ if (token === "in") {
+ return SQLCommand.whereIn;
+ }
+ return SQLCommand.where;
+ }
+ }
+ }
+
+ return command;
+}
+
+const enum PooledConnectionState {
+ pending = 0,
+ connected = 1,
+ closed = 2,
+}
+
+const enum PooledConnectionFlags {
+ /// canBeConnected is used to indicate that at least one time we were able to connect to the database
+ canBeConnected = 1 << 0,
+ /// reserved is used to indicate that the connection is currently reserved
+ reserved = 1 << 1,
+ /// preReserved is used to indicate that the connection will be reserved in the future when queryCount drops to 0
+ preReserved = 1 << 2,
+}
+
+function onQueryFinish(this: PooledPostgresConnection, onClose: (err: Error) => void) {
+ this.queries.delete(onClose);
+ this.adapter.release(this);
+}
+
+class PooledPostgresConnection {
+ private static async createConnection(
+ options: Bun.SQL.__internal.DefinedPostgresOptions,
+ onConnected: (err: Error | null, connection: $ZigGeneratedClasses.PostgresSQLConnection) => void,
+ onClose: (err: Error | null) => void,
+ ): Promise<$ZigGeneratedClasses.PostgresSQLConnection | null> {
+ const {
+ hostname,
+ port,
+ username,
+ tls,
+ query,
+ database,
+ sslMode,
+ idleTimeout = 0,
+ connectionTimeout = 30 * 1000,
+ maxLifetime = 0,
+ prepare = true,
+
+ // @ts-expect-error path is currently removed from the types
+ path,
+ } = options;
+
+ let password: Bun.MaybePromise | string | undefined | (() => Bun.MaybePromise) = options.password;
+
+ try {
+ if (typeof password === "function") {
+ password = password();
+
+ if (password && $isPromise(password)) {
+ password = await password;
+ }
+ }
+
+ return createPostgresConnection(
+ hostname,
+ Number(port),
+ username || "",
+ password || "",
+ database || "",
+ // > The default value for sslmode is prefer. As is shown in the table, this
+ // makes no sense from a security point of view, and it only promises
+ // performance overhead if possible. It is only provided as the default for
+ // backward compatibility, and is not recommended in secure deployments.
+ sslMode || SSLMode.disable,
+ tls || null,
+ query || "",
+ path || "",
+ onConnected,
+ onClose,
+ idleTimeout,
+ connectionTimeout,
+ maxLifetime,
+ !prepare,
+ );
+ } catch (e) {
+ onClose(e as Error);
+ return null;
+ }
+ }
+
+ adapter: PostgresAdapter;
+ connection: $ZigGeneratedClasses.PostgresSQLConnection | null = null;
+ state: PooledConnectionState = PooledConnectionState.pending;
+ storedError: Error | null = null;
+ queries: Set<(err: Error) => void> = new Set();
+ onFinish: ((err: Error | null) => void) | null = null;
+ connectionInfo: Bun.SQL.__internal.DefinedPostgresOptions;
+ flags: number = 0;
+ /// queryCount is used to indicate the number of queries using the connection, if a connection is reserved or if its a transaction queryCount will be 1 independently of the number of queries
+ queryCount: number = 0;
+
+ #onConnected(err, _) {
+ const connectionInfo = this.connectionInfo;
+ if (connectionInfo?.onconnect) {
+ connectionInfo.onconnect(err);
+ }
+ this.storedError = err;
+ if (!err) {
+ this.flags |= PooledConnectionFlags.canBeConnected;
+ }
+ this.state = err ? PooledConnectionState.closed : PooledConnectionState.connected;
+ const onFinish = this.onFinish;
+ if (onFinish) {
+ this.queryCount = 0;
+ this.flags &= ~PooledConnectionFlags.reserved;
+ this.flags &= ~PooledConnectionFlags.preReserved;
+
+ // pool is closed, lets finish the connection
+ // pool is closed, lets finish the connection
+ if (err) {
+ onFinish(err);
+ } else {
+ this.connection?.close();
+ }
+ return;
+ }
+ this.adapter.release(this, true);
+ }
+
+ #onClose(err) {
+ const connectionInfo = this.connectionInfo;
+ if (connectionInfo?.onclose) {
+ connectionInfo.onclose(err);
+ }
+ this.state = PooledConnectionState.closed;
+ this.connection = null;
+ this.storedError = err;
+
+ // remove from ready connections if its there
+ this.adapter.readyConnections.delete(this);
+ const queries = new Set(this.queries);
+ this.queries.clear();
+ this.queryCount = 0;
+ this.flags &= ~PooledConnectionFlags.reserved;
+
+ // notify all queries that the connection is closed
+ for (const onClose of queries) {
+ onClose(err);
+ }
+ const onFinish = this.onFinish;
+ if (onFinish) {
+ onFinish(err);
+ }
+
+ this.adapter.release(this, true);
+ }
+
+ constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOptions, adapter: PostgresAdapter) {
+ this.state = PooledConnectionState.pending;
+ this.adapter = adapter;
+ this.connectionInfo = connectionInfo;
+ this.#startConnection();
+ }
+
+ async #startConnection() {
+ this.connection = await PooledPostgresConnection.createConnection(
+ this.connectionInfo,
+ this.#onConnected.bind(this),
+ this.#onClose.bind(this),
+ );
+ }
+
+ onClose(onClose: (err: Error) => void) {
+ this.queries.add(onClose);
+ }
+
+ bindQuery(query: Query, onClose: (err: Error) => void) {
+ this.queries.add(onClose);
+ query.finally(onQueryFinish.bind(this, onClose));
+ }
+
+ #doRetry() {
+ if (this.adapter.closed) {
+ return;
+ }
+ // reset error and state
+ this.storedError = null;
+ this.state = PooledConnectionState.pending;
+ // retry connection
+ this.#startConnection();
+ }
+ close() {
+ try {
+ if (this.state === PooledConnectionState.connected) {
+ this.connection?.close();
+ }
+ } catch {}
+ }
+ flush() {
+ this.connection?.flush();
+ }
+ retry() {
+ // if pool is closed, we can't retry
+ if (this.adapter.closed) {
+ return false;
+ }
+ // we need to reconnect
+ // lets use a retry strategy
+
+ // we can only retry if one day we are able to connect
+ if (this.flags & PooledConnectionFlags.canBeConnected) {
+ this.#doRetry();
+ } else {
+ // analyse type of error to see if we can retry
+ switch (this.storedError?.code) {
+ case "ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD":
+ case "ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD":
+ case "ERR_POSTGRES_TLS_NOT_AVAILABLE":
+ case "ERR_POSTGRES_TLS_UPGRADE_FAILED":
+ case "ERR_POSTGRES_INVALID_SERVER_SIGNATURE":
+ case "ERR_POSTGRES_INVALID_SERVER_KEY":
+ case "ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2":
+ // we can't retry these are authentication errors
+ return false;
+ default:
+ // we can retry
+ this.#doRetry();
+ }
+ }
+ return true;
+ }
+}
+
+export class PostgresAdapter
+ implements
+ DatabaseAdapter<
+ PooledPostgresConnection,
+ $ZigGeneratedClasses.PostgresSQLConnection,
+ $ZigGeneratedClasses.PostgresSQLQuery
+ >
+{
+ public readonly connectionInfo: Bun.SQL.__internal.DefinedPostgresOptions;
+
+ public readonly connections: PooledPostgresConnection[];
+ public readonly readyConnections: Set;
+
+ public waitingQueue: Array<(err: Error | null, result: any) => void> = [];
+ public reservedQueue: Array<(err: Error | null, result: any) => void> = [];
+
+ public poolStarted: boolean = false;
+ public closed: boolean = false;
+ public totalQueries: number = 0;
+ public onAllQueriesFinished: (() => void) | null = null;
+
+ constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOptions) {
+ this.connectionInfo = connectionInfo;
+ this.connections = new Array(connectionInfo.max);
+ this.readyConnections = new Set();
+ }
+
+ supportsReservedConnections() {
+ return true;
+ }
+
+ getConnectionForQuery(pooledConnection: PooledPostgresConnection) {
+ return pooledConnection.connection;
+ }
+
+ attachConnectionCloseHandler(connection: PooledPostgresConnection, handler: () => void): void {
+ // PostgreSQL pooled connections support onClose handlers
+ if (connection.onClose) {
+ connection.onClose(handler);
+ }
+ }
+
+ detachConnectionCloseHandler(connection: PooledPostgresConnection, handler: () => void): void {
+ // PostgreSQL pooled connections track queries
+ if (connection.queries) {
+ connection.queries.delete(handler);
+ }
+ }
+
+ getTransactionCommands(options?: string): import("./shared").TransactionCommands {
+ let BEGIN = "BEGIN";
+ if (options) {
+ BEGIN = `BEGIN ${options}`;
+ }
+
+ return {
+ BEGIN,
+ COMMIT: "COMMIT",
+ ROLLBACK: "ROLLBACK",
+ SAVEPOINT: "SAVEPOINT",
+ RELEASE_SAVEPOINT: "RELEASE SAVEPOINT",
+ ROLLBACK_TO_SAVEPOINT: "ROLLBACK TO SAVEPOINT",
+ };
+ }
+
+ getDistributedTransactionCommands(name: string): import("./shared").TransactionCommands | null {
+ if (!this.validateDistributedTransactionName(name).valid) {
+ return null;
+ }
+
+ return {
+ BEGIN: "BEGIN",
+ COMMIT: `PREPARE TRANSACTION '${name}'`,
+ ROLLBACK: "ROLLBACK",
+ SAVEPOINT: "SAVEPOINT",
+ RELEASE_SAVEPOINT: "RELEASE SAVEPOINT",
+ ROLLBACK_TO_SAVEPOINT: "ROLLBACK TO SAVEPOINT",
+ BEFORE_COMMIT_OR_ROLLBACK: null,
+ };
+ }
+
+ validateTransactionOptions(_options: string): { valid: boolean; error?: string } {
+ // PostgreSQL accepts any transaction options
+ return { valid: true };
+ }
+
+ validateDistributedTransactionName(name: string): { valid: boolean; error?: string } {
+ if (name.indexOf("'") !== -1) {
+ return {
+ valid: false,
+ error: "Distributed transaction name cannot contain single quotes.",
+ };
+ }
+ return { valid: true };
+ }
+
+ getCommitDistributedSQL(name: string): string {
+ const validation = this.validateDistributedTransactionName(name);
+ if (!validation.valid) {
+ throw new Error(validation.error);
+ }
+ return `COMMIT PREPARED '${name}'`;
+ }
+
+ getRollbackDistributedSQL(name: string): string {
+ const validation = this.validateDistributedTransactionName(name);
+ if (!validation.valid) {
+ throw new Error(validation.error);
+ }
+ return `ROLLBACK PREPARED '${name}'`;
+ }
+
+ createQueryHandle(sql: string, values: unknown[], flags: number) {
+ if (!(flags & SQLQueryFlags.allowUnsafeTransaction)) {
+ if (this.connectionInfo.max !== 1) {
+ const upperCaseSqlString = sql.toUpperCase().trim();
+ if (upperCaseSqlString.startsWith("BEGIN") || upperCaseSqlString.startsWith("START TRANSACTION")) {
+ throw new PostgresError("Only use sql.begin, sql.reserved or max: 1", {
+ code: "ERR_POSTGRES_UNSAFE_TRANSACTION",
+ });
+ }
+ }
+ }
+
+ return createPostgresQuery(
+ sql,
+ values,
+ new SQLResultArray(),
+ undefined,
+ !!(flags & SQLQueryFlags.bigint),
+ !!(flags & SQLQueryFlags.simple),
+ );
+ }
+
+ maxDistribution() {
+ if (!this.waitingQueue.length) return 0;
+ const result = Math.ceil((this.waitingQueue.length + this.totalQueries) / this.connections.length);
+ return result ? result : 1;
+ }
+
+ flushConcurrentQueries() {
+ const maxDistribution = this.maxDistribution();
+ if (maxDistribution === 0) {
+ return;
+ }
+
+ while (true) {
+ const nonReservedConnections = Array.from(this.readyConnections).filter(
+ c => !(c.flags & PooledConnectionFlags.preReserved) && c.queryCount < maxDistribution,
+ );
+ if (nonReservedConnections.length === 0) {
+ return;
+ }
+ const orderedConnections = nonReservedConnections.sort((a, b) => a.queryCount - b.queryCount);
+ for (const connection of orderedConnections) {
+ const pending = this.waitingQueue.shift();
+ if (!pending) {
+ return;
+ }
+ connection.queryCount++;
+ this.totalQueries++;
+ pending(null, connection);
+ }
+ }
+ }
+
+ release(connection: PooledPostgresConnection, connectingEvent: boolean = false) {
+ if (!connectingEvent) {
+ connection.queryCount--;
+ this.totalQueries--;
+ }
+ const currentQueryCount = connection.queryCount;
+ if (currentQueryCount == 0) {
+ connection.flags &= ~PooledConnectionFlags.reserved;
+ connection.flags &= ~PooledConnectionFlags.preReserved;
+ }
+ if (this.onAllQueriesFinished) {
+ // we are waiting for all queries to finish, lets check if we can call it
+ if (!this.hasPendingQueries()) {
+ this.onAllQueriesFinished();
+ }
+ }
+
+ if (connection.state !== PooledConnectionState.connected) {
+ // connection is not ready
+ if (connection.storedError) {
+ // this connection got a error but maybe we can wait for another
+
+ if (this.hasConnectionsAvailable()) {
+ return;
+ }
+
+ const waitingQueue = this.waitingQueue;
+ const reservedQueue = this.reservedQueue;
+
+ this.waitingQueue = [];
+ this.reservedQueue = [];
+ // we have no connections available so lets fails
+ for (const pending of waitingQueue) {
+ pending(connection.storedError, connection);
+ }
+ for (const pending of reservedQueue) {
+ pending(connection.storedError, connection);
+ }
+ }
+ return;
+ }
+
+ if (currentQueryCount == 0) {
+ // ok we can actually bind reserved queries to it
+ const pendingReserved = this.reservedQueue.shift();
+ if (pendingReserved) {
+ connection.flags |= PooledConnectionFlags.reserved;
+ connection.queryCount++;
+ this.totalQueries++;
+ // we have a connection waiting for a reserved connection lets prioritize it
+ pendingReserved(connection.storedError, connection);
+ return;
+ }
+ }
+ this.readyConnections.add(connection);
+ this.flushConcurrentQueries();
+ }
+
+ hasConnectionsAvailable() {
+ if (this.readyConnections.size > 0) return true;
+ if (this.poolStarted) {
+ const pollSize = this.connections.length;
+ for (let i = 0; i < pollSize; i++) {
+ const connection = this.connections[i];
+ if (connection.state !== PooledConnectionState.closed) {
+ // some connection is connecting or connected
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ hasPendingQueries() {
+ if (this.waitingQueue.length > 0 || this.reservedQueue.length > 0) return true;
+ if (this.poolStarted) {
+ return this.totalQueries > 0;
+ }
+ return false;
+ }
+ isConnected() {
+ if (this.readyConnections.size > 0) {
+ return true;
+ }
+ if (this.poolStarted) {
+ const pollSize = this.connections.length;
+ for (let i = 0; i < pollSize; i++) {
+ const connection = this.connections[i];
+ if (connection.state === PooledConnectionState.connected) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ flush() {
+ if (this.closed) {
+ return;
+ }
+ if (this.poolStarted) {
+ const pollSize = this.connections.length;
+ for (let i = 0; i < pollSize; i++) {
+ const connection = this.connections[i];
+ if (connection.state === PooledConnectionState.connected) {
+ connection.connection?.flush();
+ }
+ }
+ }
+ }
+
+ async #close() {
+ let pending;
+ while ((pending = this.waitingQueue.shift())) {
+ pending(connectionClosedError(), null);
+ }
+ while (this.reservedQueue.length > 0) {
+ const pendingReserved = this.reservedQueue.shift();
+ if (pendingReserved) {
+ pendingReserved(connectionClosedError(), null);
+ }
+ }
+
+ const promises: Array> = [];
+
+ if (this.poolStarted) {
+ this.poolStarted = false;
+ const pollSize = this.connections.length;
+ for (let i = 0; i < pollSize; i++) {
+ const connection = this.connections[i];
+ switch (connection.state) {
+ case PooledConnectionState.pending:
+ {
+ const { promise, resolve } = Promise.withResolvers();
+ connection.onFinish = resolve;
+ promises.push(promise);
+ connection.connection?.close();
+ }
+ break;
+
+ case PooledConnectionState.connected:
+ {
+ const { promise, resolve } = Promise.withResolvers();
+ connection.onFinish = resolve;
+ promises.push(promise);
+ connection.connection?.close();
+ }
+ break;
+ }
+ // clean connection reference
+ // @ts-ignore
+ this.connections[i] = null;
+ }
+ }
+
+ this.readyConnections.clear();
+ this.waitingQueue.length = 0;
+ return Promise.all(promises);
+ }
+
+ async close(options?: { timeout?: number }) {
+ if (this.closed) {
+ return;
+ }
+
+ let timeout = options?.timeout;
+ if (timeout) {
+ timeout = Number(timeout);
+ if (timeout > 2 ** 31 || timeout < 0 || timeout !== timeout) {
+ throw $ERR_INVALID_ARG_VALUE("options.timeout", timeout, "must be a non-negative integer less than 2^31");
+ }
+
+ this.closed = true;
+ if (timeout === 0 || !this.hasPendingQueries()) {
+ // close immediately
+ await this.#close();
+ return;
+ }
+
+ const { promise, resolve } = Promise.withResolvers();
+ const timer = setTimeout(() => {
+ // timeout is reached, lets close and probably fail some queries
+ this.#close().finally(resolve);
+ }, timeout * 1000);
+ timer.unref(); // dont block the event loop
+
+ this.onAllQueriesFinished = () => {
+ clearTimeout(timer);
+ // everything is closed, lets close the pool
+ this.#close().finally(resolve);
+ };
+
+ return promise;
+ } else {
+ this.closed = true;
+ if (!this.hasPendingQueries()) {
+ // close immediately
+ await this.#close();
+ return;
+ }
+
+ // gracefully close the pool
+ const { promise, resolve } = Promise.withResolvers();
+
+ this.onAllQueriesFinished = () => {
+ // everything is closed, lets close the pool
+ this.#close().finally(resolve);
+ };
+
+ return promise;
+ }
+ }
+
+ /**
+ * @param {function} onConnected - The callback function to be called when the connection is established.
+ * @param {boolean} reserved - Whether the connection is reserved, if is reserved the connection will not be released until release is called, if not release will only decrement the queryCount counter
+ */
+ connect(onConnected: (err: Error | null, result: any) => void, reserved: boolean = false) {
+ if (this.closed) {
+ return onConnected(connectionClosedError(), null);
+ }
+
+ if (this.readyConnections.size === 0) {
+ // no connection ready lets make some
+ let retry_in_progress = false;
+ let all_closed = true;
+ let storedError: Error | null = null;
+
+ if (this.poolStarted) {
+ // we already started the pool
+ // lets check if some connection is available to retry
+ const pollSize = this.connections.length;
+ for (let i = 0; i < pollSize; i++) {
+ const connection = this.connections[i];
+ // we need a new connection and we have some connections that can retry
+ if (connection.state === PooledConnectionState.closed) {
+ if (connection.retry()) {
+ // lets wait for connection to be released
+ if (!retry_in_progress) {
+ // avoid adding to the queue twice, we wanna to retry every available pool connection
+ retry_in_progress = true;
+ if (reserved) {
+ // we are not sure what connection will be available so we dont pre reserve
+ this.reservedQueue.push(onConnected);
+ } else {
+ this.waitingQueue.push(onConnected);
+ }
+ }
+ } else {
+ // we have some error, lets grab it and fail if unable to start a connection
+ storedError = connection.storedError;
+ }
+ } else {
+ // we have some pending or open connections
+ all_closed = false;
+ }
+ }
+ if (!all_closed && !retry_in_progress) {
+ // is possible to connect because we have some working connections, or we are just without network for some reason
+ // wait for connection to be released or fail
+ if (reserved) {
+ // we are not sure what connection will be available so we dont pre reserve
+ this.reservedQueue.push(onConnected);
+ } else {
+ this.waitingQueue.push(onConnected);
+ }
+ } else if (!retry_in_progress) {
+ // impossible to connect or retry
+ onConnected(storedError ?? connectionClosedError(), null);
+ }
+ return;
+ }
+ // we never started the pool, lets start it
+ if (reserved) {
+ this.reservedQueue.push(onConnected);
+ } else {
+ this.waitingQueue.push(onConnected);
+ }
+ this.poolStarted = true;
+ const pollSize = this.connections.length;
+ // pool is always at least 1 connection
+ const firstConnection = new PooledPostgresConnection(this.connectionInfo, this);
+ this.connections[0] = firstConnection;
+ if (reserved) {
+ firstConnection.flags |= PooledConnectionFlags.preReserved; // lets pre reserve the first connection
+ }
+ for (let i = 1; i < pollSize; i++) {
+ this.connections[i] = new PooledPostgresConnection(this.connectionInfo, this);
+ }
+ return;
+ }
+ if (reserved) {
+ let connectionWithLeastQueries: PooledPostgresConnection | null = null;
+ let leastQueries = Infinity;
+ for (const connection of this.readyConnections) {
+ if (connection.flags & PooledConnectionFlags.preReserved || connection.flags & PooledConnectionFlags.reserved)
+ continue;
+ const queryCount = connection.queryCount;
+ if (queryCount > 0) {
+ if (queryCount < leastQueries) {
+ leastQueries = queryCount;
+ connectionWithLeastQueries = connection;
+ }
+ continue;
+ }
+ connection.flags |= PooledConnectionFlags.reserved;
+ connection.queryCount++;
+ this.totalQueries++;
+ this.readyConnections.delete(connection);
+ onConnected(null, connection);
+ return;
+ }
+
+ if (connectionWithLeastQueries) {
+ // lets mark the connection with the least queries as preReserved if any
+ connectionWithLeastQueries.flags |= PooledConnectionFlags.preReserved;
+ }
+
+ // no connection available to be reserved lets wait for a connection to be released
+ this.reservedQueue.push(onConnected);
+ } else {
+ this.waitingQueue.push(onConnected);
+ this.flushConcurrentQueries();
+ }
+ }
+
+ normalizeQuery(strings: string | TemplateStringsArray, values: unknown[], binding_idx = 1): [string, unknown[]] {
+ if (typeof strings === "string") {
+ // identifier or unsafe query
+ return [strings, values || []];
+ }
+
+ if (!$isArray(strings)) {
+ // we should not hit this path
+ throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
+ }
+
+ const str_len = strings.length;
+ if (str_len === 0) {
+ return ["", []];
+ }
+
+ let binding_values: any[] = [];
+ let query = "";
+
+ for (let i = 0; i < str_len; i++) {
+ const string = strings[i];
+
+ if (typeof string === "string") {
+ query += string;
+
+ if (values.length > i) {
+ const value = values[i];
+
+ if (value instanceof Query) {
+ const q = value as Query;
+ const [sub_query, sub_values] = this.normalizeQuery(q[_strings], q[_values], binding_idx);
+
+ query += sub_query;
+ for (let j = 0; j < sub_values.length; j++) {
+ binding_values.push(sub_values[j]);
+ }
+ binding_idx += sub_values.length;
+ } else if (value instanceof SQLHelper) {
+ const command = detectCommand(query);
+ // only selectIn, insert, update, updateSet are allowed
+ if (command === SQLCommand.none || command === SQLCommand.where) {
+ throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands");
+ }
+ const { columns, value: items } = value as SQLHelper;
+ const columnCount = columns.length;
+ if (columnCount === 0 && command !== SQLCommand.whereIn) {
+ throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`);
+ }
+ const lastColumnIndex = columns.length - 1;
+
+ if (command === SQLCommand.insert) {
+ //
+ // insert into users ${sql(users)} or insert into users ${sql(user)}
+ //
+
+ query += "(";
+ for (let j = 0; j < columnCount; j++) {
+ query += escapeIdentifier(columns[j]);
+ if (j < lastColumnIndex) {
+ query += ", ";
+ }
+ }
+ query += ") VALUES";
+ if ($isArray(items)) {
+ const itemsCount = items.length;
+ const lastItemIndex = itemsCount - 1;
+ for (let j = 0; j < itemsCount; j++) {
+ query += "(";
+ const item = items[j];
+ for (let k = 0; k < columnCount; k++) {
+ const column = columns[k];
+ const columnValue = item[column];
+ query += `$${binding_idx++}${k < lastColumnIndex ? ", " : ""}`;
+ if (typeof columnValue === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(columnValue);
+ }
+ }
+ if (j < lastItemIndex) {
+ query += "),";
+ } else {
+ query += ") "; // the user can add RETURNING * or RETURNING id
+ }
+ }
+ } else {
+ query += "(";
+ const item = items;
+ for (let j = 0; j < columnCount; j++) {
+ const column = columns[j];
+ const columnValue = item[column];
+ query += `$${binding_idx++}${j < lastColumnIndex ? ", " : ""}`;
+ if (typeof columnValue === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(columnValue);
+ }
+ }
+ query += ") "; // the user can add RETURNING * or RETURNING id
+ }
+ } else if (command === SQLCommand.whereIn) {
+ // SELECT * FROM users WHERE id IN (${sql([1, 2, 3])})
+ if (!$isArray(items)) {
+ throw new SyntaxError("An array of values is required for WHERE IN helper");
+ }
+ const itemsCount = items.length;
+ const lastItemIndex = itemsCount - 1;
+ query += "(";
+ for (let j = 0; j < itemsCount; j++) {
+ query += `$${binding_idx++}${j < lastItemIndex ? ", " : ""}`;
+ if (columnCount > 0) {
+ // we must use a key from a object
+ if (columnCount > 1) {
+ // we should not pass multiple columns here
+ throw new SyntaxError("Cannot use WHERE IN helper with multiple columns");
+ }
+ // SELECT * FROM users WHERE id IN (${sql(users, "id")})
+ const value = items[j];
+ if (typeof value === "undefined") {
+ binding_values.push(null);
+ } else {
+ const value_from_key = value[columns[0]];
+
+ if (typeof value_from_key === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(value_from_key);
+ }
+ }
+ } else {
+ const value = items[j];
+ if (typeof value === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(value);
+ }
+ }
+ }
+ query += ") "; // more conditions can be added after this
+ } else {
+ // UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1
+ let item;
+ if ($isArray(items)) {
+ if (items.length > 1) {
+ throw new SyntaxError("Cannot use array of objects for UPDATE");
+ }
+ item = items[0];
+ } else {
+ item = items;
+ }
+ // no need to include if is updateSet
+ if (command === SQLCommand.update) {
+ query += " SET ";
+ }
+ for (let i = 0; i < columnCount; i++) {
+ const column = columns[i];
+ const columnValue = item[column];
+ query += `${escapeIdentifier(column)} = $${binding_idx++}${i < lastColumnIndex ? ", " : ""}`;
+ if (typeof columnValue === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(columnValue);
+ }
+ }
+ query += " "; // the user can add where clause after this
+ }
+ } else {
+ //TODO: handle sql.array parameters
+ query += `$${binding_idx++} `;
+ if (typeof value === "undefined") {
+ binding_values.push(null);
+ } else {
+ binding_values.push(value);
+ }
+ }
+ }
+ } else {
+ throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
+ }
+ }
+
+ return [query, binding_values];
+ }
+}
+
+export default {
+ PostgresAdapter,
+ SQLCommand,
+ commandToString,
+ detectCommand,
+};
diff --git a/src/js/internal/sql/query.ts b/src/js/internal/sql/query.ts
new file mode 100644
index 0000000000..dedd2016cd
--- /dev/null
+++ b/src/js/internal/sql/query.ts
@@ -0,0 +1,330 @@
+import type { DatabaseAdapter } from "./shared.ts";
+const { escapeIdentifier, notTaggedCallError } = require("internal/sql/utils");
+
+const _resolve = Symbol("resolve");
+const _reject = Symbol("reject");
+const _handle = Symbol("handle");
+const _run = Symbol("run");
+const _queryStatus = Symbol("status");
+const _handler = Symbol("handler");
+const _strings = Symbol("strings");
+const _values = Symbol("values");
+const _flags = Symbol("flags");
+const _results = Symbol("results");
+const _adapter = Symbol("adapter");
+
+const PublicPromise = Promise;
+
+export interface BaseQueryHandle {
+ done?(): void;
+ cancel?(): void;
+ setMode(mode: SQLQueryResultMode): void;
+ run(connection: Connection, query: Query): void | Promise;
+}
+
+export type { Query };
+class Query> extends PublicPromise {
+ public [_resolve]: (value: T) => void;
+ public [_reject]: (reason?: Error) => void;
+ public [_handle]: Handle | null;
+ public [_handler]: (query: Query, handle: Handle) => T;
+ public [_queryStatus]: SQLQueryStatus;
+ public [_strings]: string | TemplateStringsArray;
+ public [_values]: any[];
+ public [_flags]: SQLQueryFlags;
+
+ public readonly [_adapter]: DatabaseAdapter;
+
+ [Symbol.for("nodejs.util.inspect.custom")](): `Query { ${string} }` {
+ const status = this[_queryStatus];
+
+ let query = "";
+ if ((status & SQLQueryStatus.active) != 0) query += "active ";
+ if ((status & SQLQueryStatus.cancelled) != 0) query += "cancelled ";
+ if ((status & SQLQueryStatus.executed) != 0) query += "executed ";
+ if ((status & SQLQueryStatus.error) != 0) query += "error ";
+
+ return `Query { ${query.trimEnd()} }`;
+ }
+
+ private getQueryHandle() {
+ let handle = this[_handle];
+
+ if (!handle) {
+ try {
+ const [sql, values] = this[_adapter].normalizeQuery(this[_strings], this[_values]);
+ this[_handle] = handle = this[_adapter].createQueryHandle(sql, values, this[_flags]);
+ } catch (err) {
+ this[_queryStatus] |= SQLQueryStatus.error | SQLQueryStatus.invalidHandle;
+ this.reject(err as Error);
+ }
+ }
+
+ return handle;
+ }
+
+ constructor(
+ strings: string | TemplateStringsArray,
+ values: any[],
+ flags: number,
+ handler,
+ adapter: DatabaseAdapter