diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6bfb790 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Keep the vendored upstream tree byte-identical; export-upstream checksums it. +upstream-xerial_sqlite-jdbc/** -text whitespace=cr-at-eol,-blank-at-eol,-blank-at-eof diff --git a/AGENTS.md b/AGENTS.md index a9a094f..a1a5280 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,11 +29,14 @@ mise exec -- ./mvnw -version ```text libsql-java/ -├── libsql-hrana-codec/ # Core protocol data structures & DirectHranaJsonCodec -├── libsql-client/ # Low-level HTTP transport, baton state management, pipeline queue -├── libsql-jdbc/ # Standard JDBC 4.3 driver (java.sql.* implementation) -├── libsql-itests/ # Testcontainers (sqld) and Toxiproxy integration tests -└── vendor/ # Upstream specifications and documentation (Hrana v3, sqld) +├── libsql-hrana-codec/ # Core protocol data structures & DirectHranaJsonCodec +├── libsql-client/ # Low-level HTTP transport, baton state management, pipeline queue +├── libsql-jdbc/ # Standard JDBC 4.3 driver (java.sql.* implementation) +├── libsql-itests/ # Testcontainers (sqld) and Toxiproxy integration tests +├── libsql-conformance/ # Selected xerial/sqlite-jdbc tests running against libSQL +├── tools/ # Maintainer scripts such as export-upstream +├── upstream-xerial_sqlite-jdbc/ # Vendored xerial/sqlite-jdbc sources for conformance tests +└── vendor/ # Upstream specifications and documentation (Hrana v3, sqld) ``` ### Module Guidelines @@ -85,6 +88,31 @@ To run a specific test: mise exec -- ./mvnw -pl libsql-itests -Dtest=HikariCpIntegrationIT test ``` +### nono Sandbox and Docker Config + +This environment may run inside a `nono` filesystem sandbox. The sandbox can block reads of `~/.docker/config.json` even when `docker info` works. Testcontainers then fails with: + +```text +ServiceConfigurationError: ... EnvironmentAndSystemPropertyClientProviderStrategy could not be instantiated +Caused by: java.io.FileNotFoundException: /home//.docker/config.json (Permission denied) +``` + +Detect the sandbox with `nono status`, or check the exact path with: + +```bash +nono why --self --path ~/.docker/config.json --op read +``` + +Do not change file permissions or the nono profile from inside the sandbox. `libsql-conformance` uses a readable Docker config automatically. It copies `src/test/docker-config/config.json` to `target/docker-config` and sets `DOCKER_CONFIG` for the Failsafe fork. Use that empty auth config only for public images. + +To use a readable copy of your real Docker config, override the module property: + +```bash +./mvnw -pl libsql-conformance -am -Ddocker.config.dir=/path/to/docker-config verify +``` + +Setting `DOCKER_CONFIG` in the shell is not enough by itself because the Failsafe fork receives its own value from `docker.config.dir`. + --- ## 5. Coding Standards & Conventions diff --git a/libsql-conformance/pom.xml b/libsql-conformance/pom.xml new file mode 100644 index 0000000..37a727c --- /dev/null +++ b/libsql-conformance/pom.xml @@ -0,0 +1,135 @@ + + + 4.0.0 + + com.github.magnusp.libsql + libsql-java-parent + ${revision} + + + libsql-conformance + libSQL xerial Conformance Tests + Selected xerial/sqlite-jdbc tests running against libSQL + + + ${project.build.directory}/docker-config + + + + + com.github.magnusp.libsql + libsql-jdbc + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.platform + junit-platform-launcher + test + + + org.assertj + assertj-core + test + + + org.testcontainers + testcontainers + test + + + org.slf4j + slf4j-simple + 2.0.19 + test + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-xerial-test-sources + generate-test-sources + + copy-resources + + + ${project.build.directory}/generated-test-sources/xerial + + + ${project.basedir}/../upstream-xerial_sqlite-jdbc/src/test/java + false + + + + + + copy-docker-config + pre-integration-test + + copy-resources + + + ${project.build.directory}/docker-config + + + ${project.basedir}/src/test/docker-config + false + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.1 + + + add-xerial-test-sources + generate-test-sources + + add-test-source + + + + ${project.build.directory}/generated-test-sources/xerial + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*Test.java + + + ${docker.config.dir} + + + + + + diff --git a/libsql-conformance/src/test/docker-config/config.json b/libsql-conformance/src/test/docker-config/config.json new file mode 100644 index 0000000..4bfe101 --- /dev/null +++ b/libsql-conformance/src/test/docker-config/config.json @@ -0,0 +1 @@ +{"auths":{}} diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java new file mode 100644 index 0000000..b755479 --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java @@ -0,0 +1,151 @@ +package com.github.magnusp.libsql.conformance; + +import com.github.magnusp.libsql.jdbc.LibsqlDriver; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.UUID; +import java.util.logging.Logger; + +public final class XerialConformanceDriver implements Driver { + + private static final String PREFIX = "jdbc:sqlite:"; + private static final String HTTP_PREFIX = PREFIX + "http://"; + private static final String HTTPS_PREFIX = PREFIX + "https://"; + + private final LibsqlDriver delegate = new LibsqlDriver(); + + public static void register() { + try { + DriverManager.registerDriver(new XerialConformanceDriver()); + } catch (SQLException e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public Connection connect(String url, Properties info) throws SQLException { + if (!acceptsURL(url)) { + return null; + } + Target target = rewrite(url); + Connection connection; + try { + connection = delegate.connect(target.url(), info); + } catch (SQLException e) { + XerialTestServer.releaseNamespace(target.namespace()); + throw e; + } + if (connection == null) { + XerialTestServer.releaseNamespace(target.namespace()); + return null; + } + try { + return (Connection) + Proxy.newProxyInstance( + XerialConformanceDriver.class.getClassLoader(), + new Class[] {Connection.class}, + new NamespaceClosingHandler(connection, target.namespace())); + } catch (RuntimeException e) { + XerialTestServer.releaseNamespace(target.namespace()); + throw e; + } + } + + @Override + public boolean acceptsURL(String url) { + if (url == null || !url.startsWith(PREFIX)) { + return false; + } + return !url.startsWith(HTTP_PREFIX) && !url.startsWith(HTTPS_PREFIX); + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return delegate.getPropertyInfo(url, info); + } + + @Override + public int getMajorVersion() { + return delegate.getMajorVersion(); + } + + @Override + public int getMinorVersion() { + return delegate.getMinorVersion(); + } + + @Override + public boolean jdbcCompliant() { + return delegate.jdbcCompliant(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return delegate.getParentLogger(); + } + + private static Target rewrite(String url) { + String target = url.substring(PREFIX.length()); + String namespace; + if (target.isEmpty() || ":memory:".equals(target)) { + namespace = "mem_" + UUID.randomUUID().toString().replace("-", ""); + } else { + namespace = + "file_" + + UUID.nameUUIDFromBytes(target.getBytes(StandardCharsets.UTF_8)) + .toString() + .replace("-", ""); + } + return new Target(XerialTestServer.jdbcUrl(namespace), namespace); + } + + private record Target(String url, String namespace) {} + + private static final class NamespaceClosingHandler implements InvocationHandler { + + private final Connection connection; + private final String namespace; + private boolean closed; + + private NamespaceClosingHandler(Connection connection, String namespace) { + this.connection = connection; + this.namespace = namespace; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("close".equals(method.getName()) && method.getParameterCount() == 0) { + close(); + return null; + } + try { + return method.invoke(connection, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private synchronized void close() throws SQLException { + if (closed) { + return; + } + closed = true; + try { + connection.close(); + } finally { + XerialTestServer.releaseNamespace(namespace); + } + } + } +} diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialLauncherSessionListener.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialLauncherSessionListener.java new file mode 100644 index 0000000..f1f503c --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialLauncherSessionListener.java @@ -0,0 +1,17 @@ +package com.github.magnusp.libsql.conformance; + +import org.junit.platform.launcher.LauncherSession; +import org.junit.platform.launcher.LauncherSessionListener; + +public final class XerialLauncherSessionListener implements LauncherSessionListener { + + @Override + public void launcherSessionOpened(LauncherSession session) { + XerialConformanceDriver.register(); + } + + @Override + public void launcherSessionClosed(LauncherSession session) { + XerialTestServer.stop(); + } +} diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java new file mode 100644 index 0000000..e4f7148 --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java @@ -0,0 +1,167 @@ +package com.github.magnusp.libsql.conformance; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class XerialTestServer { + + private static final String IMAGE = + System.getProperty( + "libsql.image", + "ghcr.io/tursodatabase/libsql-server@sha256:6dd3eb276d9d3604e4a48ac4a999a2e267814732d57d7e94c04ba71482333a67"); + private static final int HTTP_PORT = 8080; + private static final int ADMIN_PORT = 9090; + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + + private static final HttpClient HTTP = HttpClient.newHttpClient(); + private static final Map NAMESPACES = new ConcurrentHashMap<>(); + + private static GenericContainer container; + private static String httpBaseUrl; + private static String adminBaseUrl; + private static boolean shutdownHookInstalled; + + private XerialTestServer() {} + + public static synchronized void start() { + if (container != null) { + return; + } + + GenericContainer started = + new GenericContainer<>(IMAGE) + .withExposedPorts(HTTP_PORT, ADMIN_PORT) + .withEnv("SQLD_NODE", "primary") + .withEnv("SQLD_DISABLE_INTELLIGENT_THROTTLING", "true") + .withEnv("SQLD_CONNECTION_CREATION_TIMEOUT_SEC", "30") + .withCommand( + "sqld", + "--http-listen-addr", + "0.0.0.0:" + HTTP_PORT, + "--enable-namespaces", + "--max-active-namespaces", + "1000", + "--admin-listen-addr", + "0.0.0.0:" + ADMIN_PORT) + .waitingFor( + Wait.forHttp("/v2/pipeline") + .forPort(HTTP_PORT) + .forStatusCodeMatching(XerialTestServer::isReadyStatus) + .withStartupTimeout(Duration.ofSeconds(120))); + + started.start(); + container = started; + httpBaseUrl = + "http://" + started.getHost() + ":" + started.getMappedPort(HTTP_PORT); + adminBaseUrl = + "http://" + started.getHost() + ":" + started.getMappedPort(ADMIN_PORT); + + if (!shutdownHookInstalled) { + Runtime.getRuntime() + .addShutdownHook(new Thread(XerialTestServer::stop, "xerial-sqld-shutdown")); + shutdownHookInstalled = true; + } + } + + public static synchronized void stop() { + if (container == null) { + return; + } + try { + container.stop(); + } finally { + container = null; + httpBaseUrl = null; + adminBaseUrl = null; + NAMESPACES.clear(); + } + } + + public static String jdbcUrl(String namespace) { + start(); + ensureNamespace(namespace); + return "jdbc:sqlite:" + httpBaseUrl + "/dev/" + namespace; + } + + public static synchronized void releaseNamespace(String namespace) { + int active = NAMESPACES.getOrDefault(namespace, 0); + if (active == 0) { + return; + } + if (active > 1) { + NAMESPACES.put(namespace, active - 1); + return; + } + NAMESPACES.remove(namespace); + if (container == null) { + return; + } + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(adminBaseUrl + "/v1/namespaces/" + namespace)) + .timeout(REQUEST_TIMEOUT) + .DELETE() + .build(); + + try { + HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + static boolean isReadyStatus(int status) { + return status == 405 || (status >= 200 && status < 300); + } + + private static synchronized void ensureNamespace(String namespace) { + int active = NAMESPACES.getOrDefault(namespace, 0); + if (active > 0) { + NAMESPACES.put(namespace, active + 1); + return; + } + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(adminBaseUrl + "/v1/namespaces/" + namespace + "/create")) + .header("Content-Type", "application/json") + .timeout(REQUEST_TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofString("{}")) + .build(); + + HttpResponse response; + try { + response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + throw new IllegalStateException("Failed to create namespace " + namespace, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Interrupted while creating namespace " + namespace, e); + } + + if (response.statusCode() != 200 && response.statusCode() != 409) { + throw new IllegalStateException( + "Failed to create namespace " + + namespace + + ": HTTP " + + response.statusCode() + + " " + + response.body()); + } + + NAMESPACES.put(namespace, 1); + } +} diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerTest.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerTest.java new file mode 100644 index 0000000..72e1329 --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerTest.java @@ -0,0 +1,21 @@ +package com.github.magnusp.libsql.conformance; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class XerialTestServerTest { + + @Test + void acceptsOnlySuccessAndMethodNotAllowedAsReady() { + assertThat(XerialTestServer.isReadyStatus(200)).isTrue(); + assertThat(XerialTestServer.isReadyStatus(204)).isTrue(); + assertThat(XerialTestServer.isReadyStatus(405)).isTrue(); + assertThat(XerialTestServer.isReadyStatus(301)).isFalse(); + assertThat(XerialTestServer.isReadyStatus(400)).isFalse(); + assertThat(XerialTestServer.isReadyStatus(401)).isFalse(); + assertThat(XerialTestServer.isReadyStatus(403)).isFalse(); + assertThat(XerialTestServer.isReadyStatus(404)).isFalse(); + assertThat(XerialTestServer.isReadyStatus(500)).isFalse(); + } +} diff --git a/libsql-conformance/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/libsql-conformance/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 0000000..bdef061 --- /dev/null +++ b/libsql-conformance/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +com.github.magnusp.libsql.conformance.XerialLauncherSessionListener diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java index 0a812b2..b22c099 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlConnection.java @@ -188,12 +188,16 @@ private void closeStreamSilently() { } } - @Override - public Statement createStatement() throws SQLException { + LibsqlStatement createStatementInternal() throws SQLException { checkClosed(); return new LibsqlStatement(this); } + @Override + public Statement createStatement() throws SQLException { + return createStatementInternal(); + } + @Override public PreparedStatement prepareStatement(String sql) throws SQLException { checkClosed(); diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java index 5d73506..ede57eb 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java @@ -267,14 +267,29 @@ public boolean supportsTransactionIsolationLevel(int level) throws SQLException @Override public boolean dataDefinitionIgnoredInTransactions() throws SQLException { return false; } + private ResultSet executeMetadataQuery(String sql) throws SQLException { + LibsqlStatement statement = connection.createStatementInternal(); + try { + statement.closeOnCompletion(); + return statement.executeQuery(sql); + } catch (SQLException e) { + statement.close(); + throw e; + } + } + + private static String escapeLiteral(String value) { + return value.replace("'", "''"); + } + @Override public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { StringBuilder sql = new StringBuilder("SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, name AS TABLE_NAME, UPPER(type) AS TABLE_TYPE, NULL AS REMARKS, NULL AS TYPE_CAT, NULL AS TYPE_SCHEM, NULL AS TYPE_NAME, NULL AS SELF_REFERENCING_COL_NAME, NULL AS REF_GENERATION FROM sqlite_master WHERE 1=1"); if (tableNamePattern != null && !tableNamePattern.equals("%")) { - sql.append(" AND name LIKE '").append(tableNamePattern).append("'"); + sql.append(" AND name LIKE '").append(escapeLiteral(tableNamePattern)).append("'"); } sql.append(" ORDER BY TABLE_TYPE, TABLE_NAME"); - return connection.createStatement().executeQuery(sql.toString()); + return executeMetadataQuery(sql.toString()); } @Override @@ -297,27 +312,26 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa "FROM sqlite_master m JOIN pragma_table_info(m.name) p " + "WHERE 1=1"; if (tableNamePattern != null && !tableNamePattern.equals("%")) { - sql += " AND m.name LIKE '" + tableNamePattern + "'"; + sql += " AND m.name LIKE '" + escapeLiteral(tableNamePattern) + "'"; } if (columnNamePattern != null && !columnNamePattern.equals("%")) { - sql += " AND p.name LIKE '" + columnNamePattern + "'"; + sql += " AND p.name LIKE '" + escapeLiteral(columnNamePattern) + "'"; } sql += " ORDER BY TABLE_NAME, ORDINAL_POSITION"; - return connection.createStatement().executeQuery(sql); + return executeMetadataQuery(sql); } @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, '" + table + "' AS TABLE_NAME, name AS COLUMN_NAME, pk AS KEY_SEQ, NULL AS PK_NAME " + - "FROM pragma_table_info('" + table + "') WHERE pk > 0 ORDER BY pk"; - return connection.createStatement().executeQuery(sql); + String escapedTable = escapeLiteral(table == null ? "" : table); + String sql = "SELECT NULL AS TABLE_CAT, NULL AS TABLE_SCHEM, '" + escapedTable + "' AS TABLE_NAME, name AS COLUMN_NAME, pk AS KEY_SEQ, NULL AS PK_NAME " + + "FROM pragma_table_info('" + escapedTable + "') WHERE pk > 0 ORDER BY pk"; + return executeMetadataQuery(sql); } @Override public ResultSet getTableTypes() throws SQLException { - return connection.createStatement().executeQuery( - "SELECT 'TABLE' AS TABLE_TYPE UNION SELECT 'VIEW' AS TABLE_TYPE" - ); + return executeMetadataQuery("SELECT 'TABLE' AS TABLE_TYPE UNION SELECT 'VIEW' AS TABLE_TYPE"); } @Override diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java index b244e70..15b2522 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlPreparedStatement.java @@ -68,10 +68,12 @@ public int executeUpdate() throws SQLException { @Override public boolean execute() throws SQLException { + checkClosed(); + closeCurrentResultSet(); checkClosed(); StmtResult result = connection.executeStatement(buildCurrentStmt()); if (result.cols() != null && !result.cols().isEmpty()) { - this.currentResultSet = new LibsqlResultSet(this, result); + this.currentResultSet = new LibsqlResultSet(this, result, sql, false); this.updateCount = -1; return true; } else { diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java index c567f8b..16a35eb 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java @@ -24,6 +24,10 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; import java.util.Calendar; import java.util.HashMap; import java.util.List; @@ -35,17 +39,30 @@ public class LibsqlResultSet implements ResultSet { private final Statement statement; private final List cols; private final List rows; + private final String sql; + private final LibsqlConnection connection; + private final boolean closeStatementOnClose; private int currentRowIndex = -1; private boolean wasNull = false; private boolean closed = false; + private LibsqlResultSetMetaData metadata; // Mitigation for Bug 6: O(1) column label lookup private Map columnNameToIndex; public LibsqlResultSet(Statement statement, StmtResult result) { + this(statement, result, null, false); + } + + LibsqlResultSet(Statement statement, StmtResult result, String sql, boolean closeStatementOnClose) { this.statement = statement; this.cols = (result != null && result.cols() != null) ? result.cols() : List.of(); this.rows = (result != null && result.rows() != null) ? result.rows() : List.of(); + this.sql = sql; + this.connection = statement instanceof LibsqlStatement libsqlStatement + ? libsqlStatement.connection + : null; + this.closeStatementOnClose = closeStatementOnClose; } private void checkClosed() throws SQLException { @@ -77,7 +94,15 @@ public boolean next() throws SQLException { @Override public void close() throws SQLException { + if (closed) { + return; + } this.closed = true; + if (statement != null + && (closeStatementOnClose + || (statement instanceof LibsqlStatement s && s.isCloseOnCompletion()))) { + statement.close(); + } } @Override @@ -122,7 +147,20 @@ public short getShort(int columnIndex) throws SQLException { @Override public int getInt(int columnIndex) throws SQLException { - return (int) getLong(columnIndex); + Value val = getCurrentValue(columnIndex); + return switch (val) { + case Value.Null n -> 0; + case Value.Integer i -> (int) i.value(); + case Value.Float f -> (int) f.value(); + case Value.Text t -> { + try { + yield Integer.parseInt(t.value()); + } catch (NumberFormatException e) { + throw new SQLException("Bad value for type Integer: " + t.value(), e); + } + } + case Value.Blob b -> 0; + }; } @Override @@ -136,7 +174,7 @@ public long getLong(int columnIndex) throws SQLException { try { yield Long.parseLong(t.value()); } catch (NumberFormatException e) { - yield 0L; + throw new SQLException("Bad value for type Long: " + t.value(), e); } } case Value.Blob b -> 0L; @@ -145,7 +183,20 @@ public long getLong(int columnIndex) throws SQLException { @Override public float getFloat(int columnIndex) throws SQLException { - return (float) getDouble(columnIndex); + Value val = getCurrentValue(columnIndex); + return switch (val) { + case Value.Null n -> 0.0F; + case Value.Integer i -> (float) i.value(); + case Value.Float f -> (float) f.value(); + case Value.Text t -> { + try { + yield Float.parseFloat(t.value()); + } catch (NumberFormatException e) { + throw new SQLException("Bad value for type Float: " + t.value(), e); + } + } + case Value.Blob b -> 0.0F; + }; } @Override @@ -159,7 +210,7 @@ public double getDouble(int columnIndex) throws SQLException { try { yield Double.parseDouble(t.value()); } catch (NumberFormatException e) { - yield 0.0; + throw new SQLException("Bad value for type Double: " + t.value(), e); } } case Value.Blob b -> 0.0; @@ -229,7 +280,14 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { String s = getString(columnIndex); - return s == null ? null : new BigDecimal(s); + if (s == null) { + return null; + } + try { + return new BigDecimal(s); + } catch (NumberFormatException e) { + throw new SQLException("Bad value for type BigDecimal: " + s, e); + } } @Override @@ -237,7 +295,9 @@ public Object getObject(int columnIndex) throws SQLException { Value val = getCurrentValue(columnIndex); return switch (val) { case Value.Null n -> null; - case Value.Integer i -> i.value(); + case Value.Integer i -> i.value() >= Integer.MIN_VALUE && i.value() <= Integer.MAX_VALUE + ? (Object) (int) i.value() + : (Object) i.value(); case Value.Float f -> f.value(); case Value.Text t -> t.value(); case Value.Blob b -> b.bytes(); @@ -343,7 +403,10 @@ public Object getObject(String columnLabel) throws SQLException { @Override public ResultSetMetaData getMetaData() throws SQLException { checkClosed(); - return new LibsqlResultSetMetaData(cols); + if (metadata == null) { + metadata = new LibsqlResultSetMetaData(cols, rows, sql, connection); + } + return metadata; } @Override @@ -537,10 +600,109 @@ public boolean isLast() throws SQLException { @Override public T getObject(int columnIndex, Class type) throws SQLException { + if (type == null) { + throw new SQLException("Type must not be null"); + } Object val = getObject(columnIndex); - if (val == null) return null; - if (type.isInstance(val)) return type.cast(val); - throw new SQLException("Cannot convert value of type " + val.getClass() + " to " + type); + if (val == null) { + return null; + } + if (type.isInstance(val)) { + return type.cast(val); + } + if (type == String.class) { + return type.cast(getString(columnIndex)); + } + if (type == Boolean.class) { + return type.cast(getBoolean(columnIndex)); + } + if (type == BigDecimal.class) { + return type.cast(getBigDecimal(columnIndex)); + } + if (type == byte[].class) { + return type.cast(getBytes(columnIndex)); + } + if (type == Double.class) { + return type.cast(getDouble(columnIndex)); + } + if (type == Long.class) { + return type.cast(getLong(columnIndex)); + } + if (type == Float.class) { + return type.cast(getFloat(columnIndex)); + } + if (type == Integer.class) { + return type.cast(getInt(columnIndex)); + } + if (type == Short.class) { + return type.cast(getShort(columnIndex)); + } + if (type == Byte.class) { + return type.cast(getByte(columnIndex)); + } + if (type == Date.class) { + return type.cast(getDate(columnIndex)); + } + if (type == Time.class) { + return type.cast(getTime(columnIndex)); + } + if (type == Timestamp.class) { + return type.cast(getTimestamp(columnIndex)); + } + if (type == LocalDate.class) { + return type.cast(getLocalDate(columnIndex)); + } + if (type == LocalTime.class) { + return type.cast(getLocalTime(columnIndex)); + } + if (type == LocalDateTime.class) { + return type.cast(getLocalDateTime(columnIndex)); + } + throw new SQLException( + "Cannot convert value of type " + val.getClass() + " to " + type.getName()); + } + + private LocalDate getLocalDate(int columnIndex) throws SQLException { + String s = getString(columnIndex); + if (s == null) { + return null; + } + try { + return LocalDate.parse(s.length() >= 10 ? s.substring(0, 10) : s); + } catch (DateTimeParseException e) { + throw new SQLException("Bad value for type LocalDate: " + s, e); + } + } + + private LocalTime getLocalTime(int columnIndex) throws SQLException { + String s = getString(columnIndex); + if (s == null) { + return null; + } + int separator = Math.max(s.indexOf(' '), s.indexOf('T')); + String value = separator >= 0 ? s.substring(separator + 1) : s; + try { + return LocalTime.parse(value); + } catch (DateTimeParseException e) { + throw new SQLException("Bad value for type LocalTime: " + s, e); + } + } + + private LocalDateTime getLocalDateTime(int columnIndex) throws SQLException { + String s = getString(columnIndex); + if (s == null) { + return null; + } + String value = s.indexOf(' ') >= 0 ? s.replace(' ', 'T') : s; + try { + return LocalDateTime.parse(value); + } catch (DateTimeParseException e) { + try { + return LocalDate.parse(value).atStartOfDay(); + } catch (DateTimeParseException e2) { + throw new SQLException("Bad value for type LocalDateTime: " + s, e2); + } + } } @Override diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java index c21c685..902403a 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaData.java @@ -1,18 +1,44 @@ package com.github.magnusp.libsql.jdbc; import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.Row; +import com.github.magnusp.libsql.hrana.codec.Stmt; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import com.github.magnusp.libsql.hrana.codec.Value; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class LibsqlResultSetMetaData implements ResultSetMetaData { + private static final Pattern COLUMN_TYPECAST = + Pattern.compile("(?i)cast\\(.*?\\s+as\\s+(.*?)\\s*\\)"); + private static final Pattern COLUMN_PRECISION = Pattern.compile(".*?\\((.*?)\\)"); + private final List cols; + private final List rows; + private final String sql; + private final LibsqlConnection connection; + + private boolean tableContextResolved; + private TableInfo tableContext; public LibsqlResultSetMetaData(List cols) { + this(cols, List.of(), null, null); + } + + LibsqlResultSetMetaData(List cols, List rows, String sql, LibsqlConnection connection) { this.cols = cols != null ? cols : List.of(); + this.rows = rows != null ? rows : List.of(); + this.sql = sql; + this.connection = connection; } @Override @@ -29,38 +55,66 @@ private Col getCol(int column) throws SQLException { @Override public boolean isAutoIncrement(int column) throws SQLException { - return false; + getCol(column); + TableInfo table = resolveTableContext(); + ColumnInfo info = table != null ? table.columns().get(columnName(column)) : null; + return info != null + && info.primaryKey() + && table.createSql() != null + && stripSqlCommentsAndLiterals(table.createSql()) + .toUpperCase(Locale.ENGLISH) + .contains("AUTOINCREMENT"); } @Override public boolean isCaseSensitive(int column) throws SQLException { + getCol(column); return true; } @Override public boolean isSearchable(int column) throws SQLException { + getCol(column); return true; } @Override public boolean isCurrency(int column) throws SQLException { + getCol(column); return false; } @Override public int isNullable(int column) throws SQLException { - return columnNullableUnknown; + getCol(column); + TableInfo table = resolveTableContext(); + ColumnInfo info = table != null ? table.columns().get(columnName(column)) : null; + if (info == null) { + return columnNullableUnknown; + } + return info.notNull() ? columnNoNulls : columnNullable; } @Override public boolean isSigned(int column) throws SQLException { - int type = getColumnType(column); - return type == Types.INTEGER || type == Types.BIGINT || type == Types.FLOAT || type == Types.DOUBLE; + return switch (getColumnType(column)) { + case Types.TINYINT, + Types.SMALLINT, + Types.INTEGER, + Types.BIGINT, + Types.NUMERIC, + Types.DECIMAL, + Types.REAL, + Types.FLOAT, + Types.DOUBLE -> true; + default -> false; + }; } @Override public int getColumnDisplaySize(int column) throws SQLException { - return 50; + getCol(column); + return Integer.MAX_VALUE; } @Override @@ -76,76 +130,472 @@ public String getColumnName(int column) throws SQLException { @Override public String getSchemaName(int column) throws SQLException { + getCol(column); return ""; } @Override public int getPrecision(int column) throws SQLException { - return 0; + String declType = declType(column); + if (declType == null) { + return 0; + } + Matcher matcher = COLUMN_PRECISION.matcher(declType); + if (!matcher.find()) { + return 0; + } + try { + return Integer.parseInt(matcher.group(1).split(",")[0].trim()); + } catch (NumberFormatException e) { + return 0; + } } @Override public int getScale(int column) throws SQLException { + String declType = declType(column); + if (declType == null) { + return 0; + } + Matcher matcher = COLUMN_PRECISION.matcher(declType); + if (matcher.find()) { + String[] parts = matcher.group(1).split(","); + if (parts.length == 2) { + try { + return Integer.parseInt(parts[1].trim()); + } catch (NumberFormatException e) { + return 0; + } + } + } return 0; } @Override public String getTableName(int column) throws SQLException { - return ""; + getCol(column); + TableInfo table = resolveTableContext(); + if (table == null || !table.columns().containsKey(columnName(column))) { + return ""; + } + return table.name(); } @Override public String getCatalogName(int column) throws SQLException { + getCol(column); return ""; } @Override public int getColumnType(int column) throws SQLException { - String decltype = getCol(column).decltype(); - if (decltype == null) return Types.OTHER; - String upper = decltype.toUpperCase(); - if (upper.contains("INT")) return Types.BIGINT; - if (upper.contains("CHAR") || upper.contains("TEXT") || upper.contains("CLOB")) return Types.VARCHAR; - if (upper.contains("BLOB")) return Types.BLOB; - if (upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB")) return Types.DOUBLE; - if (upper.contains("NUM") || upper.contains("DEC")) return Types.NUMERIC; - if (upper.contains("BOOL")) return Types.BOOLEAN; - return Types.OTHER; + getCol(column); + String typeName = getColumnTypeName(column); + Value sample = sampleValue(column); + + return switch (typeName) { + case "BOOLEAN" -> Types.BOOLEAN; + case "TINYINT" -> Types.TINYINT; + case "SMALLINT", "INT2" -> Types.SMALLINT; + case "BIGINT", "INT8", "UNSIGNED BIG INT" -> Types.BIGINT; + case "DATE" -> Types.DATE; + case "DATETIME", "TIMESTAMP" -> Types.TIMESTAMP; + case "INT", "INTEGER", "MEDIUMINT" -> integerType(column); + case "DECIMAL" -> Types.DECIMAL; + case "DOUBLE", "DOUBLE PRECISION" -> Types.DOUBLE; + case "NUMERIC" -> Types.NUMERIC; + case "REAL" -> Types.REAL; + case "FLOAT" -> Types.FLOAT; + case "CHARACTER", "NCHAR", "NATIVE CHARACTER", "CHAR" -> Types.CHAR; + case "CLOB" -> Types.CLOB; + case "VARCHAR", "VARYING CHARACTER", "NVARCHAR", "TEXT" -> Types.VARCHAR; + case "BINARY" -> Types.BINARY; + case "BLOB" -> Types.BLOB; + default -> inferTypeFromValue(sample); + }; } @Override public String getColumnTypeName(int column) throws SQLException { - String decltype = getCol(column).decltype(); - return decltype != null ? decltype : "OTHER"; + getCol(column); + String declType = declType(column); + if (declType != null) { + int paren = declType.indexOf('('); + String name = (paren >= 0 ? declType.substring(0, paren) : declType) + .trim() + .toUpperCase(Locale.ENGLISH); + if (!name.isEmpty()) { + return name; + } + } + return switch (sampleType(column)) { + case INTEGER -> "INTEGER"; + case FLOAT -> "FLOAT"; + case BLOB -> "BLOB"; + case TEXT -> "TEXT"; + default -> "NUMERIC"; + }; } @Override public boolean isReadOnly(int column) throws SQLException { + getCol(column); return false; } @Override public boolean isWritable(int column) throws SQLException { + getCol(column); return true; } @Override public boolean isDefinitelyWritable(int column) throws SQLException { + getCol(column); return true; } @Override public String getColumnClassName(int column) throws SQLException { + getCol(column); + Value sample = sampleValue(column); + if (sample != null) { + return switch (sample) { + case Value.Integer i -> integerClassName(column); + case Value.Float f -> Double.class.getName(); + case Value.Text t -> String.class.getName(); + case Value.Blob b -> Object.class.getName(); + case Value.Null n -> Object.class.getName(); + }; + } + if (!rows.isEmpty()) { + return Object.class.getName(); + } return switch (getColumnType(column)) { + case Types.TINYINT, Types.SMALLINT, Types.INTEGER -> Integer.class.getName(); case Types.BIGINT -> Long.class.getName(); - case Types.DOUBLE -> Double.class.getName(); - case Types.VARCHAR -> String.class.getName(); - case Types.BLOB -> byte[].class.getName(); + case Types.FLOAT, Types.REAL, Types.DOUBLE, Types.NUMERIC, Types.DECIMAL -> Double.class.getName(); + case Types.CHAR, Types.VARCHAR, Types.CLOB -> String.class.getName(); case Types.BOOLEAN -> Boolean.class.getName(); default -> Object.class.getName(); }; } + private String integerClassName(int column) { + Value firstNonInteger = null; + boolean outsideIntRange = false; + for (Row row : rows) { + List values = row.values(); + if (values == null || column < 1 || column > values.size()) { + continue; + } + Value value = values.get(column - 1); + if (value instanceof Value.Integer) { + if (!isIntInRange(value)) { + outsideIntRange = true; + } + } else if (value != null + && !(value instanceof Value.Null) + && firstNonInteger == null) { + firstNonInteger = value; + } + } + if (firstNonInteger instanceof Value.Float) { + return Double.class.getName(); + } + if (firstNonInteger instanceof Value.Text) { + return String.class.getName(); + } + if (firstNonInteger != null) { + return Object.class.getName(); + } + return outsideIntRange ? Long.class.getName() : Integer.class.getName(); + } + + private String declType(int column) throws SQLException { + Col col = getCol(column); + String declType = col.decltype(); + if (declType != null && !declType.isBlank()) { + return declType; + } + String name = col.name(); + if (name != null) { + Matcher matcher = COLUMN_TYPECAST.matcher(name); + if (matcher.find()) { + return matcher.group(1); + } + } + return null; + } + + private String columnName(int column) throws SQLException { + String name = getCol(column).name(); + return name != null ? name.toLowerCase(Locale.ROOT) : ""; + } + + private Value sampleValue(int column) { + for (Row row : rows) { + List values = row.values(); + if (values == null || column < 1 || column > values.size()) { + continue; + } + Value value = values.get(column - 1); + if (value != null && !(value instanceof Value.Null)) { + return value; + } + } + return null; + } + + private SampleType sampleType(int column) { + Value sample = sampleValue(column); + if (sample instanceof Value.Integer) { + return SampleType.INTEGER; + } + if (sample instanceof Value.Float) { + return SampleType.FLOAT; + } + if (sample instanceof Value.Blob) { + return SampleType.BLOB; + } + if (sample instanceof Value.Text) { + return SampleType.TEXT; + } + return SampleType.NULL; + } + + private static boolean isIntInRange(Value value) { + return value instanceof Value.Integer i + && i.value() >= Integer.MIN_VALUE + && i.value() <= Integer.MAX_VALUE; + } + + private int integerType(int column) { + Value firstNonInteger = null; + boolean outsideIntRange = false; + for (Row row : rows) { + List values = row.values(); + if (values == null || column < 1 || column > values.size()) { + continue; + } + Value value = values.get(column - 1); + if (value == null || value instanceof Value.Null) { + continue; + } + if (value instanceof Value.Integer) { + if (!isIntInRange(value)) { + outsideIntRange = true; + } + } else if (firstNonInteger == null) { + firstNonInteger = value; + } + } + if (firstNonInteger != null) { + return inferTypeFromValue(firstNonInteger); + } + return outsideIntRange ? Types.BIGINT : Types.INTEGER; + } + + private static int inferTypeFromValue(Value sample) { + if (sample instanceof Value.Integer i) { + return i.value() >= Integer.MIN_VALUE && i.value() <= Integer.MAX_VALUE + ? Types.INTEGER + : Types.BIGINT; + } + if (sample instanceof Value.Float) { + return Types.FLOAT; + } + if (sample instanceof Value.Text) { + return Types.VARCHAR; + } + if (sample instanceof Value.Blob) { + return Types.BLOB; + } + return Types.NUMERIC; + } + + private TableInfo resolveTableContext() throws SQLException { + if (!tableContextResolved) { + TableInfo resolved = null; + if (connection != null && sql != null) { + SqlTableReference.TableRef reference = SqlTableReference.parse(sql); + if (reference != null) { + resolved = loadTableInfo(reference); + } + } + tableContext = resolved; + tableContextResolved = true; + } + return tableContext; + } + + private TableInfo loadTableInfo(SqlTableReference.TableRef reference) throws SQLException { + String schema = reference.schema(); + String table = reference.table(); + if (schema == null) { + TableInfo temp = loadTableInfo("temp", table); + if (temp != null) { + return temp; + } + return loadTableInfo("main", table); + } + return switch (schema.toLowerCase(Locale.ROOT)) { + case "main" -> loadTableInfo("main", table); + case "temp" -> loadTableInfo("temp", table); + default -> loadAttachedTableInfo(schema, table); + }; + } + + private TableInfo loadTableInfo(String schema, String table) throws SQLException { + String master = schema.equals("temp") ? "sqlite_temp_master" : "sqlite_master"; + String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " + + "FROM " + master + " m JOIN pragma_table_info(?, ?) p " + + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ?"; + StmtResult result = connection.executeStatement( + new Stmt(query, List.of(Value.of(table), Value.of(schema), Value.of(table)))); + return toTableInfo(result, table); + } + + private TableInfo loadAttachedTableInfo(String schema, String table) throws SQLException { + String actualSchema = findSchema(schema); + if (actualSchema == null) { + return null; + } + String quotedSchema = "\"" + actualSchema.replace("\"", "\"\"") + "\""; + String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk " + + "FROM " + quotedSchema + ".sqlite_master m JOIN pragma_table_info(?, ?) p " + + "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ?"; + StmtResult result = connection.executeStatement(new Stmt( + query, List.of(Value.of(table), Value.of(actualSchema), Value.of(table)))); + return toTableInfo(result, table); + } + + private String findSchema(String schema) throws SQLException { + StmtResult databases = + connection.executeStatement(new Stmt("SELECT name FROM pragma_database_list")); + if (databases == null || databases.rows() == null) { + return null; + } + for (Row row : databases.rows()) { + List values = row.values(); + if (values == null || values.isEmpty()) { + continue; + } + String name = text(values.get(0)); + if (name != null && name.equalsIgnoreCase(schema)) { + return name; + } + } + return null; + } + + private static TableInfo toTableInfo(StmtResult result, String fallbackName) { + if (result == null || result.rows() == null || result.rows().isEmpty()) { + return null; + } + String tableName = null; + String createSql = null; + Map columns = new HashMap<>(); + for (Row row : result.rows()) { + List values = row.values(); + if (values == null || values.size() < 5) { + continue; + } + if (tableName == null) { + tableName = text(values.get(0)); + createSql = text(values.get(1)); + } + String columnName = text(values.get(2)); + if (columnName != null) { + columns.put( + columnName.toLowerCase(Locale.ROOT), + new ColumnInfo(integer(values.get(3)) != 0, integer(values.get(4)) != 0)); + } + } + return new TableInfo(tableName != null ? tableName : fallbackName, createSql, columns); + } + + private static String stripSqlCommentsAndLiterals(String sql) { + StringBuilder result = new StringBuilder(sql.length()); + int i = 0; + while (i < sql.length()) { + char c = sql.charAt(i); + if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') { + i += 2; + while (i < sql.length() && sql.charAt(i) != '\n' && sql.charAt(i) != '\r') { + i++; + } + result.append(' '); + continue; + } + if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { + int end = sql.indexOf("*/", i + 2); + if (end < 0) { + break; + } + result.append(' '); + i = end + 2; + continue; + } + if (c == '\'' || c == '"' || c == '`') { + int end = skipQuoted(sql, i, c); + if (end < 0) { + break; + } + result.append(' '); + i = end; + continue; + } + if (c == '[') { + int end = sql.indexOf(']', i + 1); + if (end < 0) { + break; + } + result.append(' '); + i = end + 1; + continue; + } + result.append(c); + i++; + } + return result.toString(); + } + + private static int skipQuoted(String sql, int start, char quote) { + int i = start + 1; + while (i < sql.length()) { + if (sql.charAt(i) == quote) { + if (i + 1 < sql.length() && sql.charAt(i + 1) == quote) { + i += 2; + continue; + } + return i + 1; + } + i++; + } + return -1; + } + + private static String text(Value value) { + return value instanceof Value.Text t ? t.value() : null; + } + + private static long integer(Value value) { + return value instanceof Value.Integer i ? i.value() : 0L; + } + + private enum SampleType { + INTEGER, + FLOAT, + BLOB, + TEXT, + NULL + } + + private record ColumnInfo(boolean notNull, boolean primaryKey) {} + + private record TableInfo(String name, String createSql, Map columns) {} + @Override public T unwrap(Class iface) throws SQLException { if (iface.isInstance(this)) return iface.cast(this); diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java index 1935023..951f5bc 100644 --- a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlStatement.java @@ -17,6 +17,7 @@ public class LibsqlStatement implements Statement { protected LibsqlResultSet currentResultSet = null; protected long updateCount = -1; protected boolean closed = false; + protected boolean closeOnCompletion = false; protected final List batchStatements = new ArrayList<>(); public LibsqlStatement(LibsqlConnection connection) { @@ -47,10 +48,12 @@ public int executeUpdate(String sql) throws SQLException { @Override public boolean execute(String sql) throws SQLException { + checkClosed(); + closeCurrentResultSet(); checkClosed(); StmtResult result = connection.executeStatement(new Stmt(sql)); if (result.cols() != null && !result.cols().isEmpty()) { - this.currentResultSet = new LibsqlResultSet(this, result); + this.currentResultSet = new LibsqlResultSet(this, result, sql, false); this.updateCount = -1; return true; } else { @@ -60,6 +63,13 @@ public boolean execute(String sql) throws SQLException { } } + protected void closeCurrentResultSet() throws SQLException { + if (currentResultSet != null) { + currentResultSet.close(); + currentResultSet = null; + } + } + @Override public ResultSet getResultSet() throws SQLException { checkClosed(); @@ -81,10 +91,7 @@ public long getLargeUpdateCount() throws SQLException { @Override public boolean getMoreResults() throws SQLException { checkClosed(); - if (currentResultSet != null) { - currentResultSet.close(); - currentResultSet = null; - } + closeCurrentResultSet(); updateCount = -1; return false; } @@ -161,8 +168,8 @@ public boolean isClosed() throws SQLException { @Override public int getResultSetHoldability() throws SQLException { return ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override public void setPoolable(boolean poolable) throws SQLException {} @Override public boolean isPoolable() throws SQLException { return false; } - @Override public void closeOnCompletion() throws SQLException {} - @Override public boolean isCloseOnCompletion() throws SQLException { return false; } + @Override public void closeOnCompletion() throws SQLException { closeOnCompletion = true; } + @Override public boolean isCloseOnCompletion() throws SQLException { return closeOnCompletion; } @Override public T unwrap(Class iface) throws SQLException { diff --git a/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTableReference.java b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTableReference.java new file mode 100644 index 0000000..fd36dc2 --- /dev/null +++ b/libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/SqlTableReference.java @@ -0,0 +1,266 @@ +package com.github.magnusp.libsql.jdbc; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +final class SqlTableReference { + + record TableRef(String schema, String table) {} + + private enum Type { + WORD, + IDENT, + SYMBOL + } + + private record Token(Type type, String text, int depth) {} + + private static final Pattern UNQUOTED_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_$]*"); + + private static final Set REJECTED_KEYWORDS = + Set.of("WITH", "UNION", "INTERSECT", "EXCEPT", "JOIN"); + + private static final Set FROM_END_KEYWORDS = Set.of( + "WHERE", "GROUP", "HAVING", "ORDER", "LIMIT", "OFFSET", "WINDOW", "RETURNING", + "UNION", "INTERSECT", "EXCEPT", "FROM", "JOIN", "CROSS", "INNER", "LEFT", "RIGHT", + "FULL", "NATURAL", "OUTER", "ON", "USING", "INDEXED", "NOT", "AS"); + + private SqlTableReference() {} + + static TableRef parse(String sql) { + if (sql == null || sql.isBlank()) { + return null; + } + List tokens = tokenize(sql); + if (tokens == null) { + return null; + } + + int fromIndex = -1; + int fromCount = 0; + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token.depth() != 0) { + if (fromIndex < 0 + && token.type() == Type.WORD + && (upper(token.text()).equals("SELECT") || upper(token.text()).equals("FROM"))) { + return null; + } + continue; + } + if (token.type() != Type.WORD) { + continue; + } + String keyword = upper(token.text()); + if (REJECTED_KEYWORDS.contains(keyword)) { + return null; + } + if (keyword.equals("FROM")) { + fromCount++; + if (fromIndex < 0) { + fromIndex = i; + } + } + } + if (fromCount != 1) { + return null; + } + + int cursor = fromIndex + 1; + if (cursor >= tokens.size()) { + return null; + } + + Token first = tokens.get(cursor); + String firstName = identifier(first); + if (firstName == null) { + return null; + } + cursor++; + + String schema = null; + String table = firstName; + if (cursor < tokens.size() && isSymbol(tokens.get(cursor), ".")) { + cursor++; + if (cursor >= tokens.size()) { + return null; + } + Token tableToken = tokens.get(cursor); + String tableName = identifier(tableToken); + if (tableName == null) { + return null; + } + schema = firstName; + table = tableName; + cursor++; + if (cursor < tokens.size() && isSymbol(tokens.get(cursor), ".")) { + return null; + } + } + + if (cursor < tokens.size()) { + Token next = tokens.get(cursor); + if (isKeyword(next, "AS")) { + cursor++; + if (cursor >= tokens.size() || identifier(tokens.get(cursor)) == null) { + return null; + } + cursor++; + } else if (next.type() != Type.SYMBOL && !isFromEnd(next)) { + if (identifier(next) == null) { + return null; + } + cursor++; + } + } + + if (cursor >= tokens.size() || isSymbol(tokens.get(cursor), ";") || isFromEnd(tokens.get(cursor))) { + return new TableRef(schema, table); + } + return null; + } + + private static boolean isFromEnd(Token token) { + return token.depth() == 0 && token.type() == Type.WORD + && FROM_END_KEYWORDS.contains(upper(token.text())); + } + + private static boolean isKeyword(Token token, String keyword) { + return token.depth() == 0 && token.type() == Type.WORD && upper(token.text()).equals(keyword); + } + + private static boolean isSymbol(Token token, String symbol) { + return token.depth() == 0 && token.type() == Type.SYMBOL && token.text().equals(symbol); + } + + private static String identifier(Token token) { + if (token.depth() != 0) { + return null; + } + if (token.type() == Type.IDENT) { + return token.text(); + } + if (token.type() == Type.WORD && UNQUOTED_IDENTIFIER.matcher(token.text()).matches()) { + return token.text(); + } + return null; + } + + private static String upper(String value) { + return value.toUpperCase(Locale.ROOT); + } + + private static List tokenize(String sql) { + List tokens = new ArrayList<>(); + int depth = 0; + int i = 0; + while (i < sql.length()) { + char c = sql.charAt(i); + if (Character.isWhitespace(c)) { + i++; + continue; + } + if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') { + i += 2; + while (i < sql.length() && sql.charAt(i) != '\n' && sql.charAt(i) != '\r') { + i++; + } + continue; + } + if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') { + int end = sql.indexOf("*/", i + 2); + if (end < 0) { + return null; + } + i = end + 2; + continue; + } + if (c == '\'') { + i = scanQuoted(sql, i, '\''); + if (i < 0) { + return null; + } + continue; + } + if (c == '"' || c == '`') { + int end = scanQuoted(sql, i, c); + if (end < 0) { + return null; + } + tokens.add(new Token(Type.IDENT, unescape(sql.substring(i + 1, end - 1), c), depth)); + i = end; + continue; + } + if (c == '[') { + int end = sql.indexOf(']', i + 1); + if (end < 0) { + return null; + } + tokens.add(new Token(Type.IDENT, sql.substring(i + 1, end), depth)); + i = end + 1; + continue; + } + if (c == '(') { + tokens.add(new Token(Type.SYMBOL, "(", depth)); + depth++; + i++; + continue; + } + if (c == ')') { + if (depth == 0) { + return null; + } + depth--; + tokens.add(new Token(Type.SYMBOL, ")", depth)); + i++; + continue; + } + if (c == ',' || c == '.' || c == ';') { + tokens.add(new Token(Type.SYMBOL, String.valueOf(c), depth)); + i++; + continue; + } + if (isWordChar(c)) { + int start = i; + while (i < sql.length() && isWordChar(sql.charAt(i))) { + i++; + } + tokens.add(new Token(Type.WORD, sql.substring(start, i), depth)); + continue; + } + tokens.add(new Token(Type.SYMBOL, String.valueOf(c), depth)); + i++; + } + if (depth != 0) { + return null; + } + return tokens; + } + + private static int scanQuoted(String sql, int start, char quote) { + int i = start + 1; + while (i < sql.length()) { + if (sql.charAt(i) == quote) { + if (i + 1 < sql.length() && sql.charAt(i + 1) == quote) { + i += 2; + continue; + } + return i + 1; + } + i++; + } + return -1; + } + + private static String unescape(String content, char quote) { + String doubled = new String(new char[] {quote, quote}); + return content.replace(doubled, String.valueOf(quote)); + } + + private static boolean isWordChar(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$'; + } +} diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java new file mode 100644 index 0000000..fc0a896 --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaDataTest.java @@ -0,0 +1,69 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.Stmt; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import org.junit.jupiter.api.Test; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LibsqlDatabaseMetaDataTest { + + @Test + void getTablesClosesResultSetWhenStatementCloses() throws SQLException { + LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new RecordingConnection(emptyResult())); + + ResultSet resultSet = metadata.getTables(null, null, null, null); + LibsqlStatement statement = (LibsqlStatement) resultSet.getStatement(); + statement.close(); + + assertThat(resultSet.isClosed()).isTrue(); + } + + @Test + void getTablesResultSetClosesItsStatement() throws SQLException { + LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(new RecordingConnection(emptyResult())); + + ResultSet resultSet = metadata.getTables(null, null, null, null); + LibsqlStatement statement = (LibsqlStatement) resultSet.getStatement(); + resultSet.close(); + + assertThat(statement.isClosed()).isTrue(); + } + + @Test + void escapesSingleQuoteInTableNamePattern() throws SQLException { + RecordingConnection connection = new RecordingConnection(emptyResult()); + LibsqlDatabaseMetaData metadata = new LibsqlDatabaseMetaData(connection); + + ResultSet resultSet = metadata.getTables(null, null, "O'Brien", null); + resultSet.close(); + + assertThat(connection.lastSql).contains("name LIKE 'O''Brien'"); + assertThat(connection.lastSql).doesNotContain("name LIKE 'O'Brien'"); + } + + private static StmtResult emptyResult() { + return new StmtResult(List.of(new Col("TABLE_NAME", "TEXT")), List.of(), 0, null); + } + + private static final class RecordingConnection extends LibsqlConnection { + private final StmtResult result; + private String lastSql; + + private RecordingConnection(StmtResult result) { + super(null, null); + this.result = result; + } + + @Override + public StmtResult executeStatement(Stmt stmt) throws SQLException { + lastSql = stmt.sql(); + return result; + } + } +} diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java new file mode 100644 index 0000000..045df65 --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetMetaDataTest.java @@ -0,0 +1,147 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.Row; +import com.github.magnusp.libsql.hrana.codec.Stmt; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import com.github.magnusp.libsql.hrana.codec.Value; +import org.junit.jupiter.api.Test; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LibsqlResultSetMetaDataTest { + + @Test + void resolvesTableNameNullabilityAndAutoIncrement() throws SQLException { + String createSql = "CREATE TABLE People (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)"; + StmtResult tableRows = new StmtResult( + List.of(), + List.of( + new Row(List.of( + Value.of("People"), + Value.of(createSql), + Value.of("id"), + Value.of(0L), + Value.of(1L))), + new Row(List.of( + Value.of("People"), + Value.of(createSql), + Value.of("name"), + Value.of(1L), + Value.of(0L)))), + 0, + null); + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("id", "INTEGER"), new Col("name", "TEXT")), + List.of(), + "SELECT id, name FROM main.People", + connectionReturning(tableRows)); + + assertThat(metadata.getTableName(1)).isEqualTo("People"); + assertThat(metadata.isNullable(1)).isEqualTo(ResultSetMetaData.columnNullable); + assertThat(metadata.isNullable(2)).isEqualTo(ResultSetMetaData.columnNoNulls); + assertThat(metadata.isAutoIncrement(1)).isTrue(); + assertThat(metadata.isAutoIncrement(2)).isFalse(); + } + + @Test + void ignoresAutoIncrementInsideSqlComments() throws SQLException { + String createSql = "CREATE TABLE People (id INTEGER PRIMARY KEY /* AUTOINCREMENT */, name TEXT)"; + StmtResult tableRows = new StmtResult( + List.of(), + List.of(new Row(List.of( + Value.of("People"), + Value.of(createSql), + Value.of("id"), + Value.of(0L), + Value.of(1L)))), + 0, + null); + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("id", "INTEGER")), + List.of(), + "SELECT id FROM main.People", + connectionReturning(tableRows)); + + assertThat(metadata.isAutoIncrement(1)).isFalse(); + } + + @Test + void ignoresAutoIncrementInsideStringLiterals() throws SQLException { + String createSql = "CREATE TABLE People (id INTEGER PRIMARY KEY, note TEXT DEFAULT 'AUTOINCREMENT')"; + StmtResult tableRows = new StmtResult( + List.of(), + List.of(new Row(List.of( + Value.of("People"), + Value.of(createSql), + Value.of("id"), + Value.of(0L), + Value.of(1L)))), + 0, + null); + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("id", "INTEGER")), + List.of(), + "SELECT id FROM main.People", + connectionReturning(tableRows)); + + assertThat(metadata.isAutoIncrement(1)).isFalse(); + } + + @Test + void usesFirstNonNullValueForIntegerTypeAndClass() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("wide", "INTEGER"), new Col("narrow", "INTEGER")), + List.of( + new Row(List.of(Value.ofNull(), Value.ofNull())), + new Row(List.of(Value.of(5_000_000_000L), Value.of(42L)))), + "SELECT 1", + null); + + assertThat(metadata.getColumnClassName(1)).isEqualTo(Long.class.getName()); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT); + assertThat(metadata.getColumnClassName(2)).isEqualTo(Integer.class.getName()); + assertThat(metadata.getColumnType(2)).isEqualTo(Types.INTEGER); + } + + @Test + void usesRuntimeValueMappingForNonIntegerValuesInIntegerColumn() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("value", "INTEGER")), + List.of( + new Row(List.of(Value.ofNull())), + new Row(List.of(Value.of("not a number")))), + "SELECT 1", + null); + + assertThat(metadata.getColumnClassName(1)).isEqualTo(String.class.getName()); + assertThat(metadata.getColumnType(1)).isEqualTo(Types.VARCHAR); + } + + @Test + void leavesAmbiguousTableContextUnknown() throws SQLException { + LibsqlResultSetMetaData metadata = new LibsqlResultSetMetaData( + List.of(new Col("id", "INTEGER")), + List.of(), + "SELECT * FROM a JOIN b ON a.id = b.id", + connectionReturning(new StmtResult(List.of(), List.of(), 0, null))); + + assertThat(metadata.getTableName(1)).isEmpty(); + assertThat(metadata.isNullable(1)).isEqualTo(ResultSetMetaData.columnNullableUnknown); + assertThat(metadata.isAutoIncrement(1)).isFalse(); + } + + private static LibsqlConnection connectionReturning(StmtResult result) { + return new LibsqlConnection(null, null) { + @Override + public StmtResult executeStatement(Stmt stmt) throws SQLException { + return result; + } + }; + } +} diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java new file mode 100644 index 0000000..a6d26c6 --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/LibsqlResultSetTest.java @@ -0,0 +1,84 @@ +package com.github.magnusp.libsql.jdbc; + +import com.github.magnusp.libsql.hrana.codec.Col; +import com.github.magnusp.libsql.hrana.codec.Row; +import com.github.magnusp.libsql.hrana.codec.StmtResult; +import com.github.magnusp.libsql.hrana.codec.Value; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LibsqlResultSetTest { + + private static final StmtResult INTEGER_ROWS = new StmtResult( + List.of(new Col("id", "INTEGER")), + List.of(new Row(List.of(Value.of(42L))), new Row(List.of(Value.of(5_000_000_000L)))), + 0, + null); + + @Test + void getObjectReturnsIntegerInRangeAndLongOutside() throws SQLException { + LibsqlResultSet resultSet = new LibsqlResultSet(new LibsqlStatement(null), INTEGER_ROWS); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getObject(1)).isInstanceOf(Integer.class).isEqualTo(42); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getObject(1)).isInstanceOf(Long.class).isEqualTo(5_000_000_000L); + } + + @Test + void cachesResultSetMetadata() throws SQLException { + LibsqlResultSet resultSet = new LibsqlResultSet(new LibsqlStatement(null), INTEGER_ROWS); + + assertThat(resultSet.getMetaData()).isSameAs(resultSet.getMetaData()); + } + + @Test + void closeDoesNotCloseStatementByDefault() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + LibsqlResultSet resultSet = new LibsqlResultSet(statement, INTEGER_ROWS); + + resultSet.close(); + + assertThat(resultSet.isClosed()).isTrue(); + assertThat(statement.isClosed()).isFalse(); + } + + @Test + void closeClosesStatementWhenRequested() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + LibsqlResultSet resultSet = new LibsqlResultSet(statement, INTEGER_ROWS, null, true); + + resultSet.close(); + + assertThat(resultSet.isClosed()).isTrue(); + assertThat(statement.isClosed()).isTrue(); + } + + @Test + void closeClosesStatementWithCloseOnCompletion() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + statement.closeOnCompletion(); + LibsqlResultSet resultSet = new LibsqlResultSet(statement, INTEGER_ROWS); + + resultSet.close(); + resultSet.close(); + + assertThat(resultSet.isClosed()).isTrue(); + assertThat(statement.isClosed()).isTrue(); + } + + @Test + void statementCloseClosesCurrentResultSet() throws SQLException { + LibsqlStatement statement = new LibsqlStatement(null); + LibsqlResultSet resultSet = new LibsqlResultSet(statement, INTEGER_ROWS); + statement.currentResultSet = resultSet; + + statement.close(); + + assertThat(resultSet.isClosed()).isTrue(); + } +} diff --git a/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/SqlTableReferenceTest.java b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/SqlTableReferenceTest.java new file mode 100644 index 0000000..ee4c45d --- /dev/null +++ b/libsql-jdbc/src/test/java/com/github/magnusp/libsql/jdbc/SqlTableReferenceTest.java @@ -0,0 +1,125 @@ +package com.github.magnusp.libsql.jdbc; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SqlTableReferenceTest { + + @Test + void parsesSimpleTable() { + assertThat(SqlTableReference.parse("SELECT * FROM people")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } + + @Test + void parsesSchemaQualifiedTable() { + assertThat(SqlTableReference.parse("SELECT * FROM main.people")) + .isEqualTo(new SqlTableReference.TableRef("main", "people")); + } + + @Test + void parsesTempSchema() { + assertThat(SqlTableReference.parse("SELECT * FROM temp.people")) + .isEqualTo(new SqlTableReference.TableRef("temp", "people")); + } + + @Test + void parsesQuotedNamesWithSpaces() { + assertThat(SqlTableReference.parse("SELECT * FROM \"my schema\".\"my table\"")) + .isEqualTo(new SqlTableReference.TableRef("my schema", "my table")); + } + + @Test + void parsesQuotedIdentifierKinds() { + assertThat(SqlTableReference.parse("SELECT * FROM `my table`")) + .isEqualTo(new SqlTableReference.TableRef(null, "my table")); + assertThat(SqlTableReference.parse("SELECT * FROM [my table]")) + .isEqualTo(new SqlTableReference.TableRef(null, "my table")); + assertThat(SqlTableReference.parse("SELECT * FROM \"my \"\"table\"\"\"")) + .isEqualTo(new SqlTableReference.TableRef(null, "my \"table\"")); + } + + @Test + void parsesAliasesWithAndWithoutAs() { + assertThat(SqlTableReference.parse("SELECT * FROM people p WHERE p.id = 1")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + assertThat(SqlTableReference.parse("SELECT * FROM people AS p")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } + + @Test + void ignoresStringLiteralsContainingFrom() { + assertThat(SqlTableReference.parse("SELECT 'from elsewhere' AS s FROM people WHERE name = 'from x'")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } + + @Test + void ignoresCommentsContainingFrom() { + assertThat(SqlTableReference.parse( + "SELECT * -- from noise\nFROM /* from noise */ people -- trailing from noise")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } + + @Test + void ignoresFromInNestedSubquery() { + assertThat(SqlTableReference.parse("SELECT * FROM people WHERE id IN (SELECT id FROM other)")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } + + @Test + void rejectsJoin() { + assertThat(SqlTableReference.parse("SELECT * FROM a JOIN b ON a.id = b.id")).isNull(); + } + + @Test + void rejectsCommaSeparatedTables() { + assertThat(SqlTableReference.parse("SELECT * FROM a, b")).isNull(); + } + + @Test + void rejectsCommonTableExpression() { + assertThat(SqlTableReference.parse("WITH cte AS (SELECT * FROM a) SELECT * FROM cte")).isNull(); + } + + @Test + void rejectsCompoundSelect() { + assertThat(SqlTableReference.parse("SELECT * FROM a UNION SELECT * FROM b")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a INTERSECT SELECT * FROM b")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a EXCEPT SELECT * FROM b")).isNull(); + } + + @Test + void rejectsSubqueryInFromPosition() { + assertThat(SqlTableReference.parse("SELECT * FROM (SELECT * FROM a) AS x")).isNull(); + } + + @Test + void rejectsSubqueryInSelectList() { + assertThat(SqlTableReference.parse("SELECT (SELECT id FROM other) AS id FROM people")) + .isNull(); + } + + @Test + void rejectsSecondTableReferenceOrSecondFrom() { + assertThat(SqlTableReference.parse("SELECT * FROM a b c")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a FROM b")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a AS x, b")).isNull(); + } + + @Test + void rejectsAmbiguousInput() { + assertThat(SqlTableReference.parse(null)).isNull(); + assertThat(SqlTableReference.parse("SELECT 1")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM 'people'")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a.")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a.b.c")).isNull(); + assertThat(SqlTableReference.parse("SELECT * FROM a WHERE id IN (1, 2")).isNull(); + } + + @Test + void acceptsTrailingSemicolon() { + assertThat(SqlTableReference.parse("SELECT * FROM people;")) + .isEqualTo(new SqlTableReference.TableRef(null, "people")); + } +} diff --git a/pom.xml b/pom.xml index 4cdf0a7..3967bea 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ libsql-client libsql-jdbc libsql-itests + libsql-conformance diff --git a/tools/export-upstream b/tools/export-upstream new file mode 100755 index 0000000..390059e --- /dev/null +++ b/tools/export-upstream @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: export-upstream [options] + +Export xerial/sqlite-jdbc at the given tag into upstream-xerial_sqlite-jdbc/ +and update upstream-xerial_sqlite-jdbc/UPSTREAM.md. The tag is resolved to its +commit SHA with the GitHub API, and both the tag and the SHA are recorded. + +Requires the GitHub CLI, authenticated with `gh auth login`. + +Examples: + export-upstream 3.53.4.0 + export-upstream 3.53.4.0 --manifest upstream.manifest + +Options: + --manifest FILE Repo-relative paths to export, one per line. + Blank lines and lines starting with # are ignored. + --all Export the whole repository instead of the default slice. + --force Replace a non-empty destination not created by this script. + -h, --help Show this help. + +Default export slice when no manifest is given and --all is not used: + src/test/java + src/test/resources + LICENSE + LICENSE.zentus + NOTICE + VERSION + +The destination must be empty, missing, or marked with .export-upstream. +The script never edits UPSTREAM.md content outside its managed markers. +EOF +} + +die() { + echo "error: $*" >&2 + exit 1 +} + +md_cell() { + printf '%s' "$1" | sed 's/|/\\|/g' +} + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repo_root" + +repo="xerial/sqlite-jdbc" +dest="upstream-xerial_sqlite-jdbc" +upstream_md="$dest/UPSTREAM.md" +manifest="" +manifest_set=0 +export_all=0 +force=0 + +if [ "$#" -lt 1 ]; then + usage + exit 2 +fi + +case "$1" in + -h|--help) + usage + exit 0 + ;; + -*) + usage + exit 2 + ;; +esac + +tag="$1" +shift + +while [ "$#" -gt 0 ]; do + case "$1" in + --manifest) + [ "$#" -ge 2 ] || die "--manifest needs a value" + manifest="$2" + manifest_set=1 + shift 2 + ;; + --all) + export_all=1 + shift + ;; + --force) + force=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac +done + +if [ "$export_all" -eq 1 ] && [ "$manifest_set" -eq 1 ]; then + die "--all and --manifest cannot be used together" +fi + +printf '%s' "$tag" | grep -Eq '^[A-Za-z0-9._+-]+$' \ + || die "tag contains unsupported characters: $tag" + +if [ -e "$dest" ] && [ ! -d "$dest" ]; then + die "destination is not a directory: $dest" +fi + +command -v gh >/dev/null 2>&1 || die "gh is required" +gh auth status >/dev/null 2>&1 || die "gh is not authenticated; run 'gh auth login'" + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +old_md="" +if [ -f "$upstream_md" ]; then + old_md="$tmp/UPSTREAM.md.old" + cp "$upstream_md" "$old_md" +fi + +metadata=$(gh api "repos/$repo/commits/$tag" \ + --jq '[.sha, .commit.committer.date, .commit.author.name, .commit.author.email, (.commit.message | split("\n")[0])] | @tsv') \ + || die "could not resolve tag $tag in $repo" +IFS=$'\t' read -r sha commit_date author_name author_email commit_subject <<< "$metadata" +commit_author="$author_name <$author_email>" + +mkdir -p "$tmp/src" +if ! gh api "repos/$repo/tarball/$sha" \ + | tar -xzf - -C "$tmp/src" --strip-components=1; then + die "could not download or extract tag $tag from $repo" +fi + +if [ "$export_all" -eq 1 ]; then + paths_label="whole repository" +else + paths=() + if [ -n "$manifest" ]; then + [ -f "$manifest" ] || die "manifest not found: $manifest" + while IFS= read -r line || [ -n "$line" ]; do + line=${line%$'\r'} + case "$line" in + ""|"#"*) + continue + ;; + /*|..|../*|*/../*|*/..) + die "manifest path must be relative and stay inside the repository: $line" + ;; + esac + paths+=("$line") + done < "$manifest" + else + paths=(src/test/java src/test/resources LICENSE LICENSE.zentus NOTICE VERSION) + fi + + [ "${#paths[@]}" -gt 0 ] || die "manifest is empty: $manifest" + + for path in "${paths[@]}"; do + [ -e "$tmp/src/$path" ] || die "path not found in tag $tag: $path" + done + + paths_label=$(printf '%s, ' "${paths[@]}") + paths_label=${paths_label%, } +fi + +if [ -e "$dest" ] && [ -n "$(ls -A "$dest" 2>/dev/null)" ]; then + if [ ! -f "$dest/.export-upstream" ] && [ "$force" -ne 1 ]; then + die "destination is not empty and was not created by export-upstream: $dest (pass --force)" + fi + rm -rf -- "$dest" +fi + +mkdir -p "$dest" +if [ "$export_all" -eq 1 ]; then + cp -a "$tmp/src/." "$dest/" +else + for path in "${paths[@]}"; do + mkdir -p "$dest/$(dirname "$path")" + cp -a "$tmp/src/$path" "$dest/$path" + done +fi + +{ + printf 'tag=%s\n' "$tag" + printf 'sha=%s\n' "$sha" +} > "$dest/.export-upstream" + +block="$tmp/upstream-block.md" +{ + echo '' + echo '# Upstream export' + echo + echo '| Field | Value |' + echo '|---|---|' + printf '| Repository | `https://github.com/%s` |\n' "$(md_cell "$repo")" + printf '| Tag | %s |\n' "$(md_cell "$tag")" + printf '| Commit | `%s` |\n' "$sha" + printf '| Commit date | %s |\n' "$(md_cell "$commit_date")" + printf '| Commit author | %s |\n' "$(md_cell "$commit_author")" + printf '| Commit subject | %s |\n' "$(md_cell "$commit_subject")" + printf '| Paths | %s |\n' "$(md_cell "$paths_label")" + echo '| License | Apache-2.0 and Zentus BSD. See `LICENSE`, `LICENSE.zentus`, and `NOTICE`. |' + echo + echo 'The exported upstream files are byte-identical copies at this commit and must not be edited. `export-upstream` regenerates this tree.' + echo '' +} > "$block" + +mkdir -p "$(dirname "$upstream_md")" +if [ -n "$old_md" ] && [ -f "$old_md" ]; then + if grep -q '' "$old_md"; then + awk -v blockfile="$block" ' + // { + while ((getline line < blockfile) > 0) print line + close(blockfile) + skip = 1 + next + } + // { + skip = 0 + next + } + !skip { + print + } + ' "$old_md" > "$upstream_md.tmp" + else + cat "$old_md" > "$upstream_md.tmp" + printf '\n' >> "$upstream_md.tmp" + cat "$block" >> "$upstream_md.tmp" + fi +else + cat "$block" > "$upstream_md.tmp" +fi +mv "$upstream_md.tmp" "$upstream_md" + +if command -v sha256sum >/dev/null 2>&1; then + ( + cd "$dest" + find . -type f \ + ! -name CHECKSUMS.sha256 \ + ! -name .export-upstream \ + ! -name UPSTREAM.md \ + -print0 \ + | sort -z \ + | xargs -0 -r sha256sum > CHECKSUMS.sha256 + ) +fi + +echo "Exported tag $tag ($sha) to $dest" +echo "Updated $upstream_md" diff --git a/upstream-xerial_sqlite-jdbc.manifest b/upstream-xerial_sqlite-jdbc.manifest new file mode 100644 index 0000000..2381609 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc.manifest @@ -0,0 +1,19 @@ +# First conformance batch. +src/test/java/org/sqlite/Utils.java +src/test/java/org/sqlite/ResultSetTest.java +src/test/java/org/sqlite/InsertQueryTest.java +src/test/java/org/sqlite/FetchSizeTest.java +src/test/java/org/sqlite/TypeMapTest.java +src/test/java/org/sqlite/CachedRowSetTest.java + +# Second batch: metadata, JSON, threading, and empty result sets. +src/test/java/org/sqlite/JSON1Test.java +src/test/java/org/sqlite/MetadataLeakTest.java +src/test/java/org/sqlite/PreparedStatementThreadTest.java +src/test/java/org/sqlite/ResultSetWithoutResultsTest.java +src/test/java/org/sqlite/RSMetaDataTest.java + +LICENSE +LICENSE.zentus +NOTICE +VERSION diff --git a/upstream-xerial_sqlite-jdbc/.export-upstream b/upstream-xerial_sqlite-jdbc/.export-upstream new file mode 100644 index 0000000..91c3006 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/.export-upstream @@ -0,0 +1,2 @@ +tag=3.53.4.0 +sha=cab7981c19ce04d691f0675f0b2586afc2bbf803 diff --git a/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 b/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 new file mode 100644 index 0000000..0240387 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 @@ -0,0 +1,15 @@ +3ddf9be5c28fe27dad143a5dc76eea25222ad1dd68934a047064e56ed2fa40c5 ./LICENSE +89167dab92289c7e5e2b65b044f0856b703d05e5d5e35c3548e73d9c7d2f5048 ./LICENSE.zentus +a0670cf020edf700524c43c85321c2ba027a8bace5682001db60875d4a9fe52b ./NOTICE +1351fdca2644e90ac9028ed99c6096c9ecee82ccb7d45cedd9bb454887fd2c6b ./src/test/java/org/sqlite/CachedRowSetTest.java +9fb0f2df7de66cac0a46b241c0c1cd3311a80646d0431d377097741b99053760 ./src/test/java/org/sqlite/FetchSizeTest.java +6dfcecb209e01829ba0aea782b47351443e3970ad1d33ac4c3fb979e6dff1f40 ./src/test/java/org/sqlite/InsertQueryTest.java +bb07398946480ca44c70d03e6f8c399f53506b44c83a312e2d28aebd239c0238 ./src/test/java/org/sqlite/JSON1Test.java +242bb51330240174bab5f443aa4d8de1049e6d947374e080a636c4aa4d0049ee ./src/test/java/org/sqlite/MetadataLeakTest.java +7af8ae0999a0bf26a20c96ab23131505bd79827e6af17742771e36e9d8d985f4 ./src/test/java/org/sqlite/PreparedStatementThreadTest.java +6065d284933c8462cc13217992820a9261075c0e9ee126df6cbe9961d90303f9 ./src/test/java/org/sqlite/ResultSetTest.java +958c1f2ff5ca2f1257dfd847207ea793839969b234d140576b51af346697233c ./src/test/java/org/sqlite/ResultSetWithoutResultsTest.java +d787e71bdf2b8d12a2669eb06c432cf2a39ac77f678a92459e3dd9b6c4519a43 ./src/test/java/org/sqlite/RSMetaDataTest.java +ed2fb4bf7319bcba0662b83707375501337173956bae7c57dda6e6a2c46b7505 ./src/test/java/org/sqlite/TypeMapTest.java +bef3eddca2a6b80472050a3a52d5e7246128f0644d29c24ed091709b0ed6f121 ./src/test/java/org/sqlite/Utils.java +0a60829ce6edd56de46fb87303401c5c1e1e666da5631056340f5c5ce902dcdf ./VERSION diff --git a/upstream-xerial_sqlite-jdbc/LICENSE b/upstream-xerial_sqlite-jdbc/LICENSE new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/upstream-xerial_sqlite-jdbc/LICENSE.zentus b/upstream-xerial_sqlite-jdbc/LICENSE.zentus new file mode 100644 index 0000000..3e044ab --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/LICENSE.zentus @@ -0,0 +1,24 @@ +Copyright (c) 2006, David Crawshaw. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + diff --git a/upstream-xerial_sqlite-jdbc/NOTICE b/upstream-xerial_sqlite-jdbc/NOTICE new file mode 100644 index 0000000..5b1228a --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/NOTICE @@ -0,0 +1,4 @@ +This product includes the following softwares developed by David Crawshaw. +See LICENSE.zentus file. + +And also, NestedVM (Apache License Version 2.0) is used inside sqlite- \ No newline at end of file diff --git a/upstream-xerial_sqlite-jdbc/UPSTREAM.md b/upstream-xerial_sqlite-jdbc/UPSTREAM.md new file mode 100644 index 0000000..7ad88f4 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/UPSTREAM.md @@ -0,0 +1,16 @@ + +# Upstream export + +| Field | Value | +|---|---| +| Repository | `https://github.com/xerial/sqlite-jdbc` | +| Tag | 3.53.4.0 | +| Commit | `cab7981c19ce04d691f0675f0b2586afc2bbf803` | +| Commit date | 2026-08-26T09:55:34Z | +| Commit author | github-actions <41898282+github-actions[bot]@users.noreply.github.com> | +| Commit subject | chore(release): 3.53.4.0 [skip ci] | +| Paths | src/test/java/org/sqlite/Utils.java, src/test/java/org/sqlite/ResultSetTest.java, src/test/java/org/sqlite/InsertQueryTest.java, src/test/java/org/sqlite/FetchSizeTest.java, src/test/java/org/sqlite/TypeMapTest.java, src/test/java/org/sqlite/CachedRowSetTest.java, src/test/java/org/sqlite/JSON1Test.java, src/test/java/org/sqlite/MetadataLeakTest.java, src/test/java/org/sqlite/PreparedStatementThreadTest.java, src/test/java/org/sqlite/ResultSetWithoutResultsTest.java, src/test/java/org/sqlite/RSMetaDataTest.java, LICENSE, LICENSE.zentus, NOTICE, VERSION | +| License | Apache-2.0 and Zentus BSD. See `LICENSE`, `LICENSE.zentus`, and `NOTICE`. | + +The exported upstream files are byte-identical copies at this commit and must not be edited. `export-upstream` regenerates this tree. + diff --git a/upstream-xerial_sqlite-jdbc/VERSION b/upstream-xerial_sqlite-jdbc/VERSION new file mode 100644 index 0000000..9975a18 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/VERSION @@ -0,0 +1 @@ +version=3.53.4 diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java new file mode 100644 index 0000000..c879bc8 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java @@ -0,0 +1,27 @@ +package org.sqlite; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import javax.sql.rowset.CachedRowSet; +import javax.sql.rowset.RowSetFactory; +import javax.sql.rowset.RowSetProvider; +import org.junit.jupiter.api.Test; + +public class CachedRowSetTest { + + @Test + public void gh_224() throws SQLException { + Connection connection = DriverManager.getConnection("jdbc:sqlite:"); + try (Statement statement = connection.createStatement()) { + statement.execute("create table person (id INTEGER, name VARCHAR(50))"); + statement.execute("insert into person values(1, 'leo')"); + } + RowSetFactory factory = RowSetProvider.newFactory(); + try (CachedRowSet crs = factory.createCachedRowSet()) { + crs.setCommand("select * from person"); + crs.execute(connection); + } + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/FetchSizeTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/FetchSizeTest.java new file mode 100644 index 0000000..f242993 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/FetchSizeTest.java @@ -0,0 +1,58 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Created by IntelliJ IDEA. User: david_donn Date: 19/01/2010 Time: 11:50:24 AM To change this + * template use File | Settings | File Templates. + */ +public class FetchSizeTest { + + private Connection conn; + + @BeforeEach + public void connect() throws Exception { + conn = DriverManager.getConnection("jdbc:sqlite:"); + } + + @AfterEach + public void close() throws SQLException { + conn.close(); + } + + @Test + public void testFetchSize() throws SQLException { + assertThat(conn.prepareStatement("create table s1 (c1)").executeUpdate()).isEqualTo(0); + PreparedStatement insertPrep = conn.prepareStatement("insert into s1 values (?)"); + insertPrep.setInt(1, 1); + assertThat(insertPrep.executeUpdate()).isEqualTo(1); + insertPrep.setInt(1, 2); + assertThat(insertPrep.executeUpdate()).isEqualTo(1); + insertPrep.setInt(1, 3); + assertThat(insertPrep.executeUpdate()).isEqualTo(1); + insertPrep.setInt(1, 4); + assertThat(insertPrep.executeUpdate()).isEqualTo(1); + insertPrep.setInt(1, 5); + assertThat(insertPrep.executeUpdate()).isEqualTo(1); + insertPrep.close(); + + PreparedStatement selectPrep = conn.prepareStatement("select c1 from s1"); + ResultSet rs = selectPrep.executeQuery(); + rs.setFetchSize(2); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isFalse(); + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/InsertQueryTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/InsertQueryTest.java new file mode 100644 index 0000000..a4b2ade --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/InsertQueryTest.java @@ -0,0 +1,203 @@ +// -------------------------------------- +// sqlite-jdbc Project +// +// InsertQueryTest.java +// Since: Apr 7, 2009 +// +// $URL$ +// $Author$ +// -------------------------------------- +package org.sqlite; + +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class InsertQueryTest { + String dbName; + + @BeforeEach + public void setUp(@TempDir File tempDir) throws Exception { + File tmpFile = File.createTempFile("tmp-sqlite", ".db", tempDir); + dbName = tmpFile.getAbsolutePath(); + } + + @AfterEach + public void tearDown() throws Exception {} + + interface ConnectionFactory { + Connection getConnection() throws SQLException; + + void dispose() throws SQLException; + } + + class IndependentConnectionFactory implements ConnectionFactory { + // Internal bookkeeping so connections are still closed in the end. + private final List connections = new ArrayList<>(); + + public Connection getConnection() throws SQLException { + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbName); + connections.add(conn); + return conn; + } + + public void dispose() throws SQLException { + for (Connection connection : connections) { + connection.close(); + } + connections.clear(); + } + } + + class SharedConnectionFactory implements ConnectionFactory { + private Connection conn = null; + + public Connection getConnection() throws SQLException { + if (conn == null) { + conn = DriverManager.getConnection("jdbc:sqlite:" + dbName); + } + return conn; + } + + public void dispose() throws SQLException { + if (conn != null) { + conn.close(); + } + } + } + + static class BD { + String fullId; + String type; + + public BD(String fullId, String type) { + this.fullId = fullId; + this.type = type; + } + + public String getFullId() { + return fullId; + } + + public void setFullId(String fullId) { + this.fullId = fullId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public static byte[] serializeBD(BD item) { + return new byte[0]; + } + } + + @Test + public void insertLockTestUsingSharedConnection() throws Exception { + insertAndQuery(new SharedConnectionFactory()); + } + + @Test + public void insertLockTestUsingIndependentConnection() throws Exception { + insertAndQuery(new IndependentConnectionFactory()); + } + + void insertAndQuery(ConnectionFactory factory) throws SQLException { + try { + Statement st = factory.getConnection().createStatement(); + st.executeUpdate( + "CREATE TABLE IF NOT EXISTS data (fid VARCHAR(255) PRIMARY KEY, type VARCHAR(64), data BLOB);"); + st.executeUpdate( + "CREATE TABLE IF NOT EXISTS ResourcesTags (bd_fid VARCHAR(255), name VARCHAR(64), version INTEGER);"); + st.close(); + + factory.getConnection().setAutoCommit(false); + + // Object Serialization + PreparedStatement statAddBD = + factory.getConnection() + .prepareStatement("INSERT OR REPLACE INTO data values (?, ?, ?)"); + PreparedStatement statDelRT = + factory.getConnection() + .prepareStatement("DELETE FROM ResourcesTags WHERE bd_fid = ?"); + PreparedStatement statAddRT = + factory.getConnection() + .prepareStatement("INSERT INTO ResourcesTags values (?, ?, ?)"); + + for (int i = 0; i < 10; i++) { + BD item = new BD(Integer.toHexString(i), Integer.toString(i)); + + // SQLite database insertion + statAddBD.setString(1, item.getFullId()); + statAddBD.setString(2, item.getType()); + statAddBD.setBytes(3, BD.serializeBD(item)); + statAddBD.execute(); + + // Then, its resources tags + statDelRT.setString(1, item.getFullId()); + statDelRT.execute(); + + statAddRT.setString(1, item.getFullId()); + + for (int j = 0; j < 2; j++) { + statAddRT.setString(2, "1"); + statAddRT.setLong(3, 1L); + statAddRT.execute(); + } + } + + factory.getConnection().setAutoCommit(true); + + statAddBD.close(); + statDelRT.close(); + statAddRT.close(); + + // + PreparedStatement stat; + Long result = 0L; + String query = "SELECT COUNT(fid) FROM data"; + + stat = factory.getConnection().prepareStatement(query); + ResultSet rs = stat.executeQuery(); + + rs.next(); + result = rs.getLong(1); + // System.out.println("count = " + result); + + rs.close(); + stat.close(); + } finally { + factory.dispose(); + } + } + + @Disabled("Not sure this worked recently, the second query cannot find the table 'sample'") + @Test + public void reproduceDatabaseLocked() throws SQLException { + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbName); + Connection conn2 = DriverManager.getConnection("jdbc:sqlite:" + dbName); + Statement stat = conn.createStatement(); + Statement stat2 = conn2.createStatement(); + + conn.setAutoCommit(false); + + stat.executeUpdate("drop table if exists sample"); + stat.executeUpdate("create table sample(id, name)"); + stat.executeUpdate("insert into sample values(1, 'leo')"); + + ResultSet rs = stat2.executeQuery("select count(*) from sample"); + rs.next(); + + conn.commit(); // causes "database is locked" (SQLITE_BUSY) + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java new file mode 100644 index 0000000..4bcd234 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java @@ -0,0 +1,719 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.junit.jupiter.api.Test; + +/** + * Tests the JSON1 extension using the examples listed in the documentation. + * + * @see https://www.sqlite.org/json1.html + */ +public class JSON1Test { + + public Connection getConnection() throws SQLException { + return DriverManager.getConnection("jdbc:sqlite::memory:"); + } + + @Test + public void json_Test() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json(' { \"this\" : \"is\", \"a\": [ \"test\" ] } ')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"this\":\"is\",\"a\":[\"test\"]}"); + } + + @Test + public void json_object_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement().executeQuery("select json_object('ex','[52,3.14159]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"ex\":\"[52,3.14159]\"}"); + } + + @Test + public void json_object_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_object('ex',json('[52,3.14159]'))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"ex\":[52,3.14159]}"); + } + + @Test + public void json_object_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_object('ex',json_array(52,3.14159))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"ex\":[52,3.14159]}"); + } + + @Test + public void json_object_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_object('a',2,'c',4)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":4}"); + } + + @Test + public void json_object_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_object('a',2,'c','{e:5}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":\"{e:5}\"}"); + } + + @Test + public void json_object_Test6() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_object('a',2,'c',json_object('e',5))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":{\"e\":5}}"); + } + + @Test + public void json_array_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_array(1,2,'3',4)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[1,2,\"3\",4]"); + } + + @Test + public void json_array_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_array('[1,2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[\"[1,2]\"]"); + } + + @Test + public void json_array_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_array(json_array(1,2))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[[1,2]]"); + } + + @Test + public void json_array_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_array(1,null,'3','[4,5]','{\"six\":7.7}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[1,null,\"3\",\"[4,5]\",\"{\\\"six\\\":7.7}\"]"); + } + + @Test + public void json_array_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_array(1,null,'3',json('[4,5]'),json('{\"six\":7.7}'))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[1,null,\"3\",[4,5],{\"six\":7.7}]"); + } + + @Test + public void json_array_length_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_array_length('[1,2,3,4]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(4); + } + + @Test + public void json_array_length_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement().executeQuery("select json_array_length('[1,2,3,4]', '$')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(4); + } + + @Test + public void json_array_length_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_array_length('[1,2,3,4]', '$[2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(0); + } + + @Test + public void json_array_length_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_array_length('{\"one\":[1,2,3]}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(0); + } + + @Test + public void json_array_length_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_array_length('{\"one\":[1,2,3]}', '$.one')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(3); + } + + @Test + public void json_array_length_Test6() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_array_length('{\"one\":[1,2,3]}', '$.two')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getObject(1)).isEqualTo(null); + } + + @Test + public void json_extract_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":[4,5,{\"f\":7}]}"); + } + + @Test + public void json_extract_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$.c')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[4,5,{\"f\":7}]"); + } + + @Test + public void json_extract_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$.c[2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"f\":7}"); + } + + @Test + public void json_extract_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$.c[2].f')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(7); + } + + @Test + public void json_extract_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5],\"f\":7}','$.c','$.a')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[[4,5],2]"); + } + + @Test + public void json_extract_Test6() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$.x')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo(null); + } + + @Test + public void json_extract_Test7() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_extract('{\"a\":2,\"c\":[4,5,{\"f\":7}]}', '$.x', '$.a')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[null,2]"); + } + + @Test + public void json_insert_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_insert('{\"a\":2,\"c\":4}', '$.a', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":4}"); + } + + @Test + public void json_insert_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_insert('{\"a\":2,\"c\":4}', '$.e', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":4,\"e\":99}"); + } + + @Test + public void json_replace_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_replace('{\"a\":2,\"c\":4}', '$.a', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":99,\"c\":4}"); + } + + @Test + public void json_replace_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_replace('{\"a\":2,\"c\":4}', '$.e', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":4}"); + } + + @Test + public void json_set_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_set('{\"a\":2,\"c\":4}', '$.a', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":99,\"c\":4}"); + } + + @Test + public void json_set_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_set('{\"a\":2,\"c\":4}', '$.e', 99)"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":4,\"e\":99}"); + } + + @Test + public void json_set_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_set('{\"a\":2,\"c\":4}', '$.c', '[97,96]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":\"[97,96]\"}"); + } + + @Test + public void json_set_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_set('{\"a\":2,\"c\":4}', '$.c', json('[97,96]'))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":[97,96]}"); + } + + @Test + public void json_set_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_set('{\"a\":2,\"c\":4}', '$.c', json_array(97,96))"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"a\":2,\"c\":[97,96]}"); + } + + @Test + public void json_remove_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement().executeQuery("select json_remove('[0,1,2,3,4]','$[2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[0,1,3,4]"); + } + + @Test + public void json_remove_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_remove('[0,1,2,3,4]','$[2]','$[0]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[1,3,4]"); + } + + @Test + public void json_remove_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_remove('[0,1,2,3,4]','$[0]','$[2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("[1,2,4]"); + } + + @Test + public void json_remove_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement().executeQuery("select json_remove('{\"x\":25,\"y\":42}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"x\":25,\"y\":42}"); + } + + @Test + public void json_remove_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_remove('{\"x\":25,\"y\":42}','$.z')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"x\":25,\"y\":42}"); + } + + @Test + public void json_remove_Test6() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_remove('{\"x\":25,\"y\":42}','$.y')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("{\"x\":25}"); + } + + @Test + public void json_remove_Test7() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_remove('{\"x\":25,\"y\":42}','$')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo(null); + } + + @Test + public void json_type_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery("select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("object"); + } + + @Test + public void json_type_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("object"); + } + + @Test + public void json_type_Test3() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("array"); + } + + @Test + public void json_type_Test4() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[0]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("integer"); + } + + @Test + public void json_type_Test5() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[1]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("real"); + } + + @Test + public void json_type_Test6() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[2]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("true"); + } + + @Test + public void json_type_Test7() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[3]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("false"); + } + + @Test + public void json_type_Test8() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[4]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("null"); + } + + @Test + public void json_type_Test9() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[5]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("text"); + } + + @Test + public void json_type_Test10() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = + conn.createStatement() + .executeQuery( + "select json_type('{\"a\":[2,3.5,true,false,null,\"x\"]}','$.a[6]')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo(null); + } + + @Test + public void json_valid_Test1() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_valid('{\"x\":35}')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + @Test + public void json_valid_Test2() throws SQLException { + Connection conn = getConnection(); + + ResultSet rs = conn.createStatement().executeQuery("select json_valid('{\"x\":35')"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(0); + } + + @Test + public void json_each_Test1() throws SQLException { + Connection conn = getConnection(); + + conn.createStatement().execute("create table user (name, phone)"); + + conn.createStatement() + .execute( + "insert into user values('james', json_array('704-100-0000','604-100-0000'))"); + conn.createStatement() + .execute( + "insert into user values('sally', json_array('604-200-0000','404-200-0000'))"); + conn.createStatement() + .execute( + "insert into user values('frank', json_array('704-200-0000','604-200-0000'))"); + conn.createStatement() + .execute( + "insert into user values('harry', json_array('504-200-0000','304-200-0000'))"); + + String q = + "SELECT DISTINCT user.name" + + " FROM user, json_each(user.phone)" + + " where json_each.value LIKE '704-%'"; + + ResultSet rs = conn.createStatement().executeQuery(q); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("james"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("frank"); + assertThat(rs.next()).isFalse(); + } + + @Test + public void json_each_Test2() throws SQLException { + Connection conn = getConnection(); + + conn.createStatement().execute("create table user (name, phone)"); + + conn.createStatement() + .execute( + "insert into user values('james', json_array('704-100-0000','604-100-0000'))"); + conn.createStatement().execute("insert into user values('sally', '604-200-0000')"); + conn.createStatement().execute("insert into user values('frank', '704-200-0000')"); + conn.createStatement() + .execute( + "insert into user values('harry', json_array('504-200-0000','304-200-0000'))"); + + String q = + "SELECT name FROM user WHERE phone LIKE '705-%'" + + " UNION" + + " SELECT user.name" + + " FROM user, json_each(user.phone)" + + " WHERE json_valid(user.phone)" + + " AND json_each.value LIKE '704-%'"; + + ResultSet rs = conn.createStatement().executeQuery(q); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("james"); + assertThat(rs.next()).isFalse(); + } + + @Test + public void json_tree_Test1() throws SQLException { + Connection conn = getConnection(); + + conn.createStatement().execute("create table big (json JSON)"); + + conn.createStatement().execute("insert into big values(json_object('a',2,'c',4))"); + + String q = + "SELECT big.rowid, fullkey, value" + + " FROM big, json_tree(big.json)" + + " WHERE json_tree.type NOT IN ('object', 'array')"; + + ResultSet rs = conn.createStatement().executeQuery(q); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getString(2)).isEqualTo("$.a"); + assertThat(rs.getInt(3)).isEqualTo(2); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getString(2)).isEqualTo("$.c"); + assertThat(rs.getInt(3)).isEqualTo(4); + + assertThat(rs.next()).isFalse(); + } + + @Test + public void json_tree_Test2() throws SQLException { + Connection conn = getConnection(); + + conn.createStatement().execute("create table big (json JSON)"); + + conn.createStatement().execute("insert into big values(json_object('a',2,'c',4))"); + + String q = + "SELECT big.rowid, fullkey, atom" + + " FROM big, json_tree(big.json)" + + " WHERE atom IS NOT NULL"; + + ResultSet rs = conn.createStatement().executeQuery(q); + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getString(2)).isEqualTo("$.a"); + assertThat(rs.getInt(3)).isEqualTo(2); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getString(2)).isEqualTo("$.c"); + assertThat(rs.getInt(3)).isEqualTo(4); + + assertThat(rs.next()).isFalse(); + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/MetadataLeakTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/MetadataLeakTest.java new file mode 100644 index 0000000..29c0e9c --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/MetadataLeakTest.java @@ -0,0 +1,33 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import org.junit.jupiter.api.Test; + +/** A test to make sure that getMetadata is properly freed */ +public class MetadataLeakTest { + /** + * Check that statement is properly closed when result set is closed. See + * https://github.com/xerial/sqlite-jdbc/issues/423 + * + * @throws Exception on failure + */ + @Test + void testMemoryIsFreed() throws Exception { + try (Connection con = DriverManager.getConnection("jdbc:sqlite::memory:")) { + DatabaseMetaData meta = con.getMetaData(); + + Statement statement; + try (ResultSet tables = meta.getTables(null, null, null, null); ) { + statement = tables.getStatement(); + assertThat(statement.isClosed()).isFalse(); + } + assertThat(statement.isClosed()).isTrue(); + } + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/PreparedStatementThreadTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/PreparedStatementThreadTest.java new file mode 100644 index 0000000..69685b0 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/PreparedStatementThreadTest.java @@ -0,0 +1,122 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests to ensure that incorrect usage of prepared statements does not result in a java + * segmentation fault + */ +public class PreparedStatementThreadTest { + private static ExecutorService executorService; + private Connection conn; + private PreparedStatement stat; + + @BeforeAll + static void beforeAll() { + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + static void afterAll() { + executorService.shutdownNow(); + } + + /** + * Tests to make sure that if one thread is uses a PreparedStatement which another thread is + * closing, that the application throws an exception rather than crashing due to undefined C + * behavior + */ + @Test + public void testPreparedStmtConcurrentCloseSegFault() throws SQLException { + connect(); + try { + for (int i = 0; i < 100; i++) { + testRace(executorService, () -> stat.close()); + } + } finally { + close(); + } + } + + /** + * Tests to make sure that if one thread is uses a PreparedStatement which another thread closes + * the underlying connection to, the application throws an exception rather than crashing due to + * undefined C behavior + */ + @Test + public void testPreparedStmtConcurrentCloseConnSegFault() throws SQLException { + for (int i = 0; i < 100; i++) { + connect(); + testRace(executorService, conn::close); + close(); + } + } + + public void connect() throws SQLException { + conn = DriverManager.getConnection("jdbc:sqlite:"); + } + + public void close() throws SQLException { + conn.close(); + } + + private void testRace(ExecutorService executorService, CloseCallback closeCallback) + throws SQLException { + AtomicInteger countdown = new AtomicInteger(2); + stat = conn.prepareStatement("select 1,2,3,4,5"); + + Future queryThread = + executorService.submit( + () -> { + waitFor(countdown); + for (int i = 0; i < 100; i++) { + ResultSet set = stat.executeQuery(); + assertThat(set.next()).isTrue(); + for (int j = 1; j <= 5; j++) assertThat(set.getInt(j)).isEqualTo(j); + } + return true; + }); + Future closeThread = + executorService.submit( + () -> { + waitFor(countdown); + closeCallback.close(); + return true; + }); + try { + assertThat(queryThread.get()).isTrue(); + assertThat(closeThread.get()).isTrue(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + fail("", e); + } catch (ExecutionException e) { + // this is OK, so long as we don't seg fault + } + } + + private void waitFor(AtomicInteger countdown) { + countdown.decrementAndGet(); + while (0 < countdown.get()) + ; + } + + @FunctionalInterface + interface CloseCallback { + void close() throws SQLException; + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/RSMetaDataTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/RSMetaDataTest.java new file mode 100644 index 0000000..391b5d2 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/RSMetaDataTest.java @@ -0,0 +1,266 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RSMetaDataTest { + private Connection conn; + private Statement stat; + private ResultSetMetaData meta; + + @BeforeEach + public void connect() throws Exception { + conn = DriverManager.getConnection("jdbc:sqlite:"); + stat = conn.createStatement(); + stat.executeUpdate( + "create table People (pid integer primary key autoincrement, " + + " firstname string(255), surname string(25,5), dob date);"); + stat.executeUpdate( + "create table Film (id integer primary key autoincrement, " + + " title string(255) not null, length integer not null, budget real);"); + stat.executeUpdate( + "insert into people values (null, 'Mohandas', 'Gandhi', " + " '1869-10-02');"); + meta = stat.executeQuery("select pid, firstname, surname from people;").getMetaData(); + } + + @AfterEach + public void close() throws SQLException { + stat.executeUpdate("drop table people;"); + stat.close(); + conn.close(); + } + + @Test + public void catalogName() throws SQLException { + assertThat(meta.getCatalogName(1)).isEqualTo(""); + } + + @Test + public void schemaName() throws SQLException { + assertThat(meta.getSchemaName(1)).isEqualTo(""); + } + + @Test + public void columns() throws SQLException { + assertThat(meta.getColumnCount()).isEqualTo(3); + assertThat(meta.getColumnName(1)).isEqualTo("pid"); + assertThat(meta.getColumnName(2)).isEqualTo("firstname"); + assertThat(meta.getColumnName(3)).isEqualTo("surname"); + assertThat(meta.getColumnType(1)).isEqualTo(Types.INTEGER); + assertThat(meta.getColumnType(2)).isEqualTo(Types.VARCHAR); + assertThat(meta.getColumnType(3)).isEqualTo(Types.VARCHAR); + assertThat(meta.isAutoIncrement(1)).isTrue(); + assertThat(meta.isAutoIncrement(2)).isFalse(); + assertThat(meta.isAutoIncrement(3)).isFalse(); + assertThat(meta.isNullable(1)).isEqualTo(ResultSetMetaData.columnNullable); + assertThat(meta.isNullable(2)).isEqualTo(ResultSetMetaData.columnNullable); + assertThat(meta.isNullable(3)).isEqualTo(ResultSetMetaData.columnNullable); + } + + @Test + public void columnTypes() throws SQLException { + stat.executeUpdate( + "create table tbl (col1 INT, col2 INTEGER, col3 TINYINT, " + + "col4 SMALLINT, col5 MEDIUMINT, col6 BIGINT, col7 UNSIGNED BIG INT, " + + "col8 INT2, col9 INT8, col10 CHARACTER(20), col11 VARCHAR(255), " + + "col12 VARYING CHARACTER(255), col13 NCHAR(55), " + + "col14 NATIVE CHARACTER(70), col15 NVARCHAR(100), col16 TEXT, " + + "col17 CLOB, col18 BLOB, col19 REAL, col20 DOUBLE, " + + "col21 DOUBLE PRECISION, col22 FLOAT, col23 NUMERIC, " + + "col24 DECIMAL(10,5), col25 BOOLEAN, col26 DATE, col27 DATETIME, " + + "col28 TIMESTAMP, col29 CHAR(70), col30 TEXT, col31 TIMESTAMP)"); + // insert empty data into table otherwise getColumnType returns null + stat.executeUpdate( + "insert into tbl values (1, 2, 3, 4, 5, 6, 7, 8, 9," + + "'c', 'varchar', 'varying', 'n', 'n','nvarchar', 'text', 'clob'," + + "null, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 0, 12345, 123456, 0, 'char', 'some text'," + + "'2022-08-26 10:20:00.123')"); + meta = + stat.executeQuery( + "select col1, col2, col3, col4, col5, col6, col7, col8, col9, " + + "col10, col11, col12, col13, col14, col15, col16, col17, col18, " + + "col19, col20, col21, col22, col23, col24, col25, col26, col27, " + + "col28, col29, col30, " + + "cast(col1 as boolean), col31 from tbl") + .getMetaData(); + + assertThat(meta.getColumnType(1)).isEqualTo(Types.INTEGER); + assertThat(meta.getColumnType(2)).isEqualTo(Types.INTEGER); + assertThat(meta.getColumnType(3)).isEqualTo(Types.TINYINT); + assertThat(meta.getColumnType(4)).isEqualTo(Types.SMALLINT); + assertThat(meta.getColumnType(5)).isEqualTo(Types.INTEGER); + assertThat(meta.getColumnType(6)).isEqualTo(Types.BIGINT); + assertThat(meta.getColumnType(7)).isEqualTo(Types.BIGINT); + assertThat(meta.getColumnType(8)).isEqualTo(Types.SMALLINT); + assertThat(meta.getColumnType(9)).isEqualTo(Types.BIGINT); + + assertThat(meta.getColumnType(10)).isEqualTo(Types.CHAR); + assertThat(meta.getColumnType(11)).isEqualTo(Types.VARCHAR); + assertThat(meta.getColumnType(12)).isEqualTo(Types.VARCHAR); + assertThat(meta.getColumnType(13)).isEqualTo(Types.CHAR); + assertThat(meta.getColumnType(14)).isEqualTo(Types.CHAR); + assertThat(meta.getColumnType(15)).isEqualTo(Types.VARCHAR); + assertThat(meta.getColumnType(16)).isEqualTo(Types.VARCHAR); + assertThat(meta.getColumnType(17)).isEqualTo(Types.CLOB); + + assertThat(meta.getColumnType(18)).isEqualTo(Types.BLOB); + + assertThat(meta.getColumnType(19)).isEqualTo(Types.REAL); + assertThat(meta.getColumnType(20)).isEqualTo(Types.DOUBLE); + assertThat(meta.getColumnType(21)).isEqualTo(Types.DOUBLE); + assertThat(meta.getColumnType(22)).isEqualTo(Types.FLOAT); + assertThat(meta.getColumnType(23)).isEqualTo(Types.NUMERIC); + assertThat(meta.getColumnType(24)).isEqualTo(Types.DECIMAL); + assertThat(meta.getColumnType(25)).isEqualTo(Types.BOOLEAN); + + assertThat(meta.getColumnType(26)).isEqualTo(Types.DATE); + assertThat(meta.getColumnType(27)).isEqualTo(Types.TIMESTAMP); + + assertThat(meta.getColumnType(28)).isEqualTo(Types.TIMESTAMP); + assertThat(meta.getColumnType(29)).isEqualTo(Types.CHAR); + + assertThat(meta.getColumnType(30)).isEqualTo(Types.VARCHAR); + + assertThat(meta.getColumnType(31)).isEqualTo(Types.BOOLEAN); + + assertThat(meta.getColumnType(32)).isEqualTo(Types.TIMESTAMP); + + assertThat(meta.getPrecision(24)).isEqualTo(10); + assertThat(meta.getScale(24)).isEqualTo(5); + } + + @Test + public void columTypeWithoutTable() throws SQLException { + ResultSet rs = + stat.executeQuery( + "SELECT FALSE, 1, 3900000000, CAST(3900000000 AS BIGINT), CAST(3900000000 AS VARCHAR(50))"); + ResultSetMetaData meta = rs.getMetaData(); + + assertThat(rs.next()).isTrue(); + + assertThat(meta.getColumnType(1)).isEqualTo(Types.INTEGER); + assertThat(meta.isSigned(1)).isTrue(); + + assertThat(meta.getColumnType(2)).isEqualTo(Types.INTEGER); + assertThat(meta.isSigned(2)).isTrue(); + + assertThat(meta.getColumnType(3)).isEqualTo(Types.BIGINT); + assertThat(meta.isSigned(3)).isTrue(); + + assertThat(meta.getColumnType(4)).isEqualTo(Types.BIGINT); + assertThat(meta.isSigned(4)).isTrue(); + + assertThat(meta.getColumnType(5)).isEqualTo(Types.VARCHAR); + assertThat(meta.isSigned(5)).isFalse(); + + assertThat(rs.next()).isFalse(); + } + + @Test + public void datetimeColumnTypeIsTimestamp() throws SQLException { + stat.executeUpdate( + "CREATE TABLE mytable (" + + "\"fid\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "\"date_creation\" DATETIME)"); + + try (ResultSet rs = stat.executeQuery("SELECT * FROM mytable")) { + ResultSetMetaData rsmd = rs.getMetaData(); + assertThat(rsmd.getColumnTypeName(2)).isEqualTo("DATETIME"); + assertThat(rsmd.getColumnType(2)).isEqualTo(Types.TIMESTAMP); + } + + stat.executeUpdate("INSERT INTO mytable(date_creation) VALUES ('2024-01-15 13:45:30.123')"); + try (ResultSet rs = stat.executeQuery("SELECT * FROM mytable")) { + assertThat(rs.next()).isTrue(); + ResultSetMetaData rsmd = rs.getMetaData(); + assertThat(rsmd.getColumnType(2)).isEqualTo(Types.TIMESTAMP); + assertThat(rs.getTimestamp(2)) + .isEqualTo(java.sql.Timestamp.valueOf("2024-01-15 13:45:30.123")); + } + + stat.executeUpdate("DELETE FROM mytable"); + stat.executeUpdate("INSERT INTO mytable(date_creation) VALUES (1705326330)"); + try (ResultSet rs = stat.executeQuery("SELECT * FROM mytable")) { + assertThat(rs.next()).isTrue(); + ResultSetMetaData rsmd = rs.getMetaData(); + assertThat(rsmd.getColumnTypeName(2)).isEqualTo("DATETIME"); + assertThat(rsmd.getColumnType(2)).isEqualTo(Types.TIMESTAMP); + } + + stat.executeUpdate("CREATE TABLE temporal (d DATE, dt DATETIME, ts TIMESTAMP)"); + try (ResultSet rs = stat.executeQuery("SELECT * FROM temporal")) { + ResultSetMetaData rsmd = rs.getMetaData(); + assertThat(rsmd.getColumnType(1)).isEqualTo(Types.DATE); + assertThat(rsmd.getColumnType(2)).isEqualTo(Types.TIMESTAMP); + assertThat(rsmd.getColumnType(3)).isEqualTo(Types.TIMESTAMP); + } + } + + @Test + public void testGetColumnClassName() throws SQLException { + stat.executeUpdate( + "create table gh_541 (id int, DESCRIPTION varchar(40), price DOUBLE, data BLOB, bool BOOLEAN)"); + stat.executeUpdate("insert into gh_541 values (1, 'description', 28.4, null, True);"); + ResultSetMetaData meta = stat.executeQuery("select * from gh_541").getMetaData(); + + assertThat(meta.getColumnClassName(1)).isEqualTo("java.lang.Integer"); + assertThat(meta.getColumnClassName(2)).isEqualTo("java.lang.String"); + assertThat(meta.getColumnClassName(3)).isEqualTo("java.lang.Double"); + assertThat(meta.getColumnClassName(4)).isEqualTo("java.lang.Object"); + assertThat(meta.getColumnClassName(5)).isEqualTo("java.lang.Integer"); + } + + @Test + public void differentRS() throws SQLException { + meta = stat.executeQuery("select * from people;").getMetaData(); + assertThat(meta.getColumnCount()).isEqualTo(4); + assertThat(meta.getColumnName(1)).isEqualTo("pid"); + assertThat(meta.getColumnName(2)).isEqualTo("firstname"); + assertThat(meta.getColumnName(3)).isEqualTo("surname"); + assertThat(meta.getColumnName(4)).isEqualTo("dob"); + } + + @Test + public void nullable() throws SQLException { + meta = stat.executeQuery("select * from film;").getMetaData(); + assertThat(meta.isNullable(1)).isEqualTo(ResultSetMetaData.columnNullable); + assertThat(meta.isNullable(2)).isEqualTo(ResultSetMetaData.columnNoNulls); + assertThat(meta.isNullable(3)).isEqualTo(ResultSetMetaData.columnNoNulls); + assertThat(meta.isNullable(4)).isEqualTo(ResultSetMetaData.columnNullable); + } + + @Test + public void badTableIndex() { + assertThatExceptionOfType(SQLException.class).isThrownBy(() -> meta.getTableName(5)); + } + + @Test + public void badColumnIndex() { + assertThatExceptionOfType(SQLException.class).isThrownBy(() -> meta.getColumnName(4)); + } + + @Test + public void scale() throws SQLException { + assertThat(meta.getScale(2)).isEqualTo(0); + assertThat(meta.getScale(3)).isEqualTo(5); + } + + @Test + public void tableName() throws SQLException { + final ResultSet rs = stat.executeQuery("SELECT pid, time(dob) as some_time from people"); + assertThat(rs.getMetaData().getTableName(1)).isEqualTo("People"); + assertThat(rs.getMetaData().getTableName(2)).isEqualTo(""); + rs.close(); + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetTest.java new file mode 100644 index 0000000..d00590c --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetTest.java @@ -0,0 +1,378 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ResultSetTest { + + private Connection conn; + private Statement stat; + + @BeforeEach + public void connect() throws Exception { + conn = DriverManager.getConnection("jdbc:sqlite:"); + stat = conn.createStatement(); + stat.executeUpdate( + "create table test (id int primary key, DESCRIPTION varchar(40), fOo varchar(3));"); + stat.executeUpdate("insert into test values (1, 'description', 'bar')"); + } + + @AfterEach + public void close() throws SQLException { + stat.close(); + conn.close(); + } + + @Test + void testTableColumnLowerNowFindLowerCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("id")).isEqualTo(1); + } + + @Test + void testTableColumnLowerNowFindUpperCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("ID")).isEqualTo(1); + } + + @Test + void testTableColumnLowerNowFindMixedCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("Id")).isEqualTo(1); + } + + @Test + void testTableColumnUpperNowFindLowerCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("description")).isEqualTo(2); + } + + @Test + void testTableColumnUpperNowFindUpperCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("DESCRIPTION")).isEqualTo(2); + } + + @Test + void testTableColumnUpperNowFindMixedCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("Description")).isEqualTo(2); + } + + @Test + void testTableColumnMixedNowFindLowerCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("foo")).isEqualTo(3); + } + + @Test + void testTableColumnMixedNowFindUpperCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("FOO")).isEqualTo(3); + } + + @Test + void testTableColumnMixedNowFindMixedCaseColumn() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("fOo")).isEqualTo(3); + } + + @Test + void testSelectWithTableNameAliasNowFindWithoutTableNameAlias() throws SQLException { + ResultSet resultSet = stat.executeQuery("select t.id from test as t"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("id")).isEqualTo(1); + } + + /** + * Can't produce a case where column name contains table name + * https://www.sqlite.org/c3ref/column_name.html : "If there is no AS clause then the name of + * the column is unspecified" + */ + @Test + void testSelectWithTableNameAliasNowNotFindWithTableNameAlias() throws SQLException { + ResultSet resultSet = stat.executeQuery("select t.id from test as t"); + assertThat(resultSet.next()).isTrue(); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> resultSet.findColumn("t.id")); + } + + @Test + void testSelectWithTableNameNowFindWithoutTableName() throws SQLException { + ResultSet resultSet = stat.executeQuery("select test.id from test"); + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.findColumn("id")).isEqualTo(1); + } + + @Test + void testSelectWithTableNameNowNotFindWithTableName() throws SQLException { + ResultSet resultSet = stat.executeQuery("select test.id from test"); + assertThat(resultSet.next()).isTrue(); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> resultSet.findColumn("test.id")); + } + + @Test + void testCloseStatement() throws SQLException { + ResultSet resultSet = stat.executeQuery("select test.id from test"); + + stat.close(); + + assertThat(stat.isClosed()).isTrue(); + assertThat(resultSet.isClosed()).isTrue(); + + resultSet.close(); + + assertThat(resultSet.isClosed()).isTrue(); + } + + @Test + void testReturnsNonAsciiCodepoints() throws SQLException { + String nonAsciiString = "국정의 중요한 사항에 관한"; + PreparedStatement pstat = conn.prepareStatement("select ?"); + pstat.setString(1, nonAsciiString); + + ResultSet resultSet = pstat.executeQuery(); + + assertThat(resultSet.next()).isTrue(); + assertThat(resultSet.getString(1)).isEqualTo(nonAsciiString); + assertThat(resultSet.next()).isFalse(); + } + + @Test + void testFindColumnOnEmptyResultSet() throws SQLException { + ResultSet resultSet = stat.executeQuery("select * from test where id = 0"); + assertThat(resultSet.next()).isFalse(); + assertThat(resultSet.findColumn("id")).isEqualTo(1); + } + + @Test + void testNumericTypes() throws SQLException { + stat.executeUpdate("create table numeric(c1, c2, c3)"); + stat.executeUpdate("insert into numeric values (1, 1.1, null)"); + + ResultSet rs = stat.executeQuery("select * from numeric"); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(1); + assertThat(rs.getInt(2)).isEqualTo(1); + assertThat(rs.getInt(3)).isEqualTo(0); + assertThat(rs.getLong(1)).isEqualTo(1L); + assertThat(rs.getLong(2)).isEqualTo(1L); + assertThat(rs.getLong(3)).isEqualTo(0L); + assertThat(rs.getDouble(1)).isEqualTo(1.0D); + assertThat(rs.getDouble(2)).isEqualTo(1.1D); + assertThat(rs.getDouble(3)).isEqualTo(0D); + assertThat(rs.getFloat(1)).isEqualTo(1.0F); + assertThat(rs.getFloat(2)).isEqualTo(1.1F); + assertThat(rs.getFloat(3)).isEqualTo(0F); + assertThat(rs.getString(1)).isEqualTo("1"); + assertThat(rs.getString(2)).isEqualTo("1.1"); + assertThat(rs.getString(3)).isNull(); + } + + @Test + void testGetBigDecimal() throws SQLException { + stat.executeUpdate( + "create table bigdecimal(c1, c2 integer, c3 real, c4 double, c5 decimal, c6 numeric, c7 float)"); + stat.executeUpdate("insert into bigdecimal values (1, 2, 3, 4, 5, 6, 7)"); + stat.executeUpdate("insert into bigdecimal values ('1', '2', '3', '4', '5', '6', '7')"); + stat.executeUpdate("insert into bigdecimal values (1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1)"); + stat.executeUpdate( + "insert into bigdecimal values ('1.1', '2.1', '3.1', '4.1', '5.1', '6.1', '7.1')"); + stat.executeUpdate( + "insert into bigdecimal values (null, null, null, null, null, null, null)"); + stat.executeUpdate( + "insert into bigdecimal values ('null', '', 'abc', 'abc', 'abc', 'abc', 'abc')"); + + ResultSet rs = stat.executeQuery("select * from bigdecimal"); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getBigDecimal(1)).isEqualTo(new BigDecimal("1")); + assertThat(rs.getBigDecimal(2)).isEqualTo(new BigDecimal("2")); + assertThat(rs.getBigDecimal(3)).isEqualTo(new BigDecimal("3.0")); + assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("4.0")); + assertThat(rs.getBigDecimal(5)).isEqualTo(new BigDecimal("5")); + assertThat(rs.getBigDecimal(6)).isEqualTo(new BigDecimal("6")); + assertThat(rs.getBigDecimal(7)).isEqualTo(new BigDecimal("7.0")); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getBigDecimal(1)).isEqualTo(new BigDecimal("1")); + assertThat(rs.getBigDecimal(2)).isEqualTo(new BigDecimal("2")); + assertThat(rs.getBigDecimal(3)).isEqualTo(new BigDecimal("3.0")); + assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("4.0")); + assertThat(rs.getBigDecimal(5)).isEqualTo(new BigDecimal("5")); + assertThat(rs.getBigDecimal(6)).isEqualTo(new BigDecimal("6")); + assertThat(rs.getBigDecimal(7)).isEqualTo(new BigDecimal("7.0")); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getBigDecimal(1)).isEqualTo(new BigDecimal("1.1")); + assertThat(rs.getBigDecimal(2)).isEqualTo(new BigDecimal("2.1")); + assertThat(rs.getBigDecimal(3)).isEqualTo(new BigDecimal("3.1")); + assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("4.1")); + assertThat(rs.getBigDecimal(5)).isEqualTo(new BigDecimal("5.1")); + assertThat(rs.getBigDecimal(6)).isEqualTo(new BigDecimal("6.1")); + assertThat(rs.getBigDecimal(7)).isEqualTo(new BigDecimal("7.1")); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getBigDecimal(1)).isEqualTo(new BigDecimal("1.1")); + assertThat(rs.getBigDecimal(2)).isEqualTo(new BigDecimal("2.1")); + assertThat(rs.getBigDecimal(3)).isEqualTo(new BigDecimal("3.1")); + assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("4.1")); + assertThat(rs.getBigDecimal(5)).isEqualTo(new BigDecimal("5.1")); + assertThat(rs.getBigDecimal(6)).isEqualTo(new BigDecimal("6.1")); + assertThat(rs.getBigDecimal(7)).isEqualTo(new BigDecimal("7.1")); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getBigDecimal(1)).isNull(); + assertThat(rs.getBigDecimal(2)).isNull(); + assertThat(rs.getBigDecimal(3)).isNull(); + assertThat(rs.getBigDecimal(4)).isNull(); + assertThat(rs.getBigDecimal(5)).isNull(); + assertThat(rs.getBigDecimal(6)).isNull(); + assertThat(rs.getBigDecimal(7)).isNull(); + + assertThat(rs.next()).isTrue(); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(1)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(2)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(3)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(4)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(5)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(6)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getBigDecimal(7)) + .withMessageContaining("Bad value for type BigDecimal"); + + assertThat(rs.next()).isFalse(); + } + + @Test + void getObjectWithRequestedType() throws SQLException { + stat.executeUpdate("create table getobject(c1)"); + stat.executeUpdate("insert into getobject values (1)"); + stat.executeUpdate("insert into getobject values ('abc')"); + + ResultSet rs = stat.executeQuery("select * from getobject"); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getObject(1, String.class)).isEqualTo("1"); + assertThat(rs.getObject(1, Boolean.class)).isTrue(); + assertThat(rs.getObject(1, BigDecimal.class)).isEqualTo(rs.getBigDecimal(1)); + assertThat(rs.getObject(1, byte[].class)).isEqualTo(rs.getBytes(1)); + assertThat(rs.getObject(1, Double.class)).isEqualTo(rs.getDouble(1)); + assertThat(rs.getObject(1, Long.class)).isEqualTo(rs.getLong(1)); + assertThat(rs.getObject(1, Float.class)).isEqualTo(rs.getFloat(1)); + assertThat(rs.getObject(1, Integer.class)).isEqualTo(rs.getInt(1)); + assertThat(rs.getObject(1, Date.class)).isEqualTo(rs.getDate(1)); + assertThat(rs.getObject(1, Time.class)).isEqualTo(rs.getTime(1)); + assertThat(rs.getObject(1, Timestamp.class)).isEqualTo(rs.getTimestamp(1)); + + assertThat(rs.next()).isTrue(); + assertThat(rs.getObject(1, String.class)).isEqualTo("abc"); + assertThat(rs.getObject(1, Boolean.class)).isFalse(); + assertThat(rs.getObject(1, byte[].class)).isEqualTo(rs.getBytes(1)); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getObject(1, BigDecimal.class)) + .withMessageContaining("Bad value for type BigDecimal"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getObject(1, Double.class)) + .withMessageContaining("Bad value for type Double"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getObject(1, Long.class)) + .withMessageContaining("Bad value for type Long"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getObject(1, Float.class)) + .withMessageContaining("Bad value for type Float"); + assertThatExceptionOfType(SQLException.class) + .isThrownBy(() -> rs.getObject(1, Integer.class)) + .withMessageContaining("Bad value for type Integer"); + + assertThat(rs.next()).isFalse(); + } + + @Test + void testJdk8AddedDateTimeObjects() throws SQLException { + stat.executeUpdate("create table datetime_test(c1)"); + stat.executeUpdate("insert into datetime_test values ('2021-11-09 11:20:58')"); + stat.executeUpdate("insert into datetime_test values ('2021-11-09')"); + stat.executeUpdate("insert into datetime_test values ('11:20:58')"); + stat.executeUpdate("insert into datetime_test values (NULL)"); + + ResultSet rs = stat.executeQuery("select * from datetime_test"); + + rs.next(); + assertThat(rs.getObject(1, LocalDate.class)).isEqualTo(LocalDate.of(2021, 11, 9)); + assertThat(rs.getObject(1, LocalTime.class)).isEqualTo(LocalTime.of(11, 20, 58)); + assertThat(rs.getObject(1, LocalDateTime.class)) + .isEqualTo(LocalDateTime.of(2021, 11, 9, 11, 20, 58)); + + rs.next(); + assertThat(rs.getObject(1, LocalDate.class)).isEqualTo(LocalDate.of(2021, 11, 9)); + + rs.next(); + assertThat(rs.getObject(1, LocalTime.class)).isEqualTo(LocalTime.of(11, 20, 58)); + + rs.next(); + assertThat(rs.getObject(1, LocalDate.class)).isNull(); + assertThat(rs.getObject(1, LocalTime.class)).isNull(); + assertThat(rs.getObject(1, LocalDateTime.class)).isNull(); + } + + @Test + void gh808_getResultSetMetadataAfterReadingLastRow() throws SQLException { + for (int i = 0; i < 2; i++) { + ResultSet rs = stat.executeQuery("select 1"); + assertThat(rs).isNotNull(); + + assertThat(rs.next()).isTrue(); + assertThat(rs.isAfterLast()).isFalse(); + + assertThat(rs.next()).isFalse(); + assertThat(rs.isClosed()).isFalse(); + assertThat(rs.isAfterLast()).isTrue(); + + ResultSetMetaData meta = rs.getMetaData(); + assertThat(meta).isNotNull(); + assertThat(meta.getColumnCount()).isEqualTo(1); + } + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java new file mode 100644 index 0000000..0d95dac --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/ResultSetWithoutResultsTest.java @@ -0,0 +1,53 @@ +package org.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.Test; + +/** Tests to make sure we properly reset a query when there are no results */ +public class ResultSetWithoutResultsTest { + + /** + * Validates that Statements can be reused as expected even when the result set has no results + * + * @throws Exception + */ + @Test + public void testQueryIsReset() throws Exception { + try (Connection con = DriverManager.getConnection("jdbc:sqlite::memory:")) { + try (Statement statement = con.createStatement()) { + // run a few queries with no results + for (int i = 0; i < 3; i++) { + runEmptyStatement(statement); + } + // test a query that has a result. + testStmtWithResult(statement); + } + } + } + + private void testStmtWithResult(Statement statement) throws SQLException { + try (ResultSet rs = statement.executeQuery("select 123")) { + assertThat(rs.isBeforeFirst()).isTrue(); + assertThat(rs.isAfterLast()).isFalse(); + rs.next(); + assertThat(rs.isBeforeFirst()).isFalse(); + assertThat(rs.isAfterLast()).isFalse(); + assertThat(rs.getInt(1)).isEqualTo(123); + rs.next(); + assertThat(rs.isAfterLast()).isTrue(); + } + } + + private void runEmptyStatement(Statement statement) throws SQLException { + try (ResultSet rs = statement.executeQuery("select 1 where 1=0")) { + assertThat(rs.isBeforeFirst()).isFalse(); + assertThat(rs.isAfterLast()).isFalse(); + } + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/TypeMapTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/TypeMapTest.java new file mode 100644 index 0000000..66bf9e4 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/TypeMapTest.java @@ -0,0 +1,33 @@ +package org.sqlite; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class TypeMapTest { + + public Connection getConnection() throws SQLException { + return DriverManager.getConnection("jdbc:sqlite::memory:"); + } + + @Test + public void getTypeMap() throws Exception { + Connection conn = getConnection(); + + Map> m = conn.getTypeMap(); + + conn.close(); + } + + @Test + public void setTypeMap() throws Exception { + Connection conn = getConnection(); + + Map> m = conn.getTypeMap(); + conn.setTypeMap(m); + + conn.close(); + } +} diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/Utils.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/Utils.java new file mode 100644 index 0000000..94bc481 --- /dev/null +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/Utils.java @@ -0,0 +1,59 @@ +package org.sqlite; + +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +public class Utils { + public static List getCompileOptions(Connection conn) throws SQLException { + List compileOptions = new ArrayList<>(); + try (Statement stat = conn.createStatement()) { + try (ResultSet rs = stat.executeQuery("pragma compile_options")) { + while (rs.next()) { + compileOptions.add(rs.getString(1)); + } + } + } + return compileOptions; + } + + public static void assumeJdbcExtensions(Connection conn) throws SQLException { + assumeThat(getCompileOptions(conn)) + .as("SQLite has to be compiled with JDBC Extensions") + .contains("JDBC_EXTENSIONS"); + } + + public static void assumeMathFunctions(Connection conn) throws SQLException { + assumeThat(getCompileOptions(conn)) + .as("SQLite has to be compiled with SQLITE_ENABLE_MATH_FUNCTIONS") + .contains("ENABLE_MATH_FUNCTIONS"); + } + + public static void assumeJdbcExtensionsOrMathFunctions(Connection conn) throws SQLException { + List compileOptions = getCompileOptions(conn); + boolean expected = + compileOptions.contains("JDBC_EXTENSIONS") + || compileOptions.contains("ENABLE_MATH_FUNCTIONS"); + assumeThat(expected) + .as( + "SQLite has to be compiled with JDBC Extensions or SQLITE_ENABLE_MATH_FUNCTIONS") + .isTrue(); + } + + public static void assumeJdbcExtensionsWithoutMathFunctions(Connection conn) + throws SQLException { + List compileOptions = getCompileOptions(conn); + boolean expected = + compileOptions.contains("JDBC_EXTENSIONS") + && !compileOptions.contains("ENABLE_MATH_FUNCTIONS"); + assumeThat(expected) + .as( + "SQLite has to be compiled with JDBC Extensions and without SQLITE_ENABLE_MATH_FUNCTIONS") + .isTrue(); + } +}