Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/org/duckdb/DuckDBConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/org/duckdb/DuckDBDatabaseMetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down
25 changes: 22 additions & 3 deletions src/main/java/org/duckdb/DuckDBDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/duckdb/JdbcUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
143 changes: 83 additions & 60 deletions src/test/java/org/duckdb/TestDuckDBJDBC.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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<Properties> 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();
Expand Down
22 changes: 11 additions & 11 deletions src/test/java/org/duckdb/TestMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,38 @@ 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());
assertEquals(rs.getString("TABLE_NAME"), "a2");
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());
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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<String> tableTypesList = new ArrayList<>(asList(tableTypesArray));
tableTypesList.sort(Comparator.naturalOrder());

Expand Down
17 changes: 17 additions & 0 deletions src/test/java/org/duckdb/TestPrepare.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down