Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(sql) transactions, savepoints, connection pooling and reserve #16381

Merged
merged 35 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f6bbc40
wip
cirospaciari Jan 13, 2025
f11f674
make it work again
cirospaciari Jan 14, 2025
4802de3
release query
cirospaciari Jan 14, 2025
93c58c9
fix max
cirospaciari Jan 14, 2025
84088b2
dont allow 0 on max
cirospaciari Jan 14, 2025
dd12c0d
not TODO
cirospaciari Jan 14, 2025
7e80e40
more
cirospaciari Jan 14, 2025
dc008ae
reserve()
cirospaciari Jan 14, 2025
8a92882
dispose
cirospaciari Jan 14, 2025
2acbd82
check unsafe transactions
cirospaciari Jan 14, 2025
0cd41ec
change check order
cirospaciari Jan 14, 2025
40c24ae
check .zero
cirospaciari Jan 15, 2025
caa82a0
fix promise rejection
cirospaciari Jan 15, 2025
279d848
Promise.all
cirospaciari Jan 15, 2025
91a10aa
use START TRANSACTION instead of BEGIN
cirospaciari Jan 15, 2025
7fbb8ae
make transactions adapter independent
cirospaciari Jan 15, 2025
d8ba2cb
add distributed transactions
cirospaciari Jan 15, 2025
0425019
test
cirospaciari Jan 15, 2025
f27f3e3
more tests
cirospaciari Jan 15, 2025
09e6b35
wip
cirospaciari Jan 16, 2025
be7c841
wip
cirospaciari Jan 16, 2025
a06f824
wip
cirospaciari Jan 18, 2025
775632a
comments
cirospaciari Jan 18, 2025
ae5aa88
wip
cirospaciari Jan 18, 2025
69409d0
status as enum, replace booleans for flags
cirospaciari Jan 18, 2025
7387e12
more
cirospaciari Jan 18, 2025
a068077
wip
cirospaciari Jan 18, 2025
ddabf38
more
cirospaciari Jan 18, 2025
be9b5fe
fix transaction close and reserved close
cirospaciari Jan 18, 2025
9ccafa2
remove bun:sql wip types
cirospaciari Jan 18, 2025
aadf6e2
examples
cirospaciari Jan 18, 2025
bee20b6
constructor
cirospaciari Jan 18, 2025
8d7884f
more
cirospaciari Jan 18, 2025
d3861a3
types
cirospaciari Jan 18, 2025
66dd28e
SQL and sql
cirospaciari Jan 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,298 @@ declare module "bun" {
*/
stat(path: string, options?: S3Options): Promise<S3Stats>;
};
/**
* Configuration options for SQL client connection and behavior
* @example
* const config: SQLOptions = {
* host: 'localhost',
* port: 5432,
* user: 'dbuser',
* password: 'secretpass',
* database: 'myapp',
* idleTimeout: 30000,
* max: 20,
* onconnect: (client) => {
* console.log('Connected to database');
* }
* };
*/
type SQLOptions = {
/** Connection URL (can be string or URL object) */
url: URL | string;
/** Database server hostname */
host: string;
/** Database server port number */
port: number | string;
/** Database user for authentication */
user: string;
/** Database password for authentication */
password: string;
/** Name of the database to connect to */
database: string;
/** Database adapter/driver to use */
adapter: string;
/** Maximum time in milliseconds to wait for connection to become available */
idleTimeout: number;
/** Maximum time in milliseconds to wait when establishing a connection */
connectionTimeout: number;
/** Maximum lifetime in milliseconds of a connection */
maxLifetime: number;
/** Whether to use TLS/SSL for the connection */
tls: boolean;
/** Callback function executed when a connection is established */
onconnect: (client: SQL) => void;
/** Callback function executed when a connection is closed */
onclose: (client: SQL) => void;
/** Maximum number of connections in the pool */
max: number;
};

/**
* Represents a SQL query that can be executed, with additional control methods
* Extends Promise to allow for async/await usage
*/
interface SQLQuery extends Promise<any> {
/** Indicates if the query is currently executing */
active: boolean;
/** Indicates if the query has been cancelled */
cancelled: boolean;
/** Cancels the executing query */
cancel(): SQLQuery;
/** Executes the query */
execute(): SQLQuery;
/** Returns the raw query result */
raw(): SQLQuery;
/** Returns only the values from the query result */
values(): SQLQuery;
}

