Compare commits

...

7 Commits

Author SHA1 Message Date
Ciro Spaciari
190a5030ae wip 2025-01-28 18:57:24 -08:00
Ciro Spaciari
55e74b1dab test should be valid 2025-01-28 18:05:09 -08:00
Ciro Spaciari
2bf14621cf deflaky 2025-01-28 17:53:53 -08:00
Ciro Spaciari
2c3755646d add test 2025-01-28 17:53:53 -08:00
Ciro Spaciari
80e658c953 wip 2025-01-28 17:53:53 -08:00
Ciro Spaciari
31e57102eb wip 2025-01-28 17:53:53 -08:00
Ciro Spaciari
39d9e87d00 WIP 2025-01-28 17:53:53 -08:00
8 changed files with 592 additions and 109 deletions

View File

@@ -240,15 +240,23 @@ pub const PostgresSQLQuery = struct {
}
pub const Status = enum(u8) {
/// The query was just enqueued, statement status can be checked for more details
pending,
written,
running,
/// The query is being bound to the statement
binding,
/// The query is running
running,
/// The query was successful
success,
/// The query failed
fail,
pub fn isRunning(this: Status) bool {
return this == .running or this == .binding;
return @intFromEnum(this) > @intFromEnum(Status.pending) and @intFromEnum(this) < @intFromEnum(Status.success);
}
pub fn isDone(this: Status) bool {
return this == .success or this == .fail;
}
};
@@ -606,57 +614,60 @@ pub const PostgresSQLQuery = struct {
};
const has_params = signature.fields.len > 0;
var did_write = false;
var reset_timeout = false;
enqueue: {
if (entry.found_existing) {
this.statement = entry.value_ptr.*;
this.statement.?.ref();
signature.deinit();
if (has_params and this.statement.?.status == .parsing) {
// if it has params, we need to wait for ParamDescription to be received before we can write the data
} else {
if (@intFromEnum(this.statement.?.status) == @intFromEnum(PostgresSQLStatement.Status.prepared) and !connection.isCurrentRunning()) {
this.flags.binary = this.statement.?.fields.len > 0;
log("bindAndExecute", .{});
// bindAndExecute will bind + execute, it will change to running after binding is complete
this.status = .binding;
PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, columns_value, PostgresSQLConnection.Writer, writer) catch |err| {
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to bind and execute query");
return error.JSError;
};
did_write = true;
reset_timeout = true;
}
break :enqueue;
}
// If it does not have params, we can write and execute immediately in one go
if (!has_params) {
log("prepareAndQueryWithSignature", .{});
const can_execute = !connection.isCurrentRunning();
PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to prepare and query");
return error.JSError;
};
did_write = true;
} else {
log("writeQuery", .{});
PostgresRequest.writeQuery(query_str.slice(), signature.prepared_statement_name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to write query");
return error.JSError;
};
writer.write(&protocol.Sync) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to flush");
return error.JSError;
};
if (can_execute) {
// If it does not have params, we can write and execute immediately in one go
if (!has_params) {
log("prepareAndQueryWithSignature", .{});
// prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete
this.status = .binding;
PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to prepare and query");
return error.JSError;
};
reset_timeout = true;
} else {
log("writeQuery", .{});
PostgresRequest.writeQuery(query_str.slice(), signature.prepared_statement_name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to write query");
return error.JSError;
};
writer.write(&protocol.Sync) catch |err| {
signature.deinit();
if (!globalObject.hasException())
return globalObject.throwError(err, "failed to flush");
return error.JSError;
};
reset_timeout = true;
}
}
{
@@ -664,7 +675,7 @@ pub const PostgresSQLQuery = struct {
return globalObject.throwError(err, "failed to allocate statement");
};
connection.prepared_statement_id += 1;
stmt.* = .{ .signature = signature, .ref_count = 2, .status = PostgresSQLStatement.Status.parsing };
stmt.* = .{ .signature = signature, .ref_count = 2, .status = if (can_execute) .parsing else .pending };
this.statement = stmt;
entry.value_ptr.* = stmt;
}
@@ -672,12 +683,11 @@ pub const PostgresSQLQuery = struct {
connection.requests.writeItem(this) catch {};
this.ref();
this.status = if (did_write) .binding else .pending;
PostgresSQLQuery.targetSetCached(this_value, globalObject, query);
if (connection.is_ready_for_query)
connection.flushDataAndResetTimeout()
else if (did_write)
else if (reset_timeout)
connection.resetConnectionTimeout();
return .undefined;
@@ -1508,37 +1518,37 @@ pub const PostgresSQLConnection = struct {
pub fn onHandshake(this: *PostgresSQLConnection, success: i32, ssl_error: uws.us_bun_verify_error_t) void {
debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });
const handshake_success = if (success == 1) true else false;
if (handshake_success) {
if (this.tls_config.reject_unauthorized != 0) {
// only reject the connection if reject_unauthorized == true
switch (this.ssl_mode) {
// https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279
if (this.tls_config.reject_unauthorized == 0) {
return;
}
.verify_ca, .verify_full => {
if (ssl_error.error_no != 0) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
return;
}
const do_tls_verification = switch (this.ssl_mode) {
// https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279
.verify_ca, .verify_full => true,
else => false,
};
if (!do_tls_verification) {
return;
}
if (success != 1) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
return;
}
if (ssl_error.error_no != 0) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
return;
}
const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(this.socket.getNativeHandle()));
if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| {
const hostname = servername[0..bun.len(servername)];
if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(this.socket.getNativeHandle()));
if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| {
const hostname = servername[0..bun.len(servername)];
if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
}
}
this.failWithJSValue(ssl_error.toJS(this.globalObject));
},
else => {
return;
},
}
}
} else {
// if we are here is because server rejected us, and the error_no is the cause of this
// no matter if reject_unauthorized is false because we are disconnected by the server
this.failWithJSValue(ssl_error.toJS(this.globalObject));
}
}
@@ -1707,8 +1717,10 @@ pub const PostgresSQLConnection = struct {
return .zero;
}
if (tls_config.reject_unauthorized != 0)
tls_config.request_cert = 1;
// we always request the cert so we can verify it and also we manually abort the connection if the hostname doesn't match
const original_reject_unauthorized = tls_config.reject_unauthorized;
tls_config.reject_unauthorized = 0;
tls_config.request_cert = 1;
// We create it right here so we can throw errors early.
const context_options = tls_config.asUSockets();
@@ -1720,6 +1732,8 @@ pub const PostgresSQLConnection = struct {
return globalObject.throwValue(err.toJS(globalObject));
}
};
// restore the original reject_unauthorized
tls_config.reject_unauthorized = original_reject_unauthorized;
if (err != .none) {
tls_config.deinit();
@@ -1969,6 +1983,15 @@ pub const PostgresSQLConnection = struct {
return this.requests.peekItem(0);
}
fn isCurrentRunning(this: *PostgresSQLConnection) bool {
if (this.current()) |query| {
if (query.statement) |stmt| {
return query.status.isRunning() or @intFromEnum(stmt.status) > @intFromEnum(PostgresSQLStatement.Status.pending);
}
}
return false;
}
pub const Writer = struct {
connection: *PostgresSQLConnection,
@@ -2453,55 +2476,94 @@ pub const PostgresSQLConnection = struct {
fn advance(this: *PostgresSQLConnection) !bool {
defer this.updateRef();
var any = false;
defer if (any) this.resetConnectionTimeout();
while (this.requests.readableLength() > 0) {
var req: *PostgresSQLQuery = this.requests.peekItem(0);
switch (req.status) {
.pending => {
const stmt = req.statement orelse return error.ExpectedStatement;
if (stmt.status == .failed) {
req.onError(stmt.error_response, this.globalObject);
this.requests.discard(1);
any = true;
} else {
break;
switch (stmt.status) {
.failed => {
req.onError(stmt.error_response, this.globalObject);
this.requests.discard(1);
any = true;
continue;
},
.prepared => {
const binding_value = PostgresSQLQuery.bindingGetCached(req.thisValue) orelse .zero;
const columns_value = PostgresSQLQuery.columnsGetCached(req.thisValue) orelse .zero;
req.flags.binary = stmt.fields.len > 0;
PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| {
req.onWriteFail(err, this.globalObject, this.getQueriesArray());
req.deref();
this.requests.discard(1);
continue;
};
req.status = .binding;
any = true;
return any;
},
.pending => {
// statement is pending, lets write/parse it
var query_str = req.query.toUTF8(bun.default_allocator);
defer query_str.deinit();
stmt.status = .parsing;
const has_params = stmt.signature.fields.len > 0;
// If it does not have params, we can write and execute immediately in one go
if (!has_params) {
// prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete
req.status = .binding;
const binding_value = PostgresSQLQuery.bindingGetCached(req.thisValue) orelse .zero;
PostgresRequest.prepareAndQueryWithSignature(this.globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, this.writer(), &stmt.signature) catch |err| {
req.onWriteFail(err, this.globalObject, this.getQueriesArray());
req.deref();
this.requests.discard(1);
continue;
};
any = true;
return any;
}
const connection_writer = this.writer();
// write query and wait for it to be prepared
PostgresRequest.writeQuery(query_str.slice(), stmt.signature.prepared_statement_name, stmt.signature.fields, PostgresSQLConnection.Writer, connection_writer) catch |err| {
req.onWriteFail(err, this.globalObject, this.getQueriesArray());
req.deref();
this.requests.discard(1);
continue;
};
connection_writer.write(&protocol.Sync) catch |err| {
req.onWriteFail(err, this.globalObject, this.getQueriesArray());
req.deref();
this.requests.discard(1);
continue;
};
any = true;
return any;
},
.parsing => {
// we are still parsing, lets wait for it to be prepared or failed
return any;
},
}
},
.running, .binding => {
// if we are binding it will switch to running immediately
// if we are running, we need to wait for it to be success or fail
return any;
},
.success, .fail => {
this.requests.discard(1);
req.deref();
this.requests.discard(1);
any = true;
continue;
},
else => break,
}
}
while (this.requests.readableLength() > 0) {
var req: *PostgresSQLQuery = this.requests.peekItem(0);
const stmt = req.statement orelse return error.ExpectedStatement;
switch (stmt.status) {
.prepared => {
if (req.status == .pending and stmt.status == .prepared) {
const binding_value = PostgresSQLQuery.bindingGetCached(req.thisValue) orelse .zero;
const columns_value = PostgresSQLQuery.columnsGetCached(req.thisValue) orelse .zero;
PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| {
req.onWriteFail(err, this.globalObject, this.getQueriesArray());
req.deref();
this.requests.discard(1);
continue;
};
req.status = .binding;
req.flags.binary = stmt.fields.len > 0;
any = true;
} else {
break;
}
},
else => break,
}
}
return any;
}
@@ -2973,12 +3035,13 @@ pub const PostgresSQLStatement = struct {
fields: []protocol.FieldDescription = &[_]protocol.FieldDescription{},
parameters: []const int4 = &[_]int4{},
signature: Signature,
status: Status = Status.parsing,
status: Status = Status.pending,
error_response: protocol.ErrorResponse = .{},
needs_duplicate_check: bool = true,
fields_flags: PostgresSQLConnection.DataCell.Flags = .{},
pub const Status = enum {
pending,
parsing,
prepared,
failed,

View File

@@ -0,0 +1,82 @@
# Dockerfile
FROM postgres:15
# Create directory for SSL certificates
RUN mkdir -p /etc/postgresql/ssl
# Copy existing certificates
COPY server.key server.crt /etc/postgresql/ssl/
RUN chmod 600 /etc/postgresql/ssl/server.key && \
chown postgres:postgres /etc/postgresql/ssl/server.key /etc/postgresql/ssl/server.crt
# Create initialization script
RUN echo '#!/bin/bash\n\
set -e\n\
\n\
# Wait for PostgreSQL to start\n\
until pg_isready; do\n\
echo "Waiting for PostgreSQL to start..."\n\
sleep 1\n\
done\n\
\n\
dropdb --if-exists bun_sql_test\n\
\n\
# Drop and recreate users with different auth methods\n\
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL\n\
DROP USER IF EXISTS bun_sql_test;\n\
CREATE USER bun_sql_test;\n\
\n\
ALTER SYSTEM SET password_encryption = '"'"'md5'"'"';\n\
SELECT pg_reload_conf();\n\
DROP USER IF EXISTS bun_sql_test_md5;\n\
CREATE USER bun_sql_test_md5 WITH PASSWORD '"'"'bun_sql_test_md5'"'"';\n\
\n\
ALTER SYSTEM SET password_encryption = '"'"'scram-sha-256'"'"';\n\
SELECT pg_reload_conf();\n\
DROP USER IF EXISTS bun_sql_test_scram;\n\
CREATE USER bun_sql_test_scram WITH PASSWORD '"'"'bun_sql_test_scram'"'"';\n\
EOSQL\n\
\n\
# Create database and set permissions\n\
createdb bun_sql_test\n\
\n\
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL\n\
GRANT ALL ON DATABASE bun_sql_test TO bun_sql_test;\n\
ALTER DATABASE bun_sql_test OWNER TO bun_sql_test;\n\
EOSQL\n\
' > /docker-entrypoint-initdb.d/init-users-db.sh
# Make the script executable
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 "local all postgres trust" >> /etc/postgresql/pg_hba.conf && \
echo "local all bun_sql_test trust" >> /etc/postgresql/pg_hba.conf && \
echo "local all bun_sql_test_md5 md5" >> /etc/postgresql/pg_hba.conf && \
echo "local all bun_sql_test_scram scram-sha-256" >> /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 "local replication all trust" >> /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
# Configure PostgreSQL for SSL
RUN mkdir -p /docker-entrypoint-initdb.d && \
echo "ALTER SYSTEM SET max_prepared_transactions = '100';" > /docker-entrypoint-initdb.d/configure-postgres.sql && \
echo "ALTER SYSTEM SET ssl = 'on';" >> /docker-entrypoint-initdb.d/configure-postgres.sql && \
echo "ALTER SYSTEM SET ssl_cert_file = '/etc/postgresql/ssl/server.crt';" >> /docker-entrypoint-initdb.d/configure-postgres.sql && \
echo "ALTER SYSTEM SET ssl_key_file = '/etc/postgresql/ssl/server.key';" >> /docker-entrypoint-initdb.d/configure-postgres.sql
# Set environment variables
ENV POSTGRES_HOST_AUTH_METHOD=trust
ENV POSTGRES_USER=postgres
# Expose PostgreSQL port
EXPOSE 5432

View File

@@ -0,0 +1,98 @@
# PostgreSQL Client Authentication Configuration File
# ===================================================
#
# Refer to the "Client Authentication" section in the PostgreSQL
# documentation for a complete description of this file. A short
# synopsis follows.
#
# This file controls: which hosts are allowed to connect, how clients
# are authenticated, which PostgreSQL user names they can use, which
# databases they can access. Records take one of these forms:
#
# local DATABASE USER METHOD [OPTIONS]
# host DATABASE USER ADDRESS METHOD [OPTIONS]
# hostssl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS]
# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS]
# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS]
#
# (The uppercase items must be replaced by actual values.)
#
# The first field is the connection type:
# - "local" is a Unix-domain socket
# - "host" is a TCP/IP socket (encrypted or not)
# - "hostssl" is a TCP/IP socket that is SSL-encrypted
# - "hostnossl" is a TCP/IP socket that is not SSL-encrypted
# - "hostgssenc" is a TCP/IP socket that is GSSAPI-encrypted
# - "hostnogssenc" is a TCP/IP socket that is not GSSAPI-encrypted
#
# DATABASE can be "all", "sameuser", "samerole", "replication", a
# database name, or a comma-separated list thereof. The "all"
# keyword does not match "replication". Access to replication
# must be enabled in a separate record (see example below).
#
# USER can be "all", a user name, a group name prefixed with "+", or a
# comma-separated list thereof. In both the DATABASE and USER fields
# you can also write a file name prefixed with "@" to include names
# from a separate file.
#
# ADDRESS specifies the set of hosts the record matches. It can be a
# host name, or it is made up of an IP address and a CIDR mask that is
# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that
# specifies the number of significant bits in the mask. A host name
# that starts with a dot (.) matches a suffix of the actual host name.
# Alternatively, you can write an IP address and netmask in separate
# columns to specify the set of hosts. Instead of a CIDR-address, you
# can write "samehost" to match any of the server's own IP addresses,
# or "samenet" to match any address in any subnet that the server is
# directly connected to.
#
# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
# Note that "password" sends passwords in clear text; "md5" or
# "scram-sha-256" are preferred since they send encrypted passwords.
#
# OPTIONS are a set of options for the authentication in the format
# NAME=VALUE. The available options depend on the different
# authentication methods -- refer to the "Client Authentication"
# section in the documentation for a list of which options are
# available for which authentication methods.
#
# Database and user names containing spaces, commas, quotes and other
# special characters must be quoted. Quoting one of the keywords
# "all", "sameuser", "samerole" or "replication" makes the name lose
# its special character, and just match a database or username with
# that name.
#
# This file is read on server startup and when the server receives a
# SIGHUP signal. If you edit the file on a running system, you have to
# SIGHUP the server for the changes to take effect, run "pg_ctl reload",
# or execute "SELECT pg_reload_conf()".
#
# Put your actual configuration here
# ----------------------------------
#
# If you want to allow non-local connections, you need to add more
# "host" records. In that case you will also need to make PostgreSQL
# listen on a non-local interface via the listen_addresses
# configuration parameter, or via the -i or -h command line switches.
# CAUTION: Configuring the system for local "trust" authentication
# allows any local user to connect as any PostgreSQL user, including
# the database superuser. If you do not trust all your local users,
# use another authentication method.
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all trust
# IPv4 local connections:
host all all 127.0.0.1/32 trust
# IPv6 local connections:
host all all ::1/128 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication all trust
host replication all 127.0.0.1/32 trust
host replication all ::1/128 trust

View File

@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
AWDbBPL8WN2F2/Q0QBxC2BECKSVxhg==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,125 @@
import { sql, SQL } from "bun";
const postgres = (...args) => new sql(...args);
import { expect, test, mock, beforeAll, afterAll } from "bun:test";
import { $ } from "bun";
import { bunExe, isCI, withoutAggressiveGC, isLinux } from "harness";
import path from "path";
import { exec, execSync } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
import net from "net";
const dockerCLI = Bun.which("docker") as string;
async function findRandomPort() {
return new Promise((resolve, reject) => {
// Create a server to listen on a random port
const server = net.createServer();
server.listen(0, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function waitForPostgres(port) {
for (let i = 0; i < 3; i++) {
try {
const sql = new SQL(`postgres://bun_sql_test@localhost:${port}/bun_sql_test`, {
idle_timeout: 20,
max_lifetime: 60 * 30,
tls: {
ca: Bun.file(path.join(import.meta.dir, "docker-tls", "server.crt")),
},
});
await sql`SELECT 1`;
await sql.end();
console.log("PostgreSQL is ready!");
return true;
} catch (error) {
console.log(`Waiting for PostgreSQL... (${i + 1}/3)`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
throw new Error("PostgreSQL failed to start");
}
async function startContainer(): Promise<{ port: number; containerName: string }> {
try {
// Build the Docker image
console.log("Building Docker image...");
const dockerfilePath = path.join(import.meta.dir, "docker-tls", "Dockerfile");
await execAsync(`${dockerCLI} build --pull --rm -f "${dockerfilePath}" -t custom-postgres-tls .`, {
cwd: path.join(import.meta.dir, "docker-tls"),
});
const port = await findRandomPort();
const containerName = `postgres-test-${port}`;
// Check if container exists and remove it
try {
await execAsync(`${dockerCLI} rm -f ${containerName}`);
} catch (error) {
// Container might not exist, ignore error
}
// Start the container
await execAsync(`${dockerCLI} run -d --name ${containerName} -p ${port}:5432 custom-postgres-tls`);
// Wait for PostgreSQL to be ready
await waitForPostgres(port);
return {
port,
containerName,
};
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
function isDockerEnabled(): boolean {
if (!dockerCLI) {
return false;
}
// TODO: investigate why its not starting on Linux arm64
if (isLinux && process.arch === "arm64") {
return false;
}
try {
const info = execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] });
return info.toString().indexOf("Server Version:") !== -1;
} catch {
return false;
}
}
if (isDockerEnabled()) {
const container: { port: number; containerName: string } = await startContainer();
afterAll(async () => {
try {
await execAsync(`${dockerCLI} stop -t 0 ${container.containerName}`);
await execAsync(`${dockerCLI} rm -f ${container.containerName}`);
} catch (error) {}
});
process.env.DATABASE_URL = `postgres://bun_sql_test@localhost:${container.port}/bun_sql_test`;
test.only("Connects with no options", async () => {
// we need at least the usename and port
await using sql = postgres({
max: 1,
idle_timeout: 1,
connection_timeout: 1,
// sslmode: "verify-full",
tls: {
ca: Bun.file(path.join(import.meta.dir, "docker-tls", "server.crt")),
},
});
const result = (await sql`select 1 as x`)[0].x;
sql.close();
expect(result).toBe(1);
});
}

View File

@@ -61,9 +61,7 @@ async function startContainer(): Promise<{ port: number; containerName: string }
}
// Start the container
await execAsync(
`${dockerCLI} run -d --name ${containerName} -p ${port}:5432 custom-postgres`,
);
await execAsync(`${dockerCLI} run -d --name ${containerName} -p ${port}:5432 custom-postgres`);
// Wait for PostgreSQL to be ready
await waitForPostgres(port);
@@ -491,6 +489,38 @@ if (isDockerEnabled()) {
return expect(error.code).toBe("ERR_POSTGRES_UNSAFE_TRANSACTION");
});
test("should be able to execute different queries in the same connection #16774", async () => {
const sql = postgres({ ...options, max: 1, fetch_types: false });
const random_table_name = `test_user_${Math.random().toString(36).substring(2, 15)}`;
await sql`CREATE TEMPORARY TABLE IF NOT EXISTS ${sql(random_table_name)} (id int, name text)`;
const promises: Array<Promise<any>> = [];
// POPULATE TABLE
for (let i = 0; i < 1_000; i++) {
promises.push(sql`insert into ${sql(random_table_name)} values (${i}, ${`test${i}`})`.execute());
}
await Promise.all(promises);
// QUERY TABLE using execute() to force executing the query immediately
{
for (let i = 0; i < 1_000; i++) {
// mix different parameters
switch (i % 3) {
case 0:
promises.push(sql`select "id", "name" from ${sql(random_table_name)} where "id" = ${i}`.execute());
break;
case 1:
promises.push(sql`select "id" from ${sql(random_table_name)} where "id" = ${i}`.execute());
break;
case 2:
promises.push(sql`select 1, "id", "name" from ${sql(random_table_name)} where "id" = ${i}`.execute());
break;
}
}
await Promise.all(promises);
}
});
test("Transaction throws", async () => {
await sql`create table if not exists test (a int)`;
try {

View File

@@ -167,7 +167,7 @@ if (TLS_POSTGRES_DATABASE_URL) {
await using sql = new SQL({ ...options, debug: true, idle_timeout: 1, fetch_types: false, max: 10 });
expect(
await sql
.begin(sql => [sql`select wat`, sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.begin(sql => [sql`select wat`.execute(), sql`select current_setting('bun_sql.test') as x, ${1} as a`])
.catch(e => e.errno),
).toBe("42703");
});