Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 9 additions & 4 deletions test/docker/Dockerfile.mysql-native-password
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
FROM mysql:8.0
FROM mysql:8.4

# See Dockerfile.mysql-plain for the rationale. This variant pins mysql:8.0 and
# See Dockerfile.mysql-plain for the rationale. This variant pins mysql:8.4 and
# the mysql_native_password auth plugin to cover the legacy-auth code path.
# mysql_native_password was removed in MySQL 9.x, so 8.4 is the last LTS that
# ships it; 8.4 disables it by default and removed
# --default-authentication-plugin, so enable it and set it as the default
# first-factor via --authentication-policy instead.
ENV MYSQL_ROOT_PASSWORD=bun \
MYSQL_DATABASE=bun_sql_test \
MYSQL_ROOT_HOST=% \
Expand All @@ -10,7 +14,8 @@ ENV MYSQL_ROOT_PASSWORD=bun \
RUN set -e; \
docker-entrypoint.sh mysqld \
--datadir=/var/lib/mysql-init \
--default-authentication-plugin=mysql_native_password & pid=$!; \
--mysql-native-password=ON \
--authentication-policy=mysql_native_password,, & pid=$!; \
for i in $(seq 180); do \
mysql -h127.0.0.1 -uroot -pbun -e 'SELECT 1' >/dev/null 2>&1 && break; \
sleep 1; \
Expand All @@ -19,4 +24,4 @@ RUN set -e; \
mysqladmin -h127.0.0.1 -uroot -pbun shutdown; \
wait "$pid"

CMD ["mysqld", "--datadir=/var/lib/mysql-init", "--default-authentication-plugin=mysql_native_password"]
CMD ["mysqld", "--datadir=/var/lib/mysql-init", "--mysql-native-password=ON", "--authentication-policy=mysql_native_password,,"]
2 changes: 1 addition & 1 deletion test/docker/Dockerfile.mysql-plain
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mysql:8.4
FROM mysql:9

# Bake a fully initialized data directory into the image so cold container
# start skips `mysqld --initialize` and the temp-server dance entirely and is
Expand Down
4 changes: 2 additions & 2 deletions test/docker/Dockerfile.postgres-auth
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM postgres:15
FROM postgres:18
COPY init-scripts/postgres-auth/ /docker-entrypoint-initdb.d/
COPY config/pg_hba_auth.conf /etc/postgresql/pg_hba.conf

Expand All @@ -7,7 +7,7 @@ COPY config/pg_hba_auth.conf /etc/postgresql/pg_hba.conf
# build-time init only needs the default local-trust hba to run its scripts.
ENV POSTGRES_HOST_AUTH_METHOD=trust \
POSTGRES_USER=postgres \
PGDATA=/var/lib/postgresql/pgdata-init
PGDATA=/var/lib/pgdata-init

RUN set -e; \
docker-entrypoint.sh postgres & pid=$!; \
Expand Down
12 changes: 6 additions & 6 deletions test/docker/Dockerfile.postgres-plain
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
FROM postgres:15
FROM postgres:18
Comment thread
robobun marked this conversation as resolved.
COPY init-scripts/postgres/ /docker-entrypoint-initdb.d/

# Bake a fully initialized data directory into the image so cold container
# start skips initdb + the temp-server init-script pass and is just
# `exec postgres` (~1-2s instead of ~5-8s). The base image declares
# `VOLUME /var/lib/postgresql/data`, which discards build-time writes there,
# so point PGDATA at a sibling path; the entrypoint reads PGDATA and sees it
# is already populated at runtime, so it goes straight to exec.
# `exec postgres` (~1-2s instead of ~5-8s). The postgres:18 base image declares
# `VOLUME /var/lib/postgresql`, which discards build-time writes under that
# path, so point PGDATA at a sibling outside it; the entrypoint reads PGDATA and
# sees it is already populated at runtime, so it goes straight to exec.
ENV POSTGRES_HOST_AUTH_METHOD=trust \
POSTGRES_USER=postgres \
PGDATA=/var/lib/postgresql/pgdata-init
PGDATA=/var/lib/pgdata-init

RUN set -e; \
docker-entrypoint.sh postgres & pid=$!; \
Expand Down
2 changes: 1 addition & 1 deletion test/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ Some services use volumes to persist data across container restarts:

Control behavior with environment variables:
- `COMPOSE_PROJECT_NAME`: Prefix for container names (default: "bun-test-services")
- `BUN_DOCKER_COMPOSE_PATH`: Override docker-compose.yml location
- `BUN_DOCKER_COMPOSE_FILE`: Override docker-compose.yml location

## Migration Guide

Expand Down
6 changes: 4 additions & 2 deletions test/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ services:
start_period: 60s
start_interval: 1s

# mysql_native_password was removed in MySQL 9.x, so this service stays on the
# 8.4 LTS (the last line that ships the plugin) to cover the legacy-auth code path.
mysql_native_password:
build:
context: .
Expand All @@ -121,7 +123,7 @@ services:
context: ../js/sql/mysql-tls
dockerfile: Dockerfile
args:
MYSQL_VERSION: 8.4
MYSQL_VERSION: "9"
image: bun-mysql-tls:local
ports:
- target: 3306
Expand All @@ -137,7 +139,7 @@ services:

# Redis/Valkey Services
redis_plain:
image: redis:7-alpine
image: redis:8-alpine
command: redis-server --bind 0.0.0.0 --protected-mode no
ports:
- target: 6379
Expand Down
32 changes: 3 additions & 29 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1088,19 +1088,7 @@ export async function waitForPort(port: number, timeout: number = 60_000): Promi

export async function describeWithContainer(
label: string,
{
image,
env = {},
args = [],
archs,
concurrent = false,
}: {
image: string;
env?: Record<string, string>;
args?: string[];
archs?: NodeJS.Architecture[];
concurrent?: boolean;
},
{ image, concurrent = false }: { image: string; concurrent?: boolean },
fn: (container: { port: number; host: string; ready: Promise<void> }) => void,
) {
// Check if this is one of our docker-compose services
Expand All @@ -1111,8 +1099,6 @@ export async function describeWithContainer(
"mysql_plain": 3306,
"mysql_native_password": 3306,
"mysql_tls": 3306,
"mysql:8": 3306, // Map mysql:8 to mysql_plain
"mysql:9": 3306, // Map mysql:9 to mysql_native_password
"redis_plain": 6379,
"redis_unified": 6379,
"minio": 9000,
Expand All @@ -1130,22 +1116,10 @@ export async function describeWithContainer(
return;
}

// Map mysql:8 and mysql:9 based on environment variables
let actualService = image;
if (image === "mysql:8" || image === "mysql:9") {
if (env.MYSQL_ROOT_PASSWORD === "bun") {
actualService = "mysql_native_password"; // Has password "bun"
} else if (env.MYSQL_ALLOW_EMPTY_PASSWORD === "yes") {
actualService = "mysql_plain"; // No password
} else {
actualService = "mysql_plain"; // Default to no password
}
}

// Skip only when no env override, no coordinator, and docker is unavailable.
// isDockerEnabled() may throw when docker is required but absent, so the
// env-override and coordinator checks must short-circuit before it.
if (!process.env["BUN_TEST_SERVICE_" + actualService] && !process.env.BUN_DOCKER_COORDINATOR && !isDockerEnabled()) {
if (!process.env["BUN_TEST_SERVICE_" + image] && !process.env.BUN_DOCKER_COORDINATOR && !isDockerEnabled()) {
Comment thread
robobun marked this conversation as resolved.
describe.todo(label);
return;
}
Expand Down Expand Up @@ -1179,7 +1153,7 @@ export async function describeWithContainer(
// up() de-duplicates in-flight calls per service, so two describes for
// the same service share one `compose up`. beforeAll just awaits the
// result so test failures still surface there.
const startPromise = import("./docker/index.ts").then(h => h.ensure(actualService as any));
const startPromise = import("./docker/index.ts").then(h => h.ensure(image as any));
// Surface any rejection through `ready`; without a handler the runner
// would see an unhandled rejection before beforeAll re-throws it.
startPromise.catch(readyRejecter!);
Expand Down
15 changes: 4 additions & 11 deletions test/integration/mysql2/mysql2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,23 @@ const tests: {
label: string;
database: {
image: string;
env?: Record<string, string>;
};
client: ConnectionOptions;
}[] = [
{
label: "mysql:8 with root user and password",
label: "mysql_native_password with root user and password",
database: {
image: "mysql:8",
env: {
MYSQL_ROOT_PASSWORD: "bun",
},
image: "mysql_native_password",
},
client: {
user: "root",
password: "bun",
},
},
{
label: "mysql:8 with root user and empty password",
label: "mysql_plain with root user and empty password",
database: {
image: "mysql:8",
env: {
MYSQL_ALLOW_EMPTY_PASSWORD: "yes",
},
image: "mysql_plain",
},
client: {
user: "root",
Expand Down
4 changes: 2 additions & 2 deletions test/js/sql/docker-tls/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Dockerfile
FROM postgres:15.13
FROM postgres:18

# Create directory for SSL certificates
RUN mkdir -p /etc/postgresql/ssl
Expand Down Expand Up @@ -77,7 +77,7 @@ RUN mkdir -p /docker-entrypoint-initdb.d && \
# Set environment variables
ENV POSTGRES_HOST_AUTH_METHOD=trust \
POSTGRES_USER=postgres \
PGDATA=/var/lib/postgresql/pgdata-init
PGDATA=/var/lib/pgdata-init

# Bake a fully initialized data directory into the image so cold container
# start is just `exec postgres` (~1-2s). See test/docker/Dockerfile.postgres-plain
Expand Down
2 changes: 1 addition & 1 deletion test/js/sql/mysql-tls/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG MYSQL_VERSION=8.4
ARG MYSQL_VERSION=9
FROM mysql:${MYSQL_VERSION}

# Bake a fully initialized data directory into the image so cold container
Expand Down
2 changes: 0 additions & 2 deletions test/js/sql/sql-mysql.auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ describeWithContainer(
"mysql",
{
image: "mysql_native_password",
env: {},
args: [],
concurrent: true,
},
container => {
Expand Down
2 changes: 0 additions & 2 deletions test/js/sql/sql-mysql.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ describeWithContainer(
"mysql",
{
image: "mysql_plain",
env: {},
concurrent: true,
args: [],
},
container => {
// Use a getter to avoid reading port/host at define time
Expand Down
17 changes: 6 additions & 11 deletions test/js/sql/sql-mysql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,28 @@ async function assertComputedDecimalsAreStrings(sql: SQL) {
}
if (isDockerEnabled()) {
// Ordered so the suites whose containers become healthy quickly (mysql_plain,
// mysql:9) run first; the slow-to-start mysql_tls container warms up in the
// background instead of stalling the whole file up front.
// mysql_native_password) run first; the slow-to-start mysql_tls container
// warms up in the background instead of stalling the whole file up front.
const images = [
{
name: "MySQL",
image: "mysql_plain",
},
// This image only works on x64.
process.arch === "x64" && {
name: "MySQL 9",
image: "mysql:9",
env: {
MYSQL_ROOT_PASSWORD: "bun",
},
{
name: "MySQL with mysql_native_password",
image: "mysql_native_password",
},
{
name: "MySQL with TLS",
image: "mysql_tls",
},
].filter(Boolean);
];

for (const image of images) {
describeWithContainer(
image.name,
{
image: image.image,
env: image.env,
concurrent: true,
},
container => {
Expand Down
27 changes: 14 additions & 13 deletions test/js/sql/sql-mysql.transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ describeWithContainer(
"mysql",
{
image: "mysql_plain",
env: {},
args: [],
},
container => {
// Use a getter to avoid reading port/host at define time
Expand Down Expand Up @@ -55,14 +53,16 @@ describeWithContainer(
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.message),
).toBe("Incorrect integer value: 'hej' for column 'a' at row 1");
const err = 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 is the stable wire-protocol error number (1366 = ER_TRUNCATED_WRONG_VALUE_FOR_FIELD);
// the message prose is the server's and varies across MySQL versions and MariaDB.
expect({ code: err.code, errno: err.errno }).toEqual({ code: "ERR_MYSQL_SERVER_ERROR", errno: 1366 });
expect(err.message).toContain("Incorrect integer value: 'hej' for column");
});

test("Transaction rolls back", async () => {
Expand Down Expand Up @@ -151,9 +151,10 @@ describeWithContainer(

test("Uncaught transaction request errors bubbles to transaction", async () => {
await using sql = new SQL(getOptions());
expect(await sql.begin(sql => [sql`select wat`, sql`select 1 as x, ${1} as a`]).catch(e => e.message)).toBe(
"Unknown column 'wat' in 'field list'",
);
const err = await sql.begin(sql => [sql`select wat`, sql`select 1 as x, ${1} as a`]).catch(e => e);
// 1054 = ER_BAD_FIELD_ERROR; see the note on errno vs. message prose in "Transaction throws".
expect({ code: err.code, errno: err.errno }).toEqual({ code: "ERR_MYSQL_SERVER_ERROR", errno: 1054 });
expect(err.message).toContain("Unknown column 'wat'");
});

test("Transaction rejects with rethrown error", async () => {
Expand Down
13 changes: 6 additions & 7 deletions test/js/sql/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5745,13 +5745,12 @@ CREATE TABLE ${table_name} (
E'\\n', -- newline
E'\\r', -- carriage return
E'\\t', -- tab
E'\\v', -- vertical tab (not in postgres.js)
E'\\x0b', -- vertical tab (E'\\v' only became an escape in PostgreSQL 18)
E'\\\\', -- backslash
E'\"' -- quote
]::text[] as special_escapes
`;
// vertical tab will be just "v"
expect(result[0].special_escapes).toEqual(["\b", "\f", "\n", "\r", "\t", "v", "\\", '"']);
expect(result[0].special_escapes).toEqual(["\b", "\f", "\n", "\r", "\t", "\v", "\\", '"']);
});

test("text[] - octal escape sequences", async () => {
Expand Down Expand Up @@ -11370,16 +11369,16 @@ CREATE TABLE ${table_name} (

test("pg_database[] - null values", async () => {
await using sql = postgres({ ...options, max: 1 });
// Source the record from the live server: pg_database gains columns across
// PostgreSQL majors, so a hardcoded record literal only fits one major.
const result = await sql`
SELECT ARRAY[
NULL,
'(5,postgres,10,6,c,f,t,-1,716,1,1663,C,C,,,)'::pg_database,
(SELECT d FROM pg_database d WHERE datname = 'postgres'),
NULL
]::pg_database[] as array_with_nulls
`;
expect(result[0].array_with_nulls[0]).toBeNull();
expect(result[0].array_with_nulls[1]).toBe("(5,postgres,10,6,c,f,t,-1,716,1,1663,C,C,,,)");
expect(result[0].array_with_nulls[2]).toBeNull();
expect(result[0].array_with_nulls).toEqual([null, expect.stringContaining(",postgres,"), null]);
});

test("pg_database[] - null array", async () => {
Expand Down
Loading
Loading