/**
* Callback function type for transaction contexts
* @param sql Function to execute SQL queries within the transaction
*/
type SQLContextCallback = (sql: (strings: string, ...values: any[]) => SQLQuery | Array<SQLQuery>) => Promise<any>;

/**
* Main SQL client interface providing connection and transaction management
*/
interface SQL {
/** Creates a new SQL client instance
* @example
* 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
* @example
* const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 });
*/
new (connectionString: string | URL, options: SQLOptions): SQL;
/** Creates a new SQL client instance with options
* @example
* const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 });
*/
new (options?: SQLOptions): SQL;
/** Executes a SQL query using template literals
* @example
* const [user] = await sql`select * from users where id = ${1}`;
*/
(strings: string, ...values: any[]): SQLQuery;
/** Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
* @example
* await sql.commitDistributed("my_distributed_transaction");
*/
commitDistributed(name: string): Promise<undefined>;
/** Rolls back a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
* @example
* await sql.rollbackDistributed("my_distributed_transaction");
*/
rollbackDistributed(name: string): Promise<undefined>;
/** Waits for the database connection to be established
* @example
* await sql.connect();
*/
connect(): Promise<SQL>;
/** Closes the database connection with optional timeout in seconds
* @example
* await sql.close({ timeout: 1 });
*/
close(options?: { timeout?: number }): Promise<undefined>;
/** Closes the database connection with optional timeout in seconds
* @alias close
* @example
* await sql.end({ timeout: 1 });
*/
end(options?: { timeout?: number }): Promise<undefined>;
/** Flushes any pending operations */
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
* 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();
* }
* //To make it simpler bun supportsSymbol.dispose and Symbol.asyncDispose
* {
* // always release after context (safer)
* using reserved = await sql.reserve()
* await reserved`select * from users`
* }
*/
reserve(): Promise<ReservedSQL>;
/** 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: SQLContextCallback): Promise<any>;
/** 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: SQLContextCallback): Promise<any>;
/** 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: SQLContextCallback): Promise<any>;
/** 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 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: SQLContextCallback): Promise<any>;
/** 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: SQLContextCallback): Promise<any>;
/** Alternative method to begin a distributed transaction
* @alias beginDistributed
*/
distributed(name: string, fn: SQLContextCallback): Promise<any>;
/** Current client options */
options: SQLOptions;
}

/**
* Represents a reserved connection from the connection pool
* Extends SQL with additional release functionality
*/
interface ReservedSQL extends SQL {
/** 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: SQLContextCallback): Promise<undefined>;
}

var sql: SQL;

/**
* This lets you use macros as regular imports
Expand Down
15 changes: 13 additions & 2 deletions src/bun.js/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ static JSValue constructPluginObject(VM& vm, JSObject* bunObject)
return pluginFunction;
}

static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject)
static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject)
{
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
Expand All @@ -301,6 +301,16 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject)
return sqlValue.getObject()->get(globalObject, vm.propertyNames->defaultKeyword);
}

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);
RETURN_IF_EXCEPTION(scope, {});
auto clientData = WebCore::clientData(vm);
return sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName());
}

extern "C" JSC::EncodedJSValue JSPasswordObject__create(JSGlobalObject*);

static JSValue constructPasswordObject(VM& vm, JSObject* bunObject)
Expand Down Expand Up @@ -745,7 +755,8 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
revision constructBunRevision ReadOnly|DontDelete|PropertyCallback
semver BunObject_getter_wrap_semver ReadOnly|DontDelete|PropertyCallback
s3 BunObject_callback_s3 DontDelete|Function 1
sql constructBunSQLObject DontDelete|PropertyCallback
sql defaultBunSQLObject DontDelete|PropertyCallback
SQL constructBunSQLObject DontDelete|PropertyCallback
serve BunObject_callback_serve DontDelete|Function 1
sha BunObject_callback_sha DontDelete|Function 1
shrink BunObject_callback_shrink DontDelete|Function 1
Expand Down
3 changes: 3 additions & 0 deletions src/bun.js/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ const errors: ErrorCodeMapping = [
["ERR_POSTGRES_IDLE_TIMEOUT", Error, "PostgresError"],
["ERR_POSTGRES_CONNECTION_TIMEOUT", Error, "PostgresError"],
["ERR_POSTGRES_LIFETIME_TIMEOUT", Error, "PostgresError"],
["ERR_POSTGRES_INVALID_TRANSACTION_STATE", Error, "PostgresError"],
["ERR_POSTGRES_QUERY_CANCELLED", Error, "PostgresError"],
["ERR_POSTGRES_UNSAFE_TRANSACTION", Error, "PostgresError"],

// S3
["ERR_S3_MISSING_CREDENTIALS", Error],
Expand Down
1 change: 0 additions & 1 deletion src/bun.js/event_loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,6 @@ pub const EventLoop = struct {
pub fn runCallback(this: *EventLoop, callback: JSC.JSValue, globalObject: *JSC.JSGlobalObject, thisValue: JSC.JSValue, arguments: []const JSC.JSValue) void {
this.enter();
defer this.exit();

_ = callback.call(globalObject, thisValue, arguments) catch |err|
globalObject.reportActiveExceptionAsUnhandled(err);
}
Expand Down
10 changes: 0 additions & 10 deletions src/bun.js/module_loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2514,14 +2514,7 @@ pub const ModuleLoader = struct {

// These are defined in src/js/*
.@"bun:ffi" => return jsSyntheticModule(.@"bun:ffi", specifier),
.@"bun:sql" => {
if (!Environment.isDebug) {
if (!is_allowed_to_use_internal_testing_apis and !bun.FeatureFlags.postgresql)
return null;
}

return jsSyntheticModule(.@"bun:sql", specifier);
},
.@"bun:sqlite" => return jsSyntheticModule(.@"bun:sqlite", specifier),
.@"detect-libc" => return jsSyntheticModule(if (!Environment.isLinux) .@"detect-libc" else if (!Environment.isMusl) .@"detect-libc/linux" else .@"detect-libc/musl", specifier),
.@"node:assert" => return jsSyntheticModule(.@"node:assert", specifier),
Expand Down Expand Up @@ -2729,7 +2722,6 @@ pub const HardcodedModule = enum {
@"bun:jsc",
@"bun:main",
@"bun:test", // usually replaced by the transpiler but `await import("bun:" + "test")` has to work
@"bun:sql",
@"bun:sqlite",
@"detect-libc",
@"node:assert",
Expand Down Expand Up @@ -2816,7 +2808,6 @@ pub const HardcodedModule = enum {
.{ "bun:test", HardcodedModule.@"bun:test" },
.{ "bun:sqlite", HardcodedModule.@"bun:sqlite" },
.{ "bun:internal-for-testing", HardcodedModule.@"bun:internal-for-testing" },
.{ "bun:sql", HardcodedModule.@"bun:sql" },
.{ "detect-libc", HardcodedModule.@"detect-libc" },
.{ "node-fetch", HardcodedModule.@"node-fetch" },
.{ "isomorphic-fetch", HardcodedModule.@"isomorphic-fetch" },
Expand Down Expand Up @@ -3056,7 +3047,6 @@ pub const HardcodedModule = enum {
.{ "bun:ffi", .{ .path = "bun:ffi" } },
.{ "bun:jsc", .{ .path = "bun:jsc" } },
.{ "bun:sqlite", .{ .path = "bun:sqlite" } },
.{ "bun:sql", .{ .path = "bun:sql" } },
.{ "bun:wrap", .{ .path = "bun:wrap" } },
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
.{ "ffi", .{ .path = "bun:ffi" } },
Expand Down
1 change: 1 addition & 0 deletions src/js/builtins/BunBuiltinNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ using namespace JSC;
macro(written) \
macro(napiDlopenHandle) \
macro(napiWrappedContents) \
macro(SQL) \
BUN_ADDITIONAL_BUILTIN_NAMES(macro)
// --- END of BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME ---

Expand Down
Loading
Loading