Skip to content

Commit

Permalink
Merge pull request #866 from zhicwu/fix-readonly
Browse files Browse the repository at this point in the history
Enable read-only support
  • Loading branch information
zhicwu authored Mar 14, 2022
2 parents 159e02d + 48ee26e commit 9cd3797
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ protected static ClickHouseRecord getServerInfo(ClickHouseNode node, ClickHouseR
try (ClickHouseResponse response = newReq.option(ClickHouseClientOption.ASYNC, false)
.option(ClickHouseClientOption.COMPRESS, false).option(ClickHouseClientOption.DECOMPRESS, false)
.option(ClickHouseClientOption.FORMAT, ClickHouseFormat.RowBinaryWithNamesAndTypes)
.query("select currentUser(), timezone(), version() FORMAT RowBinaryWithNamesAndTypes")
.query("select currentUser(), timezone(), version(), "
+ "ifnull((select toUInt8(value) from system.settings where name='readonly'),0) readonly "
+ "FORMAT RowBinaryWithNamesAndTypes")
.execute().get()) {
return response.firstRecord();
} catch (InterruptedException | CancellationException e) {
Expand Down Expand Up @@ -122,6 +124,7 @@ protected static ClickHouseRecord getServerInfo(ClickHouseNode node, ClickHouseR
private final TimeZone serverTimeZone;
private final ClickHouseVersion serverVersion;
private final String user;
private final int initialReadOnly;

private final Map<String, Class<?>> typeMap;

Expand Down Expand Up @@ -234,7 +237,9 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
timeZone = config.getServerTimeZone();
version = config.getServerVersion();
if (jdbcConf.isCreateDbIfNotExist()) {
getServerInfo(node, clientRequest, true);
initialReadOnly = getServerInfo(node, clientRequest, true).getValue(3).asInteger();
} else {
initialReadOnly = (int) clientRequest.getSettings().getOrDefault("readonly", 0);
}
} else {
ClickHouseRecord r = getServerInfo(node, clientRequest, jdbcConf.isCreateDbIfNotExist());
Expand All @@ -252,6 +257,7 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
}
// tsTimeZone.hasSameRules(ClickHouseValues.UTC_TIMEZONE)
timeZone = "UTC".equals(tz) ? ClickHouseValues.UTC_TIMEZONE : TimeZone.getTimeZone(tz);
initialReadOnly = r.getValue(3).asInteger();

// update request and corresponding config
clientRequest.option(ClickHouseClientOption.SERVER_TIME_ZONE, tz)
Expand All @@ -262,7 +268,7 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
this.closed = false;
this.database = config.getDatabase();
this.clientRequest.use(this.database);
this.readOnly = false;
this.readOnly = initialReadOnly != 0;
this.networkTimeout = 0;
this.rsHoldability = ResultSet.HOLD_CURSORS_OVER_COMMIT;
this.txIsolation = jdbcConf.isJdbcCompliant() ? Connection.TRANSACTION_READ_COMMITTED
Expand Down Expand Up @@ -392,7 +398,18 @@ public DatabaseMetaData getMetaData() throws SQLException {
public void setReadOnly(boolean readOnly) throws SQLException {
ensureOpen();

this.readOnly = readOnly;
if (initialReadOnly != 0) {
if (!readOnly) {
throw SqlExceptionUtils.clientError("Cannot change the setting on a read-only connection");
}
} else {
if (readOnly) {
clientRequest.set("readonly", 2);
} else {
clientRequest.removeSetting("readonly");
}
this.readOnly = readOnly;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.clickhouse.jdbc;

import java.sql.Array;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
Expand Down Expand Up @@ -69,4 +70,110 @@ public void testNonExistDatabase() throws Exception {
}
Assert.assertNotNull(exp, "Should not have SQLException because the database has been created");
}

@Test(groups = "integration")
public void testReadOnly() throws SQLException {
Properties props = new Properties();
props.setProperty("user", "dba");
props.setProperty("password", "dba");
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
Assert.assertFalse(stmt.execute(
"drop table if exists test_readonly; drop user if exists readonly1; drop user if exists readonly2; "
+ "create table test_readonly(id String)engine=Memory; "
+ "create user readonly1 IDENTIFIED WITH no_password SETTINGS readonly=1; "
+ "create user readonly2 IDENTIFIED WITH no_password SETTINGS readonly=2; "
+ "grant insert on test_readonly TO readonly1, readonly2"));
conn.setReadOnly(false);
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
conn.setReadOnly(true);
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");

try (Statement s = conn.createStatement()) {
SQLException exp = null;
try {
s.execute("insert into test_readonly values('readonly1')");
} catch (SQLException e) {
exp = e;
}
Assert.assertNotNull(exp, "Should fail with SQL exception");
Assert.assertEquals(exp.getErrorCode(), 164);
}

conn.setReadOnly(false);
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");

try (Statement s = conn.createStatement()) {
Assert.assertFalse(s.execute("insert into test_readonly values('readonly1')"));
}
}

props.clear();
props.setProperty("user", "readonly1");
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
conn.setReadOnly(true);
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
SQLException exp = null;
try {
stmt.execute("insert into test_readonly values('readonly1')");
} catch (SQLException e) {
exp = e;
}
Assert.assertNotNull(exp, "Should fail with SQL exception");
Assert.assertEquals(exp.getErrorCode(), 164);

exp = null;
try {
conn.setReadOnly(true);
stmt.execute("set max_result_rows=5; select 1");
} catch (SQLException e) {
exp = e;
}
Assert.assertNotNull(exp, "Should fail with SQL exception");
Assert.assertEquals(exp.getErrorCode(), 164);
}

props.setProperty("user", "readonly2");
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
Assert.assertTrue(stmt.execute("set max_result_rows=5; select 1"));

Assert.assertThrows(SQLException.class, () -> conn.setReadOnly(false));
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");

SQLException exp = null;
try (Statement s = conn.createStatement()) {
Assert.assertFalse(s.execute("insert into test_readonly values('readonly2')"));
} catch (SQLException e) {
exp = e;
}
Assert.assertNotNull(exp, "Should fail with SQL exception");
Assert.assertEquals(exp.getErrorCode(), 164);

conn.setReadOnly(true);
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
}

props.setProperty(ClickHouseClientOption.SERVER_TIME_ZONE.getKey(), "UTC");
props.setProperty(ClickHouseClientOption.SERVER_VERSION.getKey(), "21.8");
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
Assert.assertTrue(stmt.execute("set max_result_rows=5; select 1"));

conn.setReadOnly(true);
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
conn.setReadOnly(false);
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");

SQLException exp = null;
try (Statement s = conn.createStatement()) {
Assert.assertFalse(s.execute("insert into test_readonly values('readonly2')"));
} catch (SQLException e) {
exp = e;
}
Assert.assertNotNull(exp, "Should fail with SQL exception");
Assert.assertEquals(exp.getErrorCode(), 164);
}
}
}

0 comments on commit 9cd3797

Please sign in to comment.