Skip to content

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

Merged
Jarred-Sumner merged 35 commits into
mainfrom
ciro/postgres-transactions-cleanup
Jan 19, 2025
Merged

Jarred-Sumner merged 35 commits into
mainfrom
ciro/postgres-transactions-cleanup

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jan 13, 2025 •

Copy link
Copy Markdown
Member

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

robobun commented Jan 13, 2025 •

Copy link
Copy Markdown
Collaborator
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
Comment thread src/js/bun/sql.ts Outdated
Comment thread src/js/bun/sql.ts Outdated
Comment thread src/js/bun/sql.ts Outdated
Comment thread src/js/bun/sql.ts
Comment thread src/js/bun/sql.ts Outdated
Comment thread src/js/bun/sql.ts
class PooledConnection {
pool: ConnectionPool;
connection: ReturnType<typeof createConnection>;
state: PooledConnectionState = PooledConnectionState.pending;

Copy link
Copy Markdown
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
Copy Markdown
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

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