diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index 280f7d58f..d51c0c00e 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -87,7 +87,8 @@ public Statement createStatement(int resultSetType, int resultSetConcurrency, in public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { checkOpen(); - if (resultSetConcurrency == ResultSet.CONCUR_READ_ONLY && resultSetType == ResultSet.TYPE_FORWARD_ONLY) { + if ((resultSetConcurrency == ResultSet.CONCUR_READ_ONLY && resultSetType == ResultSet.TYPE_FORWARD_ONLY) || + readOnly) { return new DuckDBPreparedStatement(this, sql); } throw new SQLFeatureNotSupportedException("prepareStatement"); diff --git a/src/main/java/org/duckdb/DuckDBDatabaseMetaData.java b/src/main/java/org/duckdb/DuckDBDatabaseMetaData.java index 189b825f1..5ff1e3bc5 100644 --- a/src/main/java/org/duckdb/DuckDBDatabaseMetaData.java +++ b/src/main/java/org/duckdb/DuckDBDatabaseMetaData.java @@ -721,7 +721,7 @@ public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLExce @Override public ResultSet getTableTypes() throws SQLException { - String[] tableTypesArray = new String[] {"BASE TABLE", "LOCAL TEMPORARY", "VIEW"}; + String[] tableTypesArray = new String[] {"TABLE", "LOCAL TEMPORARY", "VIEW"}; StringBuilder stringBuilder = new StringBuilder(128); boolean first = true; for (String tableType : tableTypesArray) { @@ -751,7 +751,9 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam sb.append("table_catalog AS 'TABLE_CAT'").append(TRAILING_COMMA).append(lineSeparator()); sb.append("table_schema AS 'TABLE_SCHEM'").append(TRAILING_COMMA).append(lineSeparator()); sb.append("table_name AS 'TABLE_NAME'").append(TRAILING_COMMA).append(lineSeparator()); - sb.append("table_type AS 'TABLE_TYPE'").append(TRAILING_COMMA).append(lineSeparator()); + sb.append("CASE WHEN table_type = 'BASE TABLE' THEN 'TABLE' ELSE table_type END AS 'TABLE_TYPE'") + .append(TRAILING_COMMA) + .append(lineSeparator()); sb.append("TABLE_COMMENT AS 'REMARKS'").append(TRAILING_COMMA).append(lineSeparator()); sb.append("NULL::VARCHAR AS 'TYPE_CAT'").append(TRAILING_COMMA).append(lineSeparator()); sb.append("NULL::VARCHAR AS 'TYPE_SCHEM'").append(TRAILING_COMMA).append(lineSeparator()); @@ -795,7 +797,8 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam if (types != null && types.length > 0) { for (int i = 0; i < types.length; i++) { - ps.setString(paramIdx + i, types[i]); + String param = "TABLE".equals(types[i]) ? "BASE TABLE" : types[i]; + ps.setString(paramIdx + i, param); } } ps.closeOnCompletion(); diff --git a/src/main/java/org/duckdb/DuckDBDriver.java b/src/main/java/org/duckdb/DuckDBDriver.java index 8a59b40da..ae33189b1 100644 --- a/src/main/java/org/duckdb/DuckDBDriver.java +++ b/src/main/java/org/duckdb/DuckDBDriver.java @@ -21,6 +21,10 @@ public class DuckDBDriver implements java.sql.Driver { public static final String DUCKDB_READONLY_PROPERTY = "duckdb.read_only"; + public static final String DUCKDB_ACCESS_MODE_PROPERTY = "access_mode"; + public static final String DUCKDB_ACCESS_MODE_READ_ONLY = "READ_ONLY"; + public static final String DUCKDB_ACCESS_MODE_READ_WRITE = "READ_WRITE"; + public static final String DUCKDB_ACCESS_MODE_AUTOMATIC = "AUTOMATIC"; public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent"; public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results"; public static final String JDBC_AUTO_COMMIT = "jdbc_auto_commit"; @@ -94,9 +98,8 @@ public Connection connect(String url, Properties info) throws SQLException { // Ignore unsupported removeUnsupportedOptions(props); - // Read-only option - String readOnlyStr = removeOption(props, DUCKDB_READONLY_PROPERTY); - boolean readOnly = isStringTruish(readOnlyStr, false); + // Read-only options + boolean readOnly = removeReadOnly(props); // Client name option props.put("duckdb_api", "jdbc"); @@ -295,6 +298,22 @@ private static void removeUnsupportedOptions(Properties props) throws SQLExcepti } } + private static boolean removeReadOnly(Properties props) throws SQLException { + String readOnlyStr = removeOption(props, DUCKDB_READONLY_PROPERTY); + boolean readOnly = isStringTruish(readOnlyStr, false); + String accessMode = getOption(props, DUCKDB_ACCESS_MODE_PROPERTY); + if (null != accessMode) { + boolean accessReadOnly = DUCKDB_ACCESS_MODE_READ_ONLY.equalsIgnoreCase(accessMode); + if (null != readOnlyStr && readOnly != accessReadOnly) { + throw new SQLException( + "Invalid options specified, values of 'access_mode' and 'duckdb.read_only'" + + " properties does not match, use 'access_mode=READ_ONLY' to open connection in read-only mode"); + } + return accessReadOnly; + } + return readOnly; + } + private static SessionInitSQLFile readSessionInitSQLFile(ParsedProps pp) throws SQLException { if (!pp.props.containsKey(SESSION_INIT_SQL_FILE_OPTION)) { return new SessionInitSQLFile(); diff --git a/src/main/java/org/duckdb/JdbcUtils.java b/src/main/java/org/duckdb/JdbcUtils.java index d83d960ef..823ad8a82 100644 --- a/src/main/java/org/duckdb/JdbcUtils.java +++ b/src/main/java/org/duckdb/JdbcUtils.java @@ -31,6 +31,14 @@ static String removeOption(Properties props, String opt, String defaultVal) { return defaultVal; } + static String getOption(Properties props, String opt) { + Object obj = props.get(opt); + if (null != obj) { + return obj.toString().trim(); + } + return null; + } + static void setDefaultOptionValue(Properties props, String opt, Object value) { if (props.containsKey(opt)) { return; diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index 9f30d7ff9..f83a32536 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.sql.*; import java.time.*; import java.time.format.DateTimeFormatter; @@ -48,6 +49,7 @@ import java.util.logging.Logger; import javax.sql.rowset.CachedRowSet; import javax.sql.rowset.RowSetProvider; +import org.duckdb.test.TempDirectory; public class TestDuckDBJDBC { @@ -563,72 +565,93 @@ public static void test_borked_string_bug539() throws Exception { } public static void test_read_only() throws Exception { - Path database_file = Files.createTempFile("duckdb-jdbc-test-", ".duckdb"); - Files.deleteIfExists(database_file); - - String jdbc_url = JDBC_URL + database_file; - Properties ro_prop = new Properties(); - ro_prop.setProperty("duckdb.read_only", "true"); - - Connection conn_rw = DriverManager.getConnection(jdbc_url); - assertFalse(conn_rw.isReadOnly()); - assertFalse(conn_rw.getMetaData().isReadOnly()); - Statement stmt = conn_rw.createStatement(); - stmt.execute("CREATE TABLE test (i INTEGER)"); - stmt.execute("INSERT INTO test VALUES (42)"); - stmt.close(); + Properties prop1 = new Properties(); + prop1.put(DuckDBDriver.DUCKDB_READONLY_PROPERTY, "true"); + Properties prop2 = new Properties(); + prop2.put(DuckDBDriver.DUCKDB_READONLY_PROPERTY, true); + Properties prop3 = new Properties(); + prop3.put(DuckDBDriver.DUCKDB_ACCESS_MODE_PROPERTY, DuckDBDriver.DUCKDB_ACCESS_MODE_READ_ONLY); + Properties prop4 = new Properties(); + prop3.put(DuckDBDriver.DUCKDB_READONLY_PROPERTY, true); + prop4.put(DuckDBDriver.DUCKDB_ACCESS_MODE_PROPERTY, DuckDBDriver.DUCKDB_ACCESS_MODE_READ_ONLY); + List propList = Arrays.asList(prop1, prop2, prop3, prop4); + + for (Properties config : propList) { + try (TempDirectory dir = new TempDirectory()) { + Path database_file = dir.path().resolve(Paths.get("duckcb_jdbc_test_read_only.db")); + String jdbc_url = JDBC_URL + database_file; + Connection conn_rw = DriverManager.getConnection(jdbc_url); + assertFalse(conn_rw.isReadOnly()); + assertFalse(conn_rw.getMetaData().isReadOnly()); + Statement stmt = conn_rw.createStatement(); + stmt.execute("CREATE TABLE test (i INTEGER)"); + stmt.execute("INSERT INTO test VALUES (42)"); + stmt.close(); + + // Verify we can open additional write connections + // Using the Driver + try (Connection conn = DriverManager.getConnection(jdbc_url); Statement stmt1 = conn.createStatement(); + ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { + rs1.next(); + assertEquals(rs1.getInt(1), 42); + } + // Using the direct API + try (Connection conn = conn_rw.unwrap(DuckDBConnection.class).duplicate(); + Statement stmt1 = conn.createStatement(); + ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { + rs1.next(); + assertEquals(rs1.getInt(1), 42); + } - // Verify we can open additional write connections - // Using the Driver - try (Connection conn = DriverManager.getConnection(jdbc_url); Statement stmt1 = conn.createStatement(); - ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { - rs1.next(); - assertEquals(rs1.getInt(1), 42); - } - // Using the direct API - try (Connection conn = conn_rw.unwrap(DuckDBConnection.class).duplicate(); - Statement stmt1 = conn.createStatement(); ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { - rs1.next(); - assertEquals(rs1.getInt(1), 42); - } - - // At this time, mixing read and write connections on Windows doesn't work - // Read-only when we already have a read-write - // try (Connection conn = DriverManager.getConnection(jdbc_url, ro_prop); - // Statement stmt1 = conn.createStatement(); - // ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { - // rs1.next(); - // assertEquals(rs1.getInt(1), 42); - // } - - conn_rw.close(); - - assertThrows(conn_rw::createStatement, SQLException.class); - assertThrows(() -> { conn_rw.unwrap(DuckDBConnection.class).duplicate(); }, SQLException.class); - - // // we can create two parallel read only connections and query them, too - try (Connection conn_ro1 = DriverManager.getConnection(jdbc_url, ro_prop); - Connection conn_ro2 = DriverManager.getConnection(jdbc_url, ro_prop)) { - - assertTrue(conn_ro1.isReadOnly()); - assertTrue(conn_ro1.getMetaData().isReadOnly()); - assertTrue(conn_ro2.isReadOnly()); - assertTrue(conn_ro2.getMetaData().isReadOnly()); - - try (Statement stmt1 = conn_ro1.createStatement(); - ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { - rs1.next(); - assertEquals(rs1.getInt(1), 42); - } + // At this time, mixing read and write connections on Windows doesn't work + // Read-only when we already have a read-write + // try (Connection conn = DriverManager.getConnection(jdbc_url, ro_prop); + // Statement stmt1 = conn.createStatement(); + // ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { + // rs1.next(); + // assertEquals(rs1.getInt(1), 42); + // } + + conn_rw.close(); + + assertThrows(conn_rw::createStatement, SQLException.class); + assertThrows(() -> { conn_rw.unwrap(DuckDBConnection.class).duplicate(); }, SQLException.class); + + // // we can create two parallel read only connections and query them, too + try (Connection conn_ro1 = DriverManager.getConnection(jdbc_url, config); + Connection conn_ro2 = DriverManager.getConnection(jdbc_url, config)) { + + assertTrue(conn_ro1.isReadOnly()); + assertTrue(conn_ro1.getMetaData().isReadOnly()); + assertTrue(conn_ro2.isReadOnly()); + assertTrue(conn_ro2.getMetaData().isReadOnly()); + + try (Statement stmt1 = conn_ro1.createStatement(); + ResultSet rs1 = stmt1.executeQuery("SELECT * FROM test")) { + rs1.next(); + assertEquals(rs1.getInt(1), 42); + } - try (Statement stmt2 = conn_ro2.createStatement(); - ResultSet rs2 = stmt2.executeQuery("SELECT * FROM test")) { - rs2.next(); - assertEquals(rs2.getInt(1), 42); + try (Statement stmt2 = conn_ro2.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT * FROM test")) { + rs2.next(); + assertEquals(rs2.getInt(1), 42); + } + } } } } + public static void test_read_only_discrepancy() throws Exception { + Properties config = new Properties(); + config.put(DuckDBDriver.DUCKDB_READONLY_PROPERTY, true); + config.put(DuckDBDriver.DUCKDB_ACCESS_MODE_PROPERTY, DuckDBDriver.DUCKDB_ACCESS_MODE_READ_WRITE); + assertThrows(() -> DriverManager.getConnection(JDBC_URL, config), SQLException.class); + assertThrows(() + -> DriverManager.getConnection(JDBC_URL + ";duckdb.read_only=false;access_mode=READ_ONLY;"), + SQLException.class); + } + public static void test_temporal_types() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); diff --git a/src/test/java/org/duckdb/TestMetadata.java b/src/test/java/org/duckdb/TestMetadata.java index b7301405f..cf6c7d4b8 100644 --- a/src/test/java/org/duckdb/TestMetadata.java +++ b/src/test/java/org/duckdb/TestMetadata.java @@ -22,30 +22,30 @@ public static void test_get_table_types_bug1258() throws Exception { DatabaseMetaData dm = conn.getMetaData(); try (ResultSet rs = dm.getTables(null, null, null, null)) { + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_NAME"), "b"); assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a1"); assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a2"); assertTrue(rs.next()); - assertEquals(rs.getString("TABLE_NAME"), "b"); - assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "c"); assertFalse(rs.next()); } try (ResultSet rs = dm.getTables(null, null, null, new String[] {})) { + assertTrue(rs.next()); + assertEquals(rs.getString("TABLE_NAME"), "b"); assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a1"); assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a2"); assertTrue(rs.next()); - assertEquals(rs.getString("TABLE_NAME"), "b"); - assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "c"); assertFalse(rs.next()); } - try (ResultSet rs = dm.getTables(null, null, null, new String[] {"BASE TABLE"})) { + try (ResultSet rs = dm.getTables(null, null, null, new String[] {"TABLE"})) { assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a1"); assertTrue(rs.next()); @@ -53,7 +53,7 @@ public static void test_get_table_types_bug1258() throws Exception { assertFalse(rs.next()); } - try (ResultSet rs = dm.getTables(null, null, null, new String[] {"BASE TABLE", "VIEW"})) { + try (ResultSet rs = dm.getTables(null, null, null, new String[] {"TABLE", "VIEW"})) { assertTrue(rs.next()); assertEquals(rs.getString("TABLE_NAME"), "a1"); assertTrue(rs.next()); @@ -511,8 +511,8 @@ public static void test_schema_reflection() throws Exception { assertEquals(rs.getString(2), DuckDBConnection.DEFAULT_SCHEMA); assertEquals(rs.getString("TABLE_NAME"), "a"); assertEquals(rs.getString(3), "a"); - assertEquals(rs.getString("TABLE_TYPE"), "BASE TABLE"); - assertEquals(rs.getString(4), "BASE TABLE"); + assertEquals(rs.getString("TABLE_TYPE"), "TABLE"); + assertEquals(rs.getString(4), "TABLE"); assertEquals(rs.getObject("REMARKS"), "a table"); assertEquals(rs.getObject(5), "a table"); assertNull(rs.getObject("TYPE_CAT")); @@ -558,8 +558,8 @@ public static void test_schema_reflection() throws Exception { assertEquals(rs.getString(2), DuckDBConnection.DEFAULT_SCHEMA); assertEquals(rs.getString("TABLE_NAME"), "a"); assertEquals(rs.getString(3), "a"); - assertEquals(rs.getString("TABLE_TYPE"), "BASE TABLE"); - assertEquals(rs.getString(4), "BASE TABLE"); + assertEquals(rs.getString("TABLE_TYPE"), "TABLE"); + assertEquals(rs.getString(4), "TABLE"); assertEquals(rs.getObject("REMARKS"), "a table"); assertEquals(rs.getObject(5), "a table"); assertNull(rs.getObject("TYPE_CAT")); @@ -793,7 +793,7 @@ public static void test_get_tables_param_binding_for_table_types() throws Except } public static void test_get_table_types() throws Exception { - String[] tableTypesArray = new String[] {"BASE TABLE", "LOCAL TEMPORARY", "VIEW"}; + String[] tableTypesArray = new String[] {"TABLE", "LOCAL TEMPORARY", "VIEW"}; List tableTypesList = new ArrayList<>(asList(tableTypesArray)); tableTypesList.sort(Comparator.naturalOrder()); diff --git a/src/test/java/org/duckdb/TestPrepare.java b/src/test/java/org/duckdb/TestPrepare.java index 0fc5984d8..b32c3d79a 100644 --- a/src/test/java/org/duckdb/TestPrepare.java +++ b/src/test/java/org/duckdb/TestPrepare.java @@ -197,6 +197,23 @@ public static void test_statement_creation_bug1268() throws Exception { } } + private static void test_prepare_statement_unsupported_types() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL)) { + conn.prepareStatement("SELECT 42", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY).close(); + assertThrows( + () + -> conn.prepareStatement("SELECT 42", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY), + SQLException.class); + assertThrows( + () + -> conn.prepareStatement("SELECT 42", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE), + SQLException.class); + } + try (Connection conn = DriverManager.getConnection(JDBC_URL + ";access_mode=READ_ONLY")) { + conn.prepareStatement("SELECT 42", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE).close(); + } + } + public static void test_bug4218_prepare_types() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL)) { String query = "SELECT ($1 || $2)";