diff --git a/src/js/bun/sql.ts b/src/js/bun/sql.ts index e16faf010d..2f98380180 100644 --- a/src/js/bun/sql.ts +++ b/src/js/bun/sql.ts @@ -1295,7 +1295,6 @@ function doCreateQuery(strings, values, allowUnsafeTransaction, poolSize, bigint } } } - return createQuery(sqlString, final_values, new SQLResultArray(), undefined, !!bigint, !!simple); } diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 5e89dc5126..be62e89f57 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -853,6 +853,7 @@ pub const PostgresSQLQuery = struct { connection.prepared_statement_id += 1; stmt.* = .{ .signature = signature, .ref_count = 2, .status = if (can_execute) .parsing else .pending }; this.statement = stmt; + entry_value.* = stmt; } else { stmt.* = .{ .signature = signature, .ref_count = 1, .status = if (can_execute) .parsing else .pending }; @@ -956,9 +957,21 @@ pub const PostgresRequest = struct { iter.to(0); var i: usize = 0; while (iter.next()) |value| : (i += 1) { - const parameter_field = parameter_fields[i]; - const is_custom_type = std.math.maxInt(short) < parameter_field; - const tag: types.Tag = if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); + const tag: types.Tag = brk: { + if (i >= len) { + // parameter in array but not in parameter_fields + // this is probably a bug a bug in bun lets return .text here so the server will send a error 08P01 + // with will describe better the error saying exactly how many parameters are missing and are expected + // Example: + // SQL error: PostgresError: bind message supplies 0 parameters, but prepared statement "PSELECT * FROM test_table WHERE id=$1 .in$0" requires 1 + // errno: "08P01", + // code: "ERR_POSTGRES_SERVER_ERROR" + break :brk .text; + } + const parameter_field = parameter_fields[i]; + const is_custom_type = std.math.maxInt(short) < parameter_field; + break :brk if (is_custom_type) .text else @enumFromInt(@as(short, @intCast(parameter_field))); + }; if (value.isEmptyOrUndefinedOrNull()) { debug(" -> NULL", .{}); // As a special case, -1 indicates a @@ -3598,7 +3611,8 @@ pub const PostgresSQLConnection = struct { try reader.eatMessage(protocol.ParseComplete); const request = this.current() orelse return error.ExpectedRequest; if (request.statement) |statement| { - if (statement.status == .parsing) { + // if we have params wait for parameter description + if (statement.status == .parsing and statement.signature.fields.len == 0) { statement.status = .prepared; } } @@ -3609,6 +3623,9 @@ pub const PostgresSQLConnection = struct { const request = this.current() orelse return error.ExpectedRequest; var statement = request.statement orelse return error.ExpectedStatement; statement.parameters = description.parameters; + if (statement.status == .parsing) { + statement.status = .prepared; + } }, .RowDescription => { var description: protocol.RowDescription = undefined; diff --git a/test/bun.lock b/test/bun.lock index 978fa00516..cdf45ffb59 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -6,7 +6,7 @@ "dependencies": { "@azure/service-bus": "7.9.4", "@duckdb/node-api": "1.1.3-alpha.7", - "@electric-sql/pglite": "0.2.16", + "@electric-sql/pglite": "0.2.17", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", "@happy-dom/global-registrator": "17.0.3", @@ -52,6 +52,7 @@ "nodemailer": "6.9.3", "pg": "8.11.1", "pg-connection-string": "2.6.1", + "pg-gateway": "^0.3.0-beta.4", "pino": "9.4.0", "pino-pretty": "11.2.2", "postgres": "3.3.5", @@ -171,7 +172,7 @@ "@duckdb/node-bindings-win32-x64": ["@duckdb/node-bindings-win32-x64@1.1.3-alpha.7", "", { "os": "win32", "cpu": "x64" }, "sha512-5OqjpYRFdQATbniL0o8gF8Z92bBuINlXOve0o+qgM6W+nlIRp/cUHk6vWwYySZnF0AIHZ5JG7mngzJOLmA/kPQ=="], - "@electric-sql/pglite": ["@electric-sql/pglite@0.2.16", "", {}, "sha512-dCSHpoOKuTxecaYhWDRp2yFTN3XWcMPMrBVl5yOR8VZEUprz4+R3iuU7BipmlsqBnBDO/6l9H/C2ZwJdunkWyw=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.2.17", "", {}, "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw=="], "@emnapi/runtime": ["@emnapi/runtime@0.44.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw=="], @@ -1665,6 +1666,8 @@ "pg-connection-string": ["pg-connection-string@2.6.1", "", {}, "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg=="], + "pg-gateway": ["pg-gateway@0.3.0-beta.4", "", {}, "sha512-CTjsM7Z+0Nx2/dyZ6r8zRsc3f9FScoD5UAOlfUx1Fdv/JOIWvRbF7gou6l6vP+uypXQVoYPgw8xZDXgMGvBa4Q=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], "pg-pool": ["pg-pool@3.6.2", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg=="], diff --git a/test/js/third_party/pg-gateway/package.json b/test/js/third_party/pg-gateway/package.json new file mode 100644 index 0000000000..63aa8b1474 --- /dev/null +++ b/test/js/third_party/pg-gateway/package.json @@ -0,0 +1,7 @@ +{ + "name": "pg-gateway", + "dependencies": { + "@electric-sql/pglite": "0.2.17", + "pg-gateway": "0.3.0-beta.4" + } +} diff --git a/test/js/third_party/pg-gateway/pglite.test.ts b/test/js/third_party/pg-gateway/pglite.test.ts new file mode 100644 index 0000000000..a7dfc2f732 --- /dev/null +++ b/test/js/third_party/pg-gateway/pglite.test.ts @@ -0,0 +1,90 @@ +import { PGlite } from "@electric-sql/pglite"; +import { SQL, randomUUIDv7 } from "bun"; +import net, { AddressInfo } from "node:net"; +import { fromNodeSocket } from "pg-gateway/node"; +import { expect, test } from "bun:test"; +import { once } from "events"; +test("pglite should be able to query using pg-gateway and Bun.SQL", async () => { + const name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + const dataDir = `memory://${name}`; + const db = new PGlite(dataDir); + + // Wait for the database to initialize + await db.waitReady; + + // Create a simple test table + await db.exec(` + CREATE TABLE IF NOT EXISTS test_table ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL + ); + + INSERT INTO test_table (name) VALUES ('Test 1'), ('Test 2'), ('Test 3'); + `); + + // Create a simple server using pg-gateway + const server = net.createServer(async socket => { + await fromNodeSocket(socket, { + serverVersion: "16.3", + auth: { + method: "trust", + }, + async onStartup() { + // Wait for PGlite to be ready before further processing + await db.waitReady; + }, + async onMessage(data, { isAuthenticated }: { isAuthenticated: boolean }) { + // Only forward messages to PGlite after authentication + if (!isAuthenticated) { + return; + } + + return await db.execProtocolRaw(data); + }, + }); + }); + + // Start listening + await once(server.listen(0), "listening"); + + const port = (server.address() as AddressInfo).port; + + await using sql = new SQL({ + hostname: "localhost", + port: port, + database: name, + max: 1, + }); + + { + // prepared statement without parameters + const result = await sql`SELECT * FROM test_table WHERE id = 1`; + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect(result[0]).toEqual({ id: 1, name: "Test 1" }); + } + + { + // using prepared statement + const result = await sql`SELECT * FROM test_table WHERE id = ${1}`; + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect(result[0]).toEqual({ id: 1, name: "Test 1" }); + } + + { + // using simple query + const result = await sql`SELECT * FROM test_table WHERE id = 1`.simple(); + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect(result[0]).toEqual({ id: 1, name: "Test 1" }); + } + + { + // using unsafe with parameters + const result = await sql.unsafe("SELECT * FROM test_table WHERE id = $1", [1]); + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect(result[0]).toEqual({ id: 1, name: "Test 1" }); + } +}); diff --git a/test/package.json b/test/package.json index 55ffcfb3c9..0b19467222 100644 --- a/test/package.json +++ b/test/package.json @@ -11,7 +11,7 @@ "dependencies": { "@azure/service-bus": "7.9.4", "@duckdb/node-api": "1.1.3-alpha.7", - "@electric-sql/pglite": "0.2.16", + "@electric-sql/pglite": "0.2.17", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", "@happy-dom/global-registrator": "17.0.3", @@ -57,6 +57,7 @@ "nodemailer": "6.9.3", "pg": "8.11.1", "pg-connection-string": "2.6.1", + "pg-gateway": "0.3.0-beta.4", "pino": "9.4.0", "pino-pretty": "11.2.2", "postgres": "3.3.5",