Skip to content

Commit 9cd3797

Browse files
authored
Merge pull request #866 from zhicwu/fix-readonly
Enable read-only support
2 parents 159e02d + 48ee26e commit 9cd3797

2 files changed

Lines changed: 128 additions & 4 deletions

File tree

clickhouse-jdbc/src/main/java/com/clickhouse/jdbc/internal/ClickHouseConnectionImpl.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ protected static ClickHouseRecord getServerInfo(ClickHouseNode node, ClickHouseR
7272
try (ClickHouseResponse response = newReq.option(ClickHouseClientOption.ASYNC, false)
7373
.option(ClickHouseClientOption.COMPRESS, false).option(ClickHouseClientOption.DECOMPRESS, false)
7474
.option(ClickHouseClientOption.FORMAT, ClickHouseFormat.RowBinaryWithNamesAndTypes)
75-
.query("select currentUser(), timezone(), version() FORMAT RowBinaryWithNamesAndTypes")
75+
.query("select currentUser(), timezone(), version(), "
76+
+ "ifnull((select toUInt8(value) from system.settings where name='readonly'),0) readonly "
77+
+ "FORMAT RowBinaryWithNamesAndTypes")
7678
.execute().get()) {
7779
return response.firstRecord();
7880
} catch (InterruptedException | CancellationException e) {
@@ -122,6 +124,7 @@ protected static ClickHouseRecord getServerInfo(ClickHouseNode node, ClickHouseR
122124
private final TimeZone serverTimeZone;
123125
private final ClickHouseVersion serverVersion;
124126
private final String user;
127+
private final int initialReadOnly;
125128

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

@@ -234,7 +237,9 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
234237
timeZone = config.getServerTimeZone();
235238
version = config.getServerVersion();
236239
if (jdbcConf.isCreateDbIfNotExist()) {
237-
getServerInfo(node, clientRequest, true);
240+
initialReadOnly = getServerInfo(node, clientRequest, true).getValue(3).asInteger();
241+
} else {
242+
initialReadOnly = (int) clientRequest.getSettings().getOrDefault("readonly", 0);
238243
}
239244
} else {
240245
ClickHouseRecord r = getServerInfo(node, clientRequest, jdbcConf.isCreateDbIfNotExist());
@@ -252,6 +257,7 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
252257
}
253258
// tsTimeZone.hasSameRules(ClickHouseValues.UTC_TIMEZONE)
254259
timeZone = "UTC".equals(tz) ? ClickHouseValues.UTC_TIMEZONE : TimeZone.getTimeZone(tz);
260+
initialReadOnly = r.getValue(3).asInteger();
255261

256262
// update request and corresponding config
257263
clientRequest.option(ClickHouseClientOption.SERVER_TIME_ZONE, tz)
@@ -262,7 +268,7 @@ public ClickHouseConnectionImpl(ConnectionInfo connInfo) throws SQLException {
262268
this.closed = false;
263269
this.database = config.getDatabase();
264270
this.clientRequest.use(this.database);
265-
this.readOnly = false;
271+
this.readOnly = initialReadOnly != 0;
266272
this.networkTimeout = 0;
267273
this.rsHoldability = ResultSet.HOLD_CURSORS_OVER_COMMIT;
268274
this.txIsolation = jdbcConf.isJdbcCompliant() ? Connection.TRANSACTION_READ_COMMITTED
@@ -392,7 +398,18 @@ public DatabaseMetaData getMetaData() throws SQLException {
392398
public void setReadOnly(boolean readOnly) throws SQLException {
393399
ensureOpen();
394400

395-
this.readOnly = readOnly;
401+
if (initialReadOnly != 0) {
402+
if (!readOnly) {
403+
throw SqlExceptionUtils.clientError("Cannot change the setting on a read-only connection");
404+
}
405+
} else {
406+
if (readOnly) {
407+
clientRequest.set("readonly", 2);
408+
} else {
409+
clientRequest.removeSetting("readonly");
410+
}
411+
this.readOnly = readOnly;
412+
}
396413
}
397414

398415
@Override

clickhouse-jdbc/src/test/java/com/clickhouse/jdbc/ClickHouseConnectionTest.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clickhouse.jdbc;
22

33
import java.sql.Array;
4+
import java.sql.Connection;
45
import java.sql.ResultSet;
56
import java.sql.SQLException;
67
import java.sql.Statement;
@@ -69,4 +70,110 @@ public void testNonExistDatabase() throws Exception {
6970
}
7071
Assert.assertNotNull(exp, "Should not have SQLException because the database has been created");
7172
}
73+
74+
@Test(groups = "integration")
75+
public void testReadOnly() throws SQLException {
76+
Properties props = new Properties();
77+
props.setProperty("user", "dba");
78+
props.setProperty("password", "dba");
79+
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
80+
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
81+
Assert.assertFalse(stmt.execute(
82+
"drop table if exists test_readonly; drop user if exists readonly1; drop user if exists readonly2; "
83+
+ "create table test_readonly(id String)engine=Memory; "
84+
+ "create user readonly1 IDENTIFIED WITH no_password SETTINGS readonly=1; "
85+
+ "create user readonly2 IDENTIFIED WITH no_password SETTINGS readonly=2; "
86+
+ "grant insert on test_readonly TO readonly1, readonly2"));
87+
conn.setReadOnly(false);
88+
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
89+
conn.setReadOnly(true);
90+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
91+
92+
try (Statement s = conn.createStatement()) {
93+
SQLException exp = null;
94+
try {
95+
s.execute("insert into test_readonly values('readonly1')");
96+
} catch (SQLException e) {
97+
exp = e;
98+
}
99+
Assert.assertNotNull(exp, "Should fail with SQL exception");
100+
Assert.assertEquals(exp.getErrorCode(), 164);
101+
}
102+
103+
conn.setReadOnly(false);
104+
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
105+
106+
try (Statement s = conn.createStatement()) {
107+
Assert.assertFalse(s.execute("insert into test_readonly values('readonly1')"));
108+
}
109+
}
110+
111+
props.clear();
112+
props.setProperty("user", "readonly1");
113+
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
114+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
115+
conn.setReadOnly(true);
116+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
117+
SQLException exp = null;
118+
try {
119+
stmt.execute("insert into test_readonly values('readonly1')");
120+
} catch (SQLException e) {
121+
exp = e;
122+
}
123+
Assert.assertNotNull(exp, "Should fail with SQL exception");
124+
Assert.assertEquals(exp.getErrorCode(), 164);
125+
126+
exp = null;
127+
try {
128+
conn.setReadOnly(true);
129+
stmt.execute("set max_result_rows=5; select 1");
130+
} catch (SQLException e) {
131+
exp = e;
132+
}
133+
Assert.assertNotNull(exp, "Should fail with SQL exception");
134+
Assert.assertEquals(exp.getErrorCode(), 164);
135+
}
136+
137+
props.setProperty("user", "readonly2");
138+
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
139+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
140+
Assert.assertTrue(stmt.execute("set max_result_rows=5; select 1"));
141+
142+
Assert.assertThrows(SQLException.class, () -> conn.setReadOnly(false));
143+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
144+
145+
SQLException exp = null;
146+
try (Statement s = conn.createStatement()) {
147+
Assert.assertFalse(s.execute("insert into test_readonly values('readonly2')"));
148+
} catch (SQLException e) {
149+
exp = e;
150+
}
151+
Assert.assertNotNull(exp, "Should fail with SQL exception");
152+
Assert.assertEquals(exp.getErrorCode(), 164);
153+
154+
conn.setReadOnly(true);
155+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
156+
}
157+
158+
props.setProperty(ClickHouseClientOption.SERVER_TIME_ZONE.getKey(), "UTC");
159+
props.setProperty(ClickHouseClientOption.SERVER_VERSION.getKey(), "21.8");
160+
try (Connection conn = newConnection(props); Statement stmt = conn.createStatement()) {
161+
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
162+
Assert.assertTrue(stmt.execute("set max_result_rows=5; select 1"));
163+
164+
conn.setReadOnly(true);
165+
Assert.assertTrue(conn.isReadOnly(), "Connection should be readonly");
166+
conn.setReadOnly(false);
167+
Assert.assertFalse(conn.isReadOnly(), "Connection should NOT be readonly");
168+
169+
SQLException exp = null;
170+
try (Statement s = conn.createStatement()) {
171+
Assert.assertFalse(s.execute("insert into test_readonly values('readonly2')"));
172+
} catch (SQLException e) {
173+
exp = e;
174+
}
175+
Assert.assertNotNull(exp, "Should fail with SQL exception");
176+
Assert.assertEquals(exp.getErrorCode(), 164);
177+
}
178+
}
72179
}

0 commit comments

Comments
 (0)