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

Conversation

cirospaciari
Copy link
Member

@cirospaciari cirospaciari commented Jan 13, 2025

What does this PR do?

  • Documentation or TypeScript types (it's okay to leave the rest blank in this case)
  • Code changes

How did you verify your code works?

Add support for transactions:

BEGIN / COMMIT await sql.begin([options = ''], fn) -> fn()

import { SQL } from "bun:sql";

const sql = new SQL({
  host: "localhost",
  port: 5432,
  user: "ciro",
  password: "bunbunbun",
  database: "bun",
});

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]
})

SAVEPOINT await sql.savepoint([name], fn) -> fn()

sql.begin('read write', async sql => {
  const [user] = await sql`
    insert into users (
      name
    ) values (
      'Murray'
    )
  `

  const [account] = (await sql.savepoint(sql =>
    sql`
      insert into accounts (
        user_id
      ) values (
        ${ user.user_id }
      )
    `
  ).catch(err => {
    // Account could not be created. ROLLBACK SAVEPOINT is called because we caught the rejection.
  })) || []

  return [user, account]
})
.then(([user, account]) => {
  // great success - COMMIT succeeded
})
.catch(() => {
  // not so good - ROLLBACK was called
})

ERR_POSTGRES_UNSAFE_TRANSACTION error is throw if BEGIN command is used without .begin(), max: 1 or .reserve().

Like postgres package if returned an array of Promises on .begin or .savepoint it will Promise.all all promises before COMMIT.

 await sql.begin(async sql => [
  sql`insert into test values(1)`,
  sql`insert into test values(2)`,
  sql`insert into test values(3)`
]);

sql.transaction is a alias for sql.begin

RESERVE await sql.reserve()

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.

// compatible with `postgres` 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`
}

Calling .reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package).

DISTRIBUTED TRANSACTIONS await sql.beginDistributed(name, fn) -> fn()

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.

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");

sql.distributed is a alias for sql.beginDistributed

Connection Pool

No connection will be made until a query is made.

const sql = Bun.sql() // no connection are created

await sql`...` // pool is started until max is reached (if possible), first available connection is used
await sql`...` // previous connection is reused

// two connections are used now at the same time
await Promise.all([
  sql`...`,
  sql`...`
]);

await sql.close(); // close all connection from the pool no new queries can be made.

If a connection is closed or not able to connect it will only retry it if no other connection is currently available to be used, this enables Bun to run even if not all connections are able to established minimizing downtimes. Bun will open as many connections until max number of connections is reached. By default max is 10. This can be changed by setting max in the Bun.sql() call. Example - Bun.sql({ max: 20 }).

Differences from postgres package:

1 - Error code UNSAFE_TRANSACTION is ERR_POSTGRES_UNSAFE_TRANSACTION

2 - postgres package will accept 0 in max value, we will throw a error, the default value will be the same 10, postgres will use 0 with means hanging when trying to do a query.

3 - postgres is conservative and will lazy open each connection only if enough concurrency is achieved, until max is reached, Bun.sql try to keep the pool full (until max is reached) when possible after the first query is called or when retries are needed to maximize throughput.

4 - Features present in postgres and not in Bun.sql:

  • sql.readable()
  • sql.writable()
  • sql.cursor
  • sql.file
  • sql.simple
  • sql.listen
  • sql.notify
  • sql.subscribe
  • sql.unsafe
  • sql.prepare in transactions see sql.beginDistributed
  • options.types
  • options.transform

@robobun
Copy link

robobun commented Jan 13, 2025

Updated 4:36 PM PT - Jan 18th, 2025

@cirospaciari, your commit 66dd28e has passed in #10069! 🎉


🧪   try this PR locally:

bunx bun-pr 16381

@cirospaciari cirospaciari force-pushed the ciro/postgres-transactions-cleanup branch from 130076c to ff50783 Compare January 14, 2025 01:18
@cirospaciari cirospaciari changed the title wip(sql) transactions, savepoints and connection pooling feat(sql) transactions, savepoints and connection pooling Jan 14, 2025
@cirospaciari cirospaciari marked this pull request as ready for review January 14, 2025 03:07
@cirospaciari cirospaciari force-pushed the ciro/postgres-transactions-cleanup branch from 0ea2f06 to 7b88aec Compare January 14, 2025 03:07
@cirospaciari cirospaciari marked this pull request as draft January 14, 2025 03:15
@cirospaciari cirospaciari changed the title feat(sql) transactions, savepoints and connection pooling feat(sql) transactions, savepoints, connection pooling and reserve Jan 14, 2025
@cirospaciari cirospaciari force-pushed the ciro/postgres-transactions-cleanup branch 7 times, most recently from 872492b to 71a5e32 Compare January 17, 2025 23:22
@cirospaciari cirospaciari force-pushed the ciro/postgres-transactions-cleanup branch from 9eb2d9c to a06f824 Compare January 18, 2025 02:16
@cirospaciari cirospaciari marked this pull request as ready for review January 18, 2025 02:16
src/js/bun/sql.ts Outdated Show resolved Hide resolved
src/js/bun/sql.ts Outdated Show resolved Hide resolved
src/js/bun/sql.ts Outdated Show resolved Hide resolved
src/js/bun/sql.ts Outdated Show resolved Hide resolved
class PooledConnection {
pool: ConnectionPool;
connection: ReturnType<typeof createConnection>;
state: PooledConnectionState = PooledConnectionState.pending;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically the flags and the connection state can be the same property since the connection state is 2 bits and the flags are 3 bits and we can go up to 32 bit int

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but its probably fine

@Jarred-Sumner Jarred-Sumner merged commit ba930ad into main Jan 19, 2025
34 of 44 checks passed
@Jarred-Sumner Jarred-Sumner deleted the ciro/postgres-transactions-cleanup branch January 19, 2025 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants