Compare commits

...

5 Commits

Author SHA1 Message Date
Dylan Conway
e44246951d fix(spawn): prevent use-after-free in subprocess stdin cleanup
When onProcessExit runs after the JSSink destructor has already freed
the FileSink, the fromJS recovery path would read stale m_sinkPtr from
the dead JSSink, accessing freed/reallocated memory. This could close
file descriptors belonging to other subprocesses, corrupting their
pipe communication.

Guard the fromJS recovery with has_stdin_destructor_called to skip it
when we know the FileSink has already been freed.
2026-01-28 14:52:00 -08:00
Dylan Conway
7ebfdf97a8 fix(npm): remove shebang from placeholder scripts to fix npm i -g bun on Windows (#26517)
## Summary
- Removes the `#!/bin/sh` shebang from placeholder `bin/bun.exe` and
`bin/bunx.exe` scripts in the npm package
- Fixes `npm i -g bun` being completely broken on Windows since v1.3.7

## Problem
PR #26259 added a `#!/bin/sh` shebang to the placeholder scripts to show
a helpful error when postinstall hasn't run. However, npm's `cmd-shim`
reads shebangs to generate `.ps1`/`.cmd` wrappers **before** postinstall
runs, and bakes the interpreter path into them. On Windows, the wrappers
referenced `/bin/sh` which doesn't exist, causing:

```
& "/bin/sh$exe"  "$basedir/node_modules/bun/bin/bun.exe" $args
   ~~~~~~~~~~~~~
The term '/bin/sh.exe' is not recognized...
```

Even after postinstall successfully replaced the placeholder with the
real binary, the stale wrappers still tried to invoke `/bin/sh`.

## Fix
Remove the shebang. Without it, `cmd-shim` generates a direct invocation
wrapper that works after postinstall replaces the placeholder. On Unix,
bash/zsh still execute shebang-less files as shell scripts via ENOEXEC
fallback, so the helpful error message is preserved.

## Test plan
- [x] `bun bd test test/regression/issue/24329.test.ts` passes (2/2
tests)
- Manually verify `npm i -g bun` works on Windows

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 00:00:50 -08:00
SUZUKI Sosuke
4cd3b241bc Upgrade WebKit to cc5e0bddf7ea (#26526)
Upgrade WebKit from `0e6527f24783ea83` to `cc5e0bddf7eae1d8` (77
commits)

This brings in the latest changes from oven-sh/WebKit (2026-01-27).
2026-01-27 23:33:47 -08:00
robobun
cae67a17e2 chore: update mimalloc to latest dev3 (#26519)
## Summary
- Updates oven-sh/mimalloc bun-dev3 branch to latest upstream
microsoft/mimalloc dev3 (ffa38ab8)
- Merged 12 new commits from upstream

### Key upstream changes included:
- fix re-initialization of threads on macOS
- add lock for sub-pagemap allocations
- fix peak commit stat
- fix use of continue in bitmap find_and_clear (fixes rare case of not
finding space while it exists)

## Test plan
- [ ] CI passes
- [ ] Memory allocation tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:33:00 -08:00
robobun
a394063a7d refactor(test): use container-based postgres_tls for TLS SQL tests (#26518)
## Summary
- Refactors `tls-sql.test.ts` to use `describeWithContainer` with a
local Docker container instead of external Neon secrets
- Updates `postgres_tls` service to build from Dockerfile (fixes SSL key
permission issues)
- Fixes pg_hba.conf to allow local socket connections for init scripts

## Test plan
- [x] Verified tests pass locally with `bun bd test
test/js/sql/tls-sql.test.ts` (30 tests pass)
- [ ] CI passes on x64 Linux (arm64 Docker tests are currently disabled)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:32:39 -08:00
9 changed files with 258 additions and 290 deletions

View File

@@ -4,7 +4,7 @@ register_repository(
REPOSITORY
oven-sh/mimalloc
COMMIT
989115cefb6915baa13788cb8252d83aac5330ad
ffa38ab8ac914f9eb7af75c1f8ad457643dc14f2
)
set(MIMALLOC_CMAKE_ARGS

View File

@@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use")
option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading")
if(NOT WEBKIT_VERSION)
set(WEBKIT_VERSION 0e6527f24783ea832fa58f696437829cdcbc3c7c)
set(WEBKIT_VERSION cc5e0bddf7eae1d820cf673158845fe9bd83c094)
endif()
# Use preview build URL for Windows ARM64 until the fix is merged to main

View File

@@ -73,9 +73,11 @@ async function buildRootModule(dryRun?: boolean) {
});
// Create placeholder scripts that print an error message if postinstall hasn't run.
// On Unix, these are executed as shell scripts despite the .exe extension.
// On Windows, npm creates .cmd wrappers that would fail anyway if the binary isn't valid.
const placeholderScript = `#!/bin/sh
echo "Error: Bun's postinstall script was not run." >&2
// Do NOT add a shebang (#!/bin/sh) here — npm's cmd-shim reads shebangs to generate
// .ps1/.cmd wrappers BEFORE postinstall runs, and bakes the interpreter path in.
// A #!/bin/sh shebang breaks Windows because the wrappers reference /bin/sh which
// doesn't exist, even after postinstall replaces the placeholder with the real binary.
const placeholderScript = `echo "Error: Bun's postinstall script was not run." >&2
echo "" >&2
echo "This occurs when using --ignore-scripts during installation, or when using a" >&2
echo "package manager like pnpm that does not run postinstall scripts by default." >&2

View File

@@ -591,7 +591,7 @@ pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Sta
if (this_jsvalue != .zero) {
if (jsc.Codegen.JSSubprocess.stdinGetCached(this_jsvalue)) |existing_value| {
if (existing_value.isCell()) {
if (stdin == null) {
if (stdin == null and !this.flags.has_stdin_destructor_called) {
// TODO: review this cast
stdin = @ptrCast(@alignCast(jsc.WebCore.FileSink.JSSink.fromJS(existing_value)));
}

View File

@@ -21,20 +21,17 @@ services:
start_period: 5s
postgres_tls:
image: postgres:15
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_USER: postgres
volumes:
- ./init-scripts/postgres:/docker-entrypoint-initdb.d:ro
- ../js/sql/docker-tls/server.crt:/etc/postgresql/ssl/server.crt:ro
- ../js/sql/docker-tls/server.key:/etc/postgresql/ssl/server.key:ro
build:
context: ../js/sql/docker-tls
dockerfile: Dockerfile
image: bun-postgres-tls:local
ports:
- target: 5432
published: 0
protocol: tcp
command: >
postgres
-c hba_file=/etc/postgresql/pg_hba.conf
-c ssl=on
-c ssl_cert_file=/etc/postgresql/ssl/server.crt
-c ssl_key_file=/etc/postgresql/ssl/server.key
@@ -47,7 +44,8 @@ services:
interval: 1h # Effectively disable after startup
timeout: 5s
retries: 30
start_period: 5s
start_period: 60s
start_interval: 1s
postgres_auth:
image: postgres:15

View File

@@ -109,8 +109,8 @@ class DockerComposeHelper {
return;
}
// Build the service if needed (for services like mysql_tls that need building)
if (service === "mysql_tls" || service === "redis_unified") {
// Build the service if needed (for services like mysql_tls, postgres_tls that need building)
if (service === "mysql_tls" || service === "redis_unified" || service === "postgres_tls") {
const buildResult = await this.exec(["build", service]);
if (buildResult.exitCode !== 0) {
throw new Error(`Failed to build service ${service}: ${buildResult.stderr}`);

View File

@@ -51,16 +51,20 @@ RUN chmod +x /docker-entrypoint-initdb.d/init-users-db.sh
# Create pg_hba.conf with SSL requirements
RUN mkdir -p /etc/postgresql && touch /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 127.0.0.1/32 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram 127.0.0.1/32 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 ::1/128 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram ::1/128 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all 127.0.0.1/32 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all ::1/128 trust" >> /etc/postgresql/pg_hba.conf && \
echo "# Allow local socket connections for init scripts" >> /etc/postgresql/pg_hba.conf && \
echo "local all postgres trust" >> /etc/postgresql/pg_hba.conf && \
echo "local all all trust" >> /etc/postgresql/pg_hba.conf && \
echo "# Remote TLS connections" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 0.0.0.0/0 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram 0.0.0.0/0 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all postgres ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_md5 ::/0 md5" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl all bun_sql_test_scram ::/0 scram-sha-256" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all 0.0.0.0/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "hostssl replication all ::/0 trust" >> /etc/postgresql/pg_hba.conf && \
echo "host all all all reject" >> /etc/postgresql/pg_hba.conf
# Configure PostgreSQL for SSL

View File

@@ -1,280 +1,241 @@
import { postgres, randomUUIDv7, SQL, sql } from "bun";
import { SQL, randomUUIDv7 } from "bun";
import { describe, expect, test } from "bun:test";
import { getSecret } from "harness";
import { describeWithContainer, isDockerEnabled } from "harness";
const TLS_POSTGRES_DATABASE_URL = getSecret("TLS_POSTGRES_DATABASE_URL");
const PG_TRANSACTION_POOL_SUPABASE_URL = getSecret("PG_TRANSACTION_POOL_SUPABASE_URL");
for (const options of [
{
url: TLS_POSTGRES_DATABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: true,
transactionPool: false,
},
{
url: PG_TRANSACTION_POOL_SUPABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: false,
transactionPool: true,
},
{
url: TLS_POSTGRES_DATABASE_URL,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare: false,
transactionPool: false,
},
] satisfies (Bun.SQL.Options & { transactionPool?: boolean })[]) {
if (options.url === undefined) {
console.log("SKIPPING TEST", JSON.stringify(options), "BECAUSE MISSING THE URL SECRET");
continue;
}
describe.concurrent(
`${options.transactionPool ? "Transaction Pooling" : `Prepared Statements (${options.prepare ? "on" : "off"})`}`,
() => {
test("default sql", async () => {
expect(sql.reserve).toBeDefined();
expect(sql.options).toBeDefined();
expect(sql[Symbol.asyncDispose]).toBeDefined();
expect(sql.begin).toBeDefined();
expect(sql.beginDistributed).toBeDefined();
expect(sql.distributed).toBeDefined();
expect(sql.unsafe).toBeDefined();
expect(sql.end).toBeDefined();
expect(sql.close).toBeDefined();
expect(sql.transaction).toBeDefined();
expect(sql.distributed).toBeDefined();
expect(sql.unsafe).toBeDefined();
expect(sql.commitDistributed).toBeDefined();
expect(sql.rollbackDistributed).toBeDefined();
});
test("default postgres", async () => {
expect(postgres.reserve).toBeDefined();
expect(postgres.options).toBeDefined();
expect(postgres[Symbol.asyncDispose]).toBeDefined();
expect(postgres.begin).toBeDefined();
expect(postgres.beginDistributed).toBeDefined();
expect(postgres.distributed).toBeDefined();
expect(postgres.unsafe).toBeDefined();
expect(postgres.end).toBeDefined();
expect(postgres.close).toBeDefined();
expect(postgres.transaction).toBeDefined();
expect(postgres.distributed).toBeDefined();
expect(postgres.unsafe).toBeDefined();
expect(postgres.commitDistributed).toBeDefined();
expect(postgres.rollbackDistributed).toBeDefined();
});
test("tls (explicit)", async () => {
await using sql = new SQL(options);
const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`;
expect(one).toBe(1);
expect(two).toBe("2");
await sql.close();
});
test("Throws on illegal transactions", async () => {
await using sql = new SQL({ ...options, max: 2 });
const error = await sql`BEGIN`.catch(e => e);
expect(error).toBeInstanceOf(SQL.SQLError);
expect(error).toBeInstanceOf(SQL.PostgresError);
return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
});
test.skipIf(options.transactionPool)("Transaction throws", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(e => e.errno),
).toBe("22P02");
});
test.skipIf(options.transactionPool)("Transaction rolls back", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(() => {
/* ignore */
if (!isDockerEnabled()) {
test.skip("skipping TLS SQL tests - Docker is not available", () => {});
} else {
describeWithContainer(
"PostgreSQL TLS",
{
image: "postgres_tls",
},
container => {
// Test with prepared statements on and off
for (const prepare of [true, false]) {
describe(`prepared: ${prepare}`, () => {
const getOptions = (): Bun.SQL.Options => ({
url: `postgres://postgres@${container.host}:${container.port}/bun_sql_test`,
tls: true,
adapter: "postgres",
max: 1,
bigint: true,
prepare,
});
expect((await sql`select a from ${sql(random_name)}`).count).toBe(0);
});
test("tls (explicit)", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const [{ one, two }] = await sql`SELECT 1 as one, '2' as two`;
expect(one).toBe(1);
expect(two).toBe("2");
});
test.skipIf(options.transactionPool)("Transaction throws on uncaught savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(err => err.message),
).toBe("fail");
});
test("Throws on illegal transactions", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const error = await sql`BEGIN`.catch(e => e);
expect(error).toBeInstanceOf(SQL.SQLError);
expect(error).toBeInstanceOf(SQL.PostgresError);
return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
});
test.skipIf(options.transactionPool)("Transaction throws on uncaught named savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint("watpoint", async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(() => "fail"),
).toBe("fail");
});
test("Transaction throws", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(e => e.errno),
).toBe("22P02");
});
test("Transaction rolls back", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
test("Transaction succeeds on caught savepoint", async () => {
await using sql = new SQL(options);
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
try {
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql`insert into ${sql(random_name)} values('hej')`;
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
expect((await sql`select a from ${sql(random_name)}`).count).toBe(0);
});
expect((await sql`select count(1) from ${sql(random_name)}`)[0].count).toBe(2n);
} finally {
await sql`DROP TABLE IF EXISTS ${sql(random_name)}`;
}
});
test("Savepoint returns Result", async () => {
let result;
await using sql = new SQL(options);
await sql.begin(async t => {
result = await t.savepoint(s => s`select 1 as x`);
});
expect(result[0]?.x).toBe(1);
});
test("Transaction throws on uncaught savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(err => err.message),
).toBe("fail");
});
test("Transaction requests are executed implicitly", async () => {
await using sql = new SQL(options);
expect(
(
await sql.begin(sql => [
sql`select set_config('bun_sql.test', 'testing', true)`,
sql`select current_setting('bun_sql.test') as x`,
])
)[1][0].x,
).toBe("testing");
});
test("Transaction throws on uncaught named savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
expect(
await sql
.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql.savepoint("watpoint", async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("fail");
});
})
.catch(e => e.message),
).toBe("fail");
});
test("Uncaught transaction request errors bubbles to transaction", async () => {
await using sql = new SQL(options);
expect(
await sql
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.catch(e => e.errno),
).toBe("42703");
});
test("Transaction rejects with rethrown error", async () => {
await using sql = new SQL(options);
expect(
await sql
.begin(async sql => {
try {
await sql`select exception`;
} catch (ex) {
throw new Error("WAT");
}
})
.catch(e => e.message),
).toBe("WAT");
});
test("Parallel transactions", async () => {
await using sql = new SQL({ ...options, max: 2 });
expect(
(await Promise.all([sql.begin(sql => sql`select 1 as count`), sql.begin(sql => sql`select 1 as count`)]))
.map(x => x[0].count)
.join(""),
).toBe("11");
});
test("Many transactions at beginning of connection", async () => {
await using sql = new SQL({ ...options, max: 2 });
const xs = await Promise.all(Array.from({ length: 30 }, () => sql.begin(sql => sql`select 1`)));
return expect(xs.length).toBe(30);
});
test("Transactions array", async () => {
await using sql = new SQL(options);
expect(
(await sql.begin(sql => [sql`select 1 as count`, sql`select 1 as count`])).map(x => x[0].count).join(""),
).toBe("11");
});
test.skipIf(options.transactionPool)("Transaction waits", async () => {
await using sql = new SQL({ ...options, max: 2 });
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
test("Transaction succeeds on caught savepoint", async () => {
await container.ready;
await using sql = new SQL(getOptions());
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
});
await sql`insert into ${sql(random_name)} values(3)`;
expect((await sql`select count(1) from ${sql(random_name)}`)[0].count).toBe(2n);
});
test("Savepoint returns Result", async () => {
await container.ready;
let result;
await using sql = new SQL(getOptions());
await sql.begin(async t => {
result = await t.savepoint(s => s`select 1 as x`);
});
expect(result[0]?.x).toBe(1);
});
test("Transaction requests are executed implicitly", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
(
await sql.begin(sql => [
sql`select set_config('bun_sql.test', 'testing', true)`,
sql`select current_setting('bun_sql.test') as x`,
])
)[1][0].x,
).toBe("testing");
});
test("Uncaught transaction request errors bubbles to transaction", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
await sql
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.catch(e => e.errno),
).toBe("42703");
});
test("Transaction rejects with rethrown error", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
await sql
.begin(async sql => {
try {
await sql`select exception`;
} catch (ex) {
throw new Error("WAT");
}
})
.catch(e => e.message),
).toBe("WAT");
});
test("Parallel transactions", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
expect(
(await Promise.all([sql.begin(sql => sql`select 1 as count`), sql.begin(sql => sql`select 1 as count`)]))
.map(x => x[0].count)
.join(""),
).toBe("11");
});
test("Many transactions at beginning of connection", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const xs = await Promise.all(Array.from({ length: 30 }, () => sql.begin(sql => sql`select 1`)));
return expect(xs.length).toBe(30);
});
test("Transactions array", async () => {
await container.ready;
await using sql = new SQL(getOptions());
expect(
(await sql.begin(sql => [sql`select 1 as count`, sql`select 1 as count`])).map(x => x[0].count).join(""),
).toBe("11");
});
test("Transaction waits", async () => {
await container.ready;
await using sql = new SQL({ ...getOptions(), max: 2 });
const random_name = ("t_" + randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_name)} (a int)`;
await sql.begin(async sql => {
await sql`insert into ${sql(random_name)} values(1)`;
await sql
.savepoint(async sql => {
await sql`insert into ${sql(random_name)} values(2)`;
throw new Error("please rollback");
})
.catch(() => {
/* ignore */
});
await sql`insert into ${sql(random_name)} values(3)`;
});
expect(
(
await Promise.all([
sql.begin(async sql => await sql`select 1 as count`),
sql.begin(async sql => await sql`select 1 as count`),
])
)
.map(x => x[0].count)
.join(""),
).toBe("11");
});
});
expect(
(
await Promise.all([
sql.begin(async sql => await sql`select 1 as count`),
sql.begin(async sql => await sql`select 1 as count`),
])
)
.map(x => x[0].count)
.join(""),
).toBe("11");
});
}
},
);
}

View File

@@ -13,8 +13,9 @@ test("bun npm placeholder script should exit with error if postinstall hasn't ru
// This is the placeholder script content that gets written to bin/bun.exe
// during npm package build (see packages/bun-release/scripts/upload-npm.ts)
const placeholderScript = `#!/bin/sh
echo "Error: Bun's postinstall script was not run." >&2
// Note: no shebang — a #!/bin/sh shebang breaks Windows because npm's cmd-shim
// bakes the interpreter path into .ps1/.cmd wrappers before postinstall runs.
const placeholderScript = `echo "Error: Bun's postinstall script was not run." >&2
echo "" >&2
echo "This occurs when using --ignore-scripts during installation, or when using a" >&2
echo "package manager like pnpm that does not run postinstall scripts by default." >&2
@@ -38,9 +39,11 @@ exit 1
});
expect(chmodExitCode).toBe(0);
// Run the placeholder script
// Run via sh explicitly — in real usage, bash/zsh automatically fall back to sh
// interpretation when execve returns ENOEXEC for a shebang-less executable file.
// Bun.spawn doesn't have that fallback, so we invoke sh directly here.
await using proc = Bun.spawn({
cmd: ["./bun-placeholder", "--version"],
cmd: ["sh", "./bun-placeholder"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",