diff --git a/README.md b/README.md
index 23c535d..07e4e5d 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ quarkus.datasource.jdbc.driver=io.roastedroot.pglite4j.jdbc.PgLiteDriver
quarkus.datasource.username=postgres
quarkus.datasource.password=password
quarkus.datasource.jdbc.min-size=1
-quarkus.datasource.jdbc.max-size=1
+quarkus.datasource.jdbc.max-size=5
quarkus.devservices.enabled=false
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false
@@ -82,18 +82,18 @@ spring.datasource.url=jdbc:pglite:memory://
spring.datasource.driver-class-name=io.roastedroot.pglite4j.jdbc.PgLiteDriver
spring.datasource.username=postgres
spring.datasource.password=password
-spring.datasource.hikari.maximum-pool-size=1
+spring.datasource.hikari.maximum-pool-size=5
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false
```
-### HikariCP - NOT TESTED
+### HikariCP
```java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:pglite:memory://");
-config.setMaximumPoolSize(1);
+config.setMaximumPoolSize(5);
DataSource ds = new HikariDataSource(config);
```
@@ -109,12 +109,14 @@ pglite4j/
## Status and known limitations
-- [ ] **Only `memory://` is supported** — no persistent / file-backed databases yet
-- [ ] **Single connection only** — PGlite is single-threaded; connection pool max size must be 1
+- [x] ~~**Only `memory://` is supported**~~ — persistent / file-backed databases are not planned; the WASM backend uses an in-memory virtual filesystem (ZeroFS) with no disk I/O, which is fundamental to the architecture
+- [x] ~~**Single connection only**~~ — multiple JDBC connections are now supported per database instance; requests are serialized through a single PGLite backend via a lock, so connection pools with `max-size > 1` work correctly (queries execute one at a time, not in parallel)
+- [x] ~~**Error recovery**~~ — both simple and extended query protocol errors are handled correctly; PostgreSQL errors trap the WASM instance and are caught by the Java side, which resets the backend state and drains stale protocol buffers so subsequent queries work cleanly
+- [ ] **No connection isolation** — PostgreSQL runs in single-user mode with one session; all connections share the same session state (transactions, session variables). Queries are serialized, so there is no data corruption, but concurrent transactions are not isolated from each other. This is fine for connection pools that use connections sequentially (borrow, use, return).
+- [ ] **Server-side prepared statements disabled** — because all connections share a single PostgreSQL backend, named prepared statements (`S_1`, `S_2`, …) would collide across connections. The driver sets `prepareThreshold=0` so pgjdbc always uses the unnamed prepared statement. This has no functional impact but means PostgreSQL cannot cache query plans across executions.
- [ ] **Limited extensions** — only `plpgsql` and `dict_snowball` are bundled; adding more requires rebuilding the WASM binary
-- [ ] **Startup time** — first connection has some overhead it can be optimized more
+- [ ] **Startup time** — first connection has some overhead that can be optimized further
- [ ] **Binary size** — the WASM binary + pgdata resources add several MBs to the classpath
-- [ ] **Error recovery** — `clear_error()` integration for automatic transaction recovery is not yet wired up
### CMA (Contiguous Memory Allocator)
diff --git a/core/pom.xml b/core/pom.xml
index 31264c3..26ea990 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -71,8 +71,8 @@
io.roastedroot.pglite4j.core.PGLiteModule
../wasm-build/output/pglite.wasi
-
-
+ WARN
+
diff --git a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java
index b813d4f..103dd3b 100644
--- a/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java
+++ b/core/src/main/java/io/roastedroot/pglite4j/core/PGLite.java
@@ -95,7 +95,6 @@ private PGLite() {
int channel = exports.getChannel();
this.bufferAddr = exports.getBufferAddr(channel);
- // System.err.println("PGLite: channel=" + channel + " bufferAddr=" + bufferAddr);
} catch (IOException e) {
throw new RuntimeException("Failed to initialize PGLite", e);
}
@@ -115,7 +114,23 @@ public byte[] execProtocolRaw(byte[] message) {
for (int tick = 0; tick < 256; tick++) {
boolean producedBefore = collectReply(replies);
- exports.interactiveOne();
+ try {
+ exports.interactiveOne();
+ } catch (RuntimeException e) {
+ if (exports.pglCheckError() != 0) {
+ // PostgreSQL hit an ERROR (e.g. relation not found).
+ // pgl_on_error() set the WASM-side flag and the
+ // instance trapped via __builtin_unreachable().
+ // Recover: clean up PG error state and flush the
+ // ErrorResponse + ReadyForQuery back through the wire.
+ exports.clearError();
+ exports.interactiveWrite(-1);
+ exports.interactiveOne();
+ collectReply(replies);
+ break;
+ }
+ throw e;
+ }
boolean producedAfter = collectReply(replies);
if (!producedBefore && !producedAfter) {
break;
diff --git a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java
index 233d942..93c2727 100644
--- a/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java
+++ b/core/src/test/java/io/roastedroot/pglite4j/core/PGLiteTest.java
@@ -17,7 +17,6 @@ public void selectOne() {
assertTrue(result.length > 0);
String data = PgWireCodec.parseDataRows(result);
- System.out.println("SELECT 1 => " + data);
assertTrue(data.contains("1"));
}
}
@@ -27,11 +26,9 @@ public void handshake() {
try (PGLite pg = PGLite.builder().build()) {
doHandshake(pg);
- // After handshake, queries should work
byte[] result = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 42 AS answer;"));
assertNotNull(result);
String data = PgWireCodec.parseDataRows(result);
- System.out.println("After handshake: SELECT 42 => " + data);
assertTrue(data.contains("42"));
}
}
@@ -41,52 +38,62 @@ public void createTableAndInsert() {
try (PGLite pg = PGLite.builder().build()) {
doHandshake(pg);
- // DDL via simple query protocol
byte[] r1 =
pg.execProtocolRaw(
PgWireCodec.queryMessage("CREATE TABLE test (id INTEGER, name TEXT);"));
- System.out.println("CREATE TABLE: " + r1.length + " bytes");
+ assertNotNull(r1);
- // SERIAL column
byte[] r2 =
pg.execProtocolRaw(
PgWireCodec.queryMessage(
"CREATE TABLE test_serial (id SERIAL PRIMARY KEY, val TEXT);"));
- System.out.println("CREATE TABLE SERIAL: " + r2.length + " bytes");
+ assertNotNull(r2);
- // INSERT
byte[] r3 =
pg.execProtocolRaw(
PgWireCodec.queryMessage("INSERT INTO test VALUES (1, 'hello');"));
- System.out.println("INSERT: " + r3.length + " bytes");
+ assertNotNull(r3);
- // SELECT
byte[] r4 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT * FROM test;"));
String data = PgWireCodec.parseDataRows(r4);
- System.out.println("SELECT: " + data);
assertTrue(data.contains("hello"));
}
}
+ @Test
+ public void errorRecovery() {
+ try (PGLite pg = PGLite.builder().build()) {
+ doHandshake(pg);
+
+ byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;"));
+ assertNotNull(r1);
+ String data1 = PgWireCodec.parseDataRows(r1);
+ assertTrue(data1.contains("1"));
+
+ byte[] r2 =
+ pg.execProtocolRaw(
+ PgWireCodec.queryMessage("SELECT * FROM nonexistent_table_xyz;"));
+ assertNotNull(r2);
+ assertTrue(r2.length > 0, "Expected non-empty error response");
+
+ byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;"));
+ assertNotNull(r3);
+ String data3 = PgWireCodec.parseDataRows(r3);
+ assertTrue(data3.contains("2"), "Instance should be reusable after SQL error");
+ }
+ }
+
@Test
public void cmaBufferOverflow() {
try (PGLite pg = PGLite.builder().build()) {
doHandshake(pg);
int bufSize = pg.getBufferSize();
- System.out.println(
- "CMA buffer size: " + bufSize + " bytes (" + (bufSize / 1024) + " KB)");
-
- // Generate a wire protocol response that exceeds the CMA buffer.
- // repeat('x', N) returns an N-byte string in the DataRow message.
int repeatLen = bufSize + 1000;
String sql = "SELECT repeat('x', " + repeatLen + ");";
- System.out.println("Query: SELECT repeat('x', " + repeatLen + ")");
byte[] result = pg.execProtocolRaw(PgWireCodec.queryMessage(sql));
- System.out.println("Response length: " + result.length + " bytes");
assertNotNull(result);
- // The response must contain the full string + wire protocol overhead
assertTrue(
result.length > repeatLen,
"Expected response > " + repeatLen + " but got " + result.length);
@@ -94,14 +101,39 @@ public void cmaBufferOverflow() {
PgWireCodec.hasReadyForQuery(result),
"Expected ReadyForQuery in overflow response");
- // Verify a normal query still works after the overflow
byte[] r2 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 42;"));
String data = PgWireCodec.parseDataRows(r2);
- System.out.println("Post-overflow query: SELECT 42 => " + data);
assertTrue(data.contains("42"), "Normal query should work after CMA overflow");
}
}
+ @Test
+ public void extendedProtocolErrorRecovery() {
+ try (PGLite pg = PGLite.builder().build()) {
+ doHandshake(pg);
+
+ byte[] r1 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 1;"));
+ assertNotNull(r1);
+ String data1 = PgWireCodec.parseDataRows(r1);
+ assertTrue(data1.contains("1"));
+
+ byte[] batch = PgWireCodec.extendedQueryBatch("SELECT * FROM nonexistent_table_xyz");
+ byte[] r2 = pg.execProtocolRaw(batch);
+ assertNotNull(r2);
+ assertTrue(r2.length > 0, "Expected non-empty error response");
+ assertTrue(
+ PgWireCodec.hasReadyForQuery(r2),
+ "Expected ReadyForQuery after extended protocol error");
+
+ byte[] r3 = pg.execProtocolRaw(PgWireCodec.queryMessage("SELECT 2;"));
+ assertNotNull(r3);
+ String data3 = PgWireCodec.parseDataRows(r3);
+ assertTrue(
+ data3.contains("2"),
+ "Instance should be reusable after extended protocol error");
+ }
+ }
+
static void doHandshake(PGLite pg) {
byte[] startup = PgWireCodec.startupMessage("postgres", "template1");
byte[] resp1 = pg.execProtocolRaw(startup);
diff --git a/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java b/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java
index c64378d..60410f2 100644
--- a/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java
+++ b/core/src/test/java/io/roastedroot/pglite4j/core/PgWireCodec.java
@@ -185,6 +185,128 @@ static String parseDataRows(byte[] data) {
return sb.toString();
}
+ // === Extended Query Protocol ===
+
+ /** Build a Parse ('P') message. */
+ static byte[] parseMessage(String stmtName, String sql) {
+ byte[] nameBytes = (stmtName + "\0").getBytes(StandardCharsets.UTF_8);
+ byte[] sqlBytes = (sql + "\0").getBytes(StandardCharsets.UTF_8);
+ int bodyLen = nameBytes.length + sqlBytes.length + 2; // 2 for numParamTypes
+ byte[] msg = new byte[1 + 4 + bodyLen];
+ msg[0] = 'P';
+ int len = 4 + bodyLen;
+ msg[1] = (byte) (len >> 24);
+ msg[2] = (byte) (len >> 16);
+ msg[3] = (byte) (len >> 8);
+ msg[4] = (byte) len;
+ int pos = 5;
+ System.arraycopy(nameBytes, 0, msg, pos, nameBytes.length);
+ pos += nameBytes.length;
+ System.arraycopy(sqlBytes, 0, msg, pos, sqlBytes.length);
+ pos += sqlBytes.length;
+ msg[pos] = 0;
+ msg[pos + 1] = 0; // 0 parameter types
+ return msg;
+ }
+
+ /** Build a Bind ('B') message with no parameters. */
+ static byte[] bindMessage(String portal, String stmtName) {
+ byte[] portalBytes = (portal + "\0").getBytes(StandardCharsets.UTF_8);
+ byte[] nameBytes = (stmtName + "\0").getBytes(StandardCharsets.UTF_8);
+ int bodyLen = portalBytes.length + nameBytes.length + 2 + 2 + 2;
+ byte[] msg = new byte[1 + 4 + bodyLen];
+ msg[0] = 'B';
+ int len = 4 + bodyLen;
+ msg[1] = (byte) (len >> 24);
+ msg[2] = (byte) (len >> 16);
+ msg[3] = (byte) (len >> 8);
+ msg[4] = (byte) len;
+ int pos = 5;
+ System.arraycopy(portalBytes, 0, msg, pos, portalBytes.length);
+ pos += portalBytes.length;
+ System.arraycopy(nameBytes, 0, msg, pos, nameBytes.length);
+ pos += nameBytes.length;
+ // 0 format codes, 0 parameters, 0 result format codes
+ msg[pos] = 0;
+ msg[pos + 1] = 0;
+ pos += 2;
+ msg[pos] = 0;
+ msg[pos + 1] = 0;
+ pos += 2;
+ msg[pos] = 0;
+ msg[pos + 1] = 0;
+ return msg;
+ }
+
+ /** Build a Describe ('D') message. */
+ static byte[] describeMessage(char type, String name) {
+ byte[] nameBytes = (name + "\0").getBytes(StandardCharsets.UTF_8);
+ int bodyLen = 1 + nameBytes.length;
+ byte[] msg = new byte[1 + 4 + bodyLen];
+ msg[0] = 'D';
+ int len = 4 + bodyLen;
+ msg[1] = (byte) (len >> 24);
+ msg[2] = (byte) (len >> 16);
+ msg[3] = (byte) (len >> 8);
+ msg[4] = (byte) len;
+ msg[5] = (byte) type;
+ System.arraycopy(nameBytes, 0, msg, 6, nameBytes.length);
+ return msg;
+ }
+
+ /** Build an Execute ('E') message. */
+ static byte[] executeMessage(String portal, int maxRows) {
+ byte[] portalBytes = (portal + "\0").getBytes(StandardCharsets.UTF_8);
+ int bodyLen = portalBytes.length + 4;
+ byte[] msg = new byte[1 + 4 + bodyLen];
+ msg[0] = 'E';
+ int len = 4 + bodyLen;
+ msg[1] = (byte) (len >> 24);
+ msg[2] = (byte) (len >> 16);
+ msg[3] = (byte) (len >> 8);
+ msg[4] = (byte) len;
+ int pos = 5;
+ System.arraycopy(portalBytes, 0, msg, pos, portalBytes.length);
+ pos += portalBytes.length;
+ msg[pos] = (byte) (maxRows >> 24);
+ msg[pos + 1] = (byte) (maxRows >> 16);
+ msg[pos + 2] = (byte) (maxRows >> 8);
+ msg[pos + 3] = (byte) maxRows;
+ return msg;
+ }
+
+ /** Build a Sync ('S') message. */
+ static byte[] syncMessage() {
+ return new byte[] {'S', 0, 0, 0, 4};
+ }
+
+ /** Build a complete extended query batch: Parse + Bind + Describe + Execute + Sync. */
+ static byte[] extendedQueryBatch(String sql) {
+ byte[] parse = parseMessage("", sql);
+ byte[] bind = bindMessage("", "");
+ byte[] describe = describeMessage('S', "");
+ byte[] execute = executeMessage("", 0);
+ byte[] sync = syncMessage();
+ byte[] batch =
+ new byte
+ [parse.length
+ + bind.length
+ + describe.length
+ + execute.length
+ + sync.length];
+ int pos = 0;
+ System.arraycopy(parse, 0, batch, pos, parse.length);
+ pos += parse.length;
+ System.arraycopy(bind, 0, batch, pos, bind.length);
+ pos += bind.length;
+ System.arraycopy(describe, 0, batch, pos, describe.length);
+ pos += describe.length;
+ System.arraycopy(execute, 0, batch, pos, execute.length);
+ pos += execute.length;
+ System.arraycopy(sync, 0, batch, pos, sync.length);
+ return batch;
+ }
+
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
diff --git a/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties b/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties
index b86e4c8..6b8c939 100644
--- a/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties
+++ b/it/src/it/quarkus-pet-clinic/src/main/resources/application.properties
@@ -11,4 +11,4 @@ quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.devservices.enabled=false
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
-quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false
+quarkus.hibernate-orm.unsupported-properties."hibernate.boot.allow_jdbc_metadata_access"=false
\ No newline at end of file
diff --git a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java
index b950508..6401a56 100644
--- a/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java
+++ b/jdbc/src/main/java/io/roastedroot/pglite4j/jdbc/PgLiteDriver.java
@@ -4,7 +4,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
@@ -14,9 +13,13 @@
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Properties;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
public final class PgLiteDriver implements Driver {
@@ -69,6 +72,10 @@ public Connection connect(String url, Properties info) throws SQLException {
props.putIfAbsent("password", "password");
props.setProperty("sslmode", "disable");
props.setProperty("gssEncMode", "disable");
+ // All connections share one PG backend — named prepared statements
+ // would collide (S_1, S_2, ...). Force unnamed statements only.
+ props.setProperty("prepareThreshold", "0");
+ props.putIfAbsent("connectTimeout", "60");
String pgUrl = "jdbc:postgresql://127.0.0.1:" + instance.getPort() + "/template1";
return new org.postgresql.Driver().connect(pgUrl, props);
@@ -108,11 +115,15 @@ static final class ManagedInstance {
private PGLite pgLite;
private ServerSocket serverSocket;
private volatile boolean running;
+ private final Object pgLock = new Object();
+ private final AtomicInteger connectionCounter = new AtomicInteger();
+ private final Set activeSockets = ConcurrentHashMap.newKeySet();
+ private volatile List cachedStartupResponses;
void boot() {
pgLite = PGLite.builder().build();
try {
- serverSocket = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"));
+ serverSocket = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
pgLite.close();
throw new RuntimeException("Failed to create ServerSocket", e);
@@ -131,40 +142,48 @@ private void acceptLoop() {
while (running) {
try {
Socket socket = serverSocket.accept();
- handleConnection(socket);
+ Thread handler =
+ new Thread(
+ () -> handleConnection(socket),
+ "pglite-conn-" + connectionCounter.getAndIncrement());
+ handler.setDaemon(true);
+ handler.start();
} catch (IOException e) {
if (running) {
- throw new UncheckedIOException(
- "PgLiteDriver: accept error: " + e.getMessage(), e);
+ // log but don't crash the accept loop
}
}
}
}
private void handleConnection(Socket socket) {
+ activeSockets.add(socket);
try {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[65536];
+ handleStartup(in, out, buf);
+
while (running) {
int n = in.read(buf);
if (n <= 0) {
break;
}
byte[] message = Arrays.copyOf(buf, n);
- byte[] response = pgLite.execProtocolRaw(message);
+ byte[] response;
+ synchronized (pgLock) {
+ response = pgLite.execProtocolRaw(message);
+ }
if (response.length > 0) {
out.write(response);
out.flush();
}
}
- } catch (IOException e) {
- if (running) {
- throw new UncheckedIOException(
- "PgLiteDriver: connection error: " + e.getMessage(), e);
- }
+ } catch (IOException | RuntimeException e) {
+ // one connection failure must not crash other connections
} finally {
+ activeSockets.remove(socket);
try {
socket.close();
} catch (IOException e) {
@@ -173,6 +192,68 @@ private void handleConnection(Socket socket) {
}
}
+ private void handleStartup(InputStream in, OutputStream out, byte[] buf)
+ throws IOException {
+ List cached = cachedStartupResponses;
+ if (cached != null) {
+ replayStartup(cached, in, out, buf);
+ return;
+ }
+ synchronized (pgLock) {
+ cached = cachedStartupResponses;
+ if (cached != null) {
+ replayStartup(cached, in, out, buf);
+ return;
+ }
+ List responses = new ArrayList<>();
+ while (running) {
+ int n = in.read(buf);
+ if (n <= 0) {
+ throw new IOException("Connection closed during startup");
+ }
+ byte[] message = Arrays.copyOf(buf, n);
+ byte[] response = pgLite.execProtocolRaw(message);
+ responses.add(response);
+ if (response.length > 0) {
+ out.write(response);
+ out.flush();
+ }
+ if (endsWithReadyForQuery(response)) {
+ break;
+ }
+ }
+ cachedStartupResponses = responses;
+ }
+ }
+
+ private static void replayStartup(
+ List cached, InputStream in, OutputStream out, byte[] buf)
+ throws IOException {
+ for (byte[] cachedResp : cached) {
+ int n = in.read(buf);
+ if (n <= 0) {
+ throw new IOException("Connection closed during startup replay");
+ }
+ if (cachedResp.length > 0) {
+ out.write(cachedResp);
+ out.flush();
+ }
+ }
+ }
+
+ private static boolean endsWithReadyForQuery(byte[] response) {
+ // ReadyForQuery: type='Z' (0x5A), length=5 (00 00 00 05), status byte
+ if (response.length < 6) {
+ return false;
+ }
+ int off = response.length - 6;
+ return response[off] == 'Z'
+ && response[off + 1] == 0
+ && response[off + 2] == 0
+ && response[off + 3] == 0
+ && response[off + 4] == 5;
+ }
+
void close() {
running = false;
try {
@@ -180,6 +261,13 @@ void close() {
} catch (IOException e) {
// cleanup
}
+ for (Socket s : activeSockets) {
+ try {
+ s.close();
+ } catch (IOException e) {
+ // cleanup
+ }
+ }
pgLite.close();
}
}
diff --git a/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java b/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java
index 04a8264..c5006b3 100644
--- a/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java
+++ b/jdbc/src/test/java/io/roastedroot/pglite4j/jdbc/PgLiteDriverTest.java
@@ -158,4 +158,69 @@ void driverAcceptsUrl() throws SQLException {
assertFalse(driver.acceptsURL("jdbc:postgresql://localhost/test"));
assertFalse(driver.acceptsURL("jdbc:mysql://localhost/test"));
}
+
+ @Test
+ @Order(10)
+ void namedDatabasesAreIndependent() throws SQLException {
+ try (Connection db1 = DriverManager.getConnection("jdbc:pglite:memory:db1");
+ Connection db2 = DriverManager.getConnection("jdbc:pglite:memory:db2")) {
+ try (Statement stmt = db1.createStatement()) {
+ stmt.execute("CREATE TABLE only_in_db1 (id INT)");
+ }
+ // Verify db2 does not have the table created on db1
+ try (Statement stmt = db2.createStatement();
+ ResultSet rs =
+ stmt.executeQuery(
+ "SELECT EXISTS ("
+ + "SELECT 1 FROM pg_class"
+ + " WHERE relname = 'only_in_db1'"
+ + " AND relkind = 'r')")) {
+ assertTrue(rs.next());
+ assertFalse(rs.getBoolean(1));
+ }
+ }
+ }
+
+ @Test
+ @Order(11)
+ void multipleConnectionsSameDatabase() throws SQLException {
+ String url = "jdbc:pglite:memory:multiconn";
+ try (Connection conn1 = DriverManager.getConnection(url);
+ Connection conn2 = DriverManager.getConnection(url)) {
+ try (Statement stmt = conn1.createStatement()) {
+ stmt.execute("CREATE TABLE shared_table (id INT, val TEXT)");
+ stmt.executeUpdate("INSERT INTO shared_table VALUES (1, 'hello')");
+ }
+ try (Statement stmt = conn2.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT val FROM shared_table WHERE id = 1")) {
+ assertTrue(rs.next());
+ assertEquals("hello", rs.getString("val"));
+ }
+ }
+ }
+
+ @Test
+ @Order(12)
+ void connectionCloseDoesNotAffectOther() throws SQLException {
+ String url = "jdbc:pglite:memory:closetest";
+ Connection conn1 = DriverManager.getConnection(url);
+ Connection conn2 = DriverManager.getConnection(url);
+ try {
+ try (Statement stmt = conn1.createStatement()) {
+ stmt.execute("CREATE TABLE survive_close (id INT)");
+ stmt.executeUpdate("INSERT INTO survive_close VALUES (42)");
+ }
+ conn1.close();
+
+ try (Statement stmt = conn2.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT id FROM survive_close")) {
+ assertTrue(rs.next());
+ assertEquals(42, rs.getInt(1));
+ }
+ } finally {
+ if (!conn2.isClosed()) {
+ conn2.close();
+ }
+ }
+ }
}
diff --git a/wasm-build/patches/pglite-wasm/interactive_one.c.diff b/wasm-build/patches/pglite-wasm/interactive_one.c.diff
new file mode 100644
index 0000000..8501eaa
--- /dev/null
+++ b/wasm-build/patches/pglite-wasm/interactive_one.c.diff
@@ -0,0 +1,14 @@
+--- a/interactive_one.c
++++ b/interactive_one.c
+@@ -646,6 +646,11 @@
+ }
+ }
+ resume_on_error:
++ ignore_till_sync = false;
++ send_ready_for_query = true;
++ /* pglite: drain stale receive buffer left from the errored batch
++ * so the next pq_startmsgread() resets cleanly to new CMA data */
++ while (pq_buffer_remaining_data() > 0) pq_getbyte();
+ if (!is_repl) {
+ wire_flush:
+ if (!ClientAuthInProgress) {
diff --git a/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff b/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff
index 36bdaad..fc98382 100644
--- a/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff
+++ b/wasm-build/patches/postgresql-pglite/src-backend-utils-error-elog.c.diff
@@ -1,37 +1,36 @@
---- REL_17_5_WASM/src/backend/utils/error/elog.c
-+++ REL_17_5_WASM-pglite/src/backend/utils/error/elog.c
-@@ -348,12 +348,16 @@
- ErrorData *edata;
- bool output_to_server;
- bool output_to_client = false;
-+#if defined(__EMSCRIPTEN__) || defined(__wasi__)
-+# warning "FIXME: error levels"
-+#else
- int i;
-
- /*
- * Check some cases in which we want to promote an error into a more
- * severe error. None of this logic applies for non-error messages.
- */
+--- a/src/backend/utils/error/elog.c
++++ b/src/backend/utils/error/elog.c
+@@ -85,6 +85,25 @@
+ #include "utils/ps_status.h"
+ #include "utils/varlena.h"
+
++#if defined(__wasi__)
++static volatile int pgl_error_flag = 0;
++
++/* Called from errfinish() instead of abort() on PG ERROR.
++ Sets flag so the host runtime can detect and recover. */
++static void pgl_on_error(void) {
++ pgl_error_flag = 1;
++}
+
- if (elevel >= ERROR)
- {
- /*
-@@ -394,7 +398,7 @@
- for (i = 0; i <= errordata_stack_depth; i++)
- elevel = Max(elevel, errordata[i].elevel);
- }
--
++/* Exported: returns 1 if a PG error trapped, resets the flag.
++ Called by Java after catching a TrapException. */
++__attribute__((export_name("pgl_check_error")))
++int pgl_check_error(void) {
++ int val = pgl_error_flag;
++ pgl_error_flag = 0;
++ return val;
++}
+#endif
- /*
- * Now decide whether we need to process this report at all; if it's
- * warning or less and not enabled for logging, just return false without
-@@ -539,7 +543,12 @@
- */
-
++
+ /* In this module, access gettext() via err_gettext() */
+ #undef _
+ #define _(x) err_gettext(x)
+@@ -546,7 +565,16 @@
recursion_depth--;
-+#if defined(__wasi__)
-+ fprintf(stderr, "# 547: PG_RE_THROW(ERROR : %d) ignored\r\n", recursion_depth);
+ #if defined(__wasi__)
+ fprintf(stderr, "# 547: PG_RE_THROW(ERROR : %d) ignored\r\n", recursion_depth);
+- abort();
+ if (errordata_stack_depth >= 0) {
+ ErrorData *__edata = &errordata[errordata_stack_depth];
+ fprintf(stderr, "# PG ERROR [%s:%d] %s: %s\r\n",
@@ -40,30 +39,8 @@
+ __edata->funcname ? __edata->funcname : "?",
+ __edata->message ? __edata->message : "(no message)");
+ }
-+ abort();
-+#else
++ pgl_on_error();
++ __builtin_unreachable();
+ #else
PG_RE_THROW();
-+#endif
- }
-
- /* Emit the message to the right places */
-@@ -587,7 +596,11 @@
- * FATAL termination. The postmaster may or may not consider this
- * worthy of panic, depending on which subprocess returns it.
- */
-+#if defined(__EMSCRIPTEN__) || defined(__wasi__)
-+ puts("# 600: proc_exit(FATAL) ignored\r\n");
-+#else
- proc_exit(1);
-+#endif
- }
-
- if (elevel >= PANIC)
-@@ -697,6 +710,7 @@
- */
- if (edata->elevel >= ERROR)
- {
-+puts("#712");
- errfinish(filename, lineno, funcname);
- pg_unreachable();
- }
+ #endif