diff --git a/pom.xml b/pom.xml index be62aa8..bb1ea1b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 4.0.0 nl.topicus spanner-jdbc - 1.1.4-SNAPSHOT + 1.2-SNAPSHOT spanner-jdbc JDBC Driver for Google Cloud Spanner https://github.com/olavloite/spanner-jdbc @@ -41,12 +41,12 @@ com.google.cloud google-cloud-spanner - 0.57.0-beta + 0.66.0-beta com.google.cloud google-cloud-storage - 1.39.0 + 1.48.0 org.json diff --git a/src/main/java/nl/topicus/jdbc/CloudSpannerConnection.java b/src/main/java/nl/topicus/jdbc/CloudSpannerConnection.java index 5341e54..dad59d4 100644 --- a/src/main/java/nl/topicus/jdbc/CloudSpannerConnection.java +++ b/src/main/java/nl/topicus/jdbc/CloudSpannerConnection.java @@ -137,6 +137,9 @@ public boolean equals(Object o) { private final Properties suppliedProperties; + private boolean originalUseServerDML; + private boolean useServerDML; + private boolean originalAllowExtendedMode; private boolean allowExtendedMode; @@ -657,6 +660,25 @@ public Properties getSuppliedProperties() { return suppliedProperties; } + @Override + public boolean isUseServerDML() { + return useServerDML; + } + + @Override + public int setUseServerDML(boolean useServerDML) { + this.useServerDML = useServerDML; + return 1; + } + + boolean isOriginalUseServerDML() { + return originalUseServerDML; + } + + void setOriginalUseServerDML(boolean useServerDML) { + this.originalUseServerDML = useServerDML; + } + @Override public boolean isAllowExtendedMode() { return allowExtendedMode; @@ -761,6 +783,10 @@ public int resetDynamicConnectionProperty(String propertyName) throws SQLExcepti } private Supplier getOriginalValueGetter(String propertyName) { + if (propertyName.equalsIgnoreCase( + ConnectionProperties.getPropertyName(ConnectionProperties.USE_SERVER_DML))) { + return this::isOriginalUseServerDML; + } if (propertyName.equalsIgnoreCase( ConnectionProperties.getPropertyName(ConnectionProperties.ALLOW_EXTENDED_MODE))) { return this::isOriginalAllowExtendedMode; @@ -791,6 +817,10 @@ static interface SqlFunction { } private SqlFunction getPropertySetter(String propertyName) { + if (propertyName.equalsIgnoreCase( + ConnectionProperties.getPropertyName(ConnectionProperties.USE_SERVER_DML))) { + return this::setUseServerDML; + } if (propertyName.equalsIgnoreCase( ConnectionProperties.getPropertyName(ConnectionProperties.ALLOW_EXTENDED_MODE))) { return this::setAllowExtendedMode; @@ -823,6 +853,11 @@ public ResultSet getDynamicConnectionProperties(CloudSpannerStatement statement) public ResultSet getDynamicConnectionProperty(CloudSpannerStatement statement, String propertyName) throws SQLException { Map values = new HashMap<>(); + if (propertyName == null || propertyName.equalsIgnoreCase( + ConnectionProperties.getPropertyName(ConnectionProperties.USE_SERVER_DML))) { + values.put(ConnectionProperties.getPropertyName(ConnectionProperties.USE_SERVER_DML), + String.valueOf(isUseServerDML())); + } if (propertyName == null || propertyName.equalsIgnoreCase( ConnectionProperties.getPropertyName(ConnectionProperties.ALLOW_EXTENDED_MODE))) { values.put(ConnectionProperties.getPropertyName(ConnectionProperties.ALLOW_EXTENDED_MODE), diff --git a/src/main/java/nl/topicus/jdbc/CloudSpannerDriver.java b/src/main/java/nl/topicus/jdbc/CloudSpannerDriver.java index 61432b0..5667244 100644 --- a/src/main/java/nl/topicus/jdbc/CloudSpannerDriver.java +++ b/src/main/java/nl/topicus/jdbc/CloudSpannerDriver.java @@ -143,6 +143,8 @@ public CloudSpannerConnection connect(String url, Properties info) throws SQLExc connection.setSimulateProductName(properties.productName); connection.setSimulateMajorVersion(properties.majorVersion); connection.setSimulateMinorVersion(properties.minorVersion); + connection.setUseServerDML(properties.useServerDML); + connection.setOriginalUseServerDML(properties.useServerDML); connection.setAllowExtendedMode(properties.allowExtendedMode); connection.setOriginalAllowExtendedMode(properties.allowExtendedMode); connection.setAsyncDdlOperations(properties.asyncDdlOperations); diff --git a/src/main/java/nl/topicus/jdbc/ConnectionProperties.java b/src/main/java/nl/topicus/jdbc/ConnectionProperties.java index 14b37f0..f877e2b 100644 --- a/src/main/java/nl/topicus/jdbc/ConnectionProperties.java +++ b/src/main/java/nl/topicus/jdbc/ConnectionProperties.java @@ -7,7 +7,7 @@ import nl.topicus.jdbc.exception.CloudSpannerSQLException; final class ConnectionProperties { - public static final int NUMBER_OF_PROPERTIES = 14; + public static final int NUMBER_OF_PROPERTIES = 15; static String getPropertyName(String propertyPart) { return propertyPart.substring(0, propertyPart.length() - 1); @@ -27,6 +27,7 @@ static String getPropertyName(String propertyPart) { static final String SIMULATE_PRODUCT_MAJOR_VERSION = "SimulateProductMajorVersion="; static final String SIMULATE_PRODUCT_MINOR_VERSION = "SimulateProductMinorVersion="; + static final String USE_SERVER_DML = "UseServerDML="; static final String ALLOW_EXTENDED_MODE = "AllowExtendedMode="; static final String ASYNC_DDL_OPERATIONS = "AsyncDdlOperations="; static final String AUTO_BATCH_DDL_OPERATIONS = "AutoBatchDdlOperations="; @@ -42,6 +43,7 @@ static String getPropertyName(String propertyPart) { String productName = null; Integer majorVersion = null; Integer minorVersion = null; + boolean useServerDML = false; boolean allowExtendedMode = false; boolean asyncDdlOperations = false; boolean autoBatchDdlOperations = false; @@ -76,6 +78,8 @@ else if (conPartLower.startsWith(SIMULATE_PRODUCT_MAJOR_VERSION.toLowerCase())) else if (conPartLower.startsWith(SIMULATE_PRODUCT_MINOR_VERSION.toLowerCase())) res.minorVersion = parseInteger(conPart.substring(SIMULATE_PRODUCT_MINOR_VERSION.length())); + else if (conPartLower.startsWith(USE_SERVER_DML.toLowerCase())) + res.useServerDML = Boolean.valueOf(conPart.substring(USE_SERVER_DML.length())); else if (conPartLower.startsWith(ALLOW_EXTENDED_MODE.toLowerCase())) res.allowExtendedMode = Boolean.valueOf(conPart.substring(ALLOW_EXTENDED_MODE.length())); else if (conPartLower.startsWith(ASYNC_DDL_OPERATIONS.toLowerCase())) @@ -142,6 +146,9 @@ void setAdditionalConnectionProperties(Properties info) { SIMULATE_PRODUCT_MINOR_VERSION .substring(0, SIMULATE_PRODUCT_MINOR_VERSION.length() - 1).toLowerCase(), defaultString(minorVersion))); + useServerDML = Boolean.valueOf(lowerCaseInfo.getProperty( + USE_SERVER_DML.substring(0, USE_SERVER_DML.length() - 1).toLowerCase(), + String.valueOf(useServerDML))); allowExtendedMode = Boolean.valueOf(lowerCaseInfo.getProperty( ALLOW_EXTENDED_MODE.substring(0, ALLOW_EXTENDED_MODE.length() - 1).toLowerCase(), String.valueOf(allowExtendedMode))); @@ -201,34 +208,38 @@ DriverPropertyInfo[] getPropertyInfo() { defaultString(minorVersion)); res[7].description = "Use this property to make the driver return a different minor version number, for example if you are using a framework like Spring that use this property to determine how to a generate data model for Spring Batch"; - res[8] = + res[8] = new DriverPropertyInfo(USE_SERVER_DML.substring(0, USE_SERVER_DML.length() - 1), + String.valueOf(useServerDML)); + res[8].description = + "Should the user send DML statements to the server, or should it try to parse and process these locally."; + res[9] = new DriverPropertyInfo(ALLOW_EXTENDED_MODE.substring(0, ALLOW_EXTENDED_MODE.length() - 1), String.valueOf(allowExtendedMode)); - res[8].description = + res[9].description = "Allow the driver to enter 'extended' mode for bulk operations. A value of false (default) indicates that the driver should never enter extended mode. If this property is set to true, the driver will execute all bulk DML-operations in a separate transaction when the number of records affected is greater than what will exceed the limitations of Cloud Spanner."; - res[9] = + res[10] = new DriverPropertyInfo(ASYNC_DDL_OPERATIONS.substring(0, ASYNC_DDL_OPERATIONS.length() - 1), String.valueOf(asyncDdlOperations)); - res[9].description = + res[10].description = "Run DDL-operations (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.) in asynchronous mode. When set to true, DDL-statements will be checked for correct syntax and other basic checks before the call returns. It can take up to several minutes before the statement has actually finished executing. The status of running DDL-operations can be queried by issuing a SHOW_DDL_OPERATIONS statement. DDL-operations that have finished can be cleared from this view by issuing a CLEAN_DDL_OPERATIONS statement."; - res[10] = new DriverPropertyInfo( + res[11] = new DriverPropertyInfo( AUTO_BATCH_DDL_OPERATIONS.substring(0, AUTO_BATCH_DDL_OPERATIONS.length() - 1), String.valueOf(autoBatchDdlOperations)); - res[10].description = + res[11].description = "Automatically batch DDL-operations (CREATE TABLE, ALTER TABLE, DROP TABLE, etc.). When set to true, DDL-statements that are submitted through a Statement (not PreparedStatement) will automatically be batched together and only executed after an EXECUTE_DDL_BATCH statement. This property can be used in combination with the AsyncDdlOperations property to run a batch asynchronously or synchronously."; - res[11] = new DriverPropertyInfo( + res[12] = new DriverPropertyInfo( REPORT_DEFAULT_SCHEMA_AS_NULL.substring(0, REPORT_DEFAULT_SCHEMA_AS_NULL.length() - 1), String.valueOf(reportDefaultSchemaAsNull)); - res[11].description = + res[12].description = "Report the default schema and catalog as null (true) or as an empty string (false)."; - res[12] = + res[13] = new DriverPropertyInfo(BATCH_READ_ONLY_MODE.substring(0, BATCH_READ_ONLY_MODE.length() - 1), String.valueOf(batchReadOnlyMode)); - res[12].description = + res[13].description = "Run queries in batch-read-only-mode. Use this mode when downloading large amounts of data from Cloud Spanner in combination with the methods Statement#execute(String) or PreparedStatement#execute()"; - res[13] = new DriverPropertyInfo(USE_CUSTOM_HOST.substring(0, USE_CUSTOM_HOST.length() - 1), + res[14] = new DriverPropertyInfo(USE_CUSTOM_HOST.substring(0, USE_CUSTOM_HOST.length() - 1), String.valueOf(useCustomHost)); - res[13].description = + res[14].description = "Connect to a custom host instead of https://spanner.googleapis.com. This enables the use of a local emulator instead of Google Cloud Spanner"; return res; diff --git a/src/main/java/nl/topicus/jdbc/ICloudSpannerConnection.java b/src/main/java/nl/topicus/jdbc/ICloudSpannerConnection.java index c283a8b..b665d86 100644 --- a/src/main/java/nl/topicus/jdbc/ICloudSpannerConnection.java +++ b/src/main/java/nl/topicus/jdbc/ICloudSpannerConnection.java @@ -24,6 +24,10 @@ public interface ICloudSpannerConnection extends Connection { public Properties getSuppliedProperties(); + public boolean isUseServerDML(); + + public int setUseServerDML(boolean useServerDML); + public boolean isAllowExtendedMode(); public int setAllowExtendedMode(boolean allowExtendedMode); diff --git a/src/main/java/nl/topicus/jdbc/statement/CloudSpannerPreparedStatement.java b/src/main/java/nl/topicus/jdbc/statement/CloudSpannerPreparedStatement.java index 47548b5..857b9b0 100644 --- a/src/main/java/nl/topicus/jdbc/statement/CloudSpannerPreparedStatement.java +++ b/src/main/java/nl/topicus/jdbc/statement/CloudSpannerPreparedStatement.java @@ -15,6 +15,7 @@ import com.google.cloud.spanner.Mutation.WriteBuilder; import com.google.cloud.spanner.Partition; import com.google.cloud.spanner.ReadContext; +import com.google.cloud.spanner.ValueBinder; import com.google.rpc.Code; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; @@ -78,6 +79,7 @@ public class CloudSpannerPreparedStatement extends AbstractCloudSpannerPreparedS private boolean forceUpdate; private List batchMutations = new ArrayList<>(); + private List batchStatements = new ArrayList<>(); public CloudSpannerPreparedStatement(String sql, CloudSpannerConnection connection, DatabaseClient dbClient) { @@ -154,6 +156,45 @@ public ResultSet executeQuery() throws SQLException { Code.INVALID_ARGUMENT); } + private com.google.cloud.spanner.Statement.Builder createDMLBuilder(String sql) { + String namedSql = convertPositionalParametersToNamedParameters(sql); + com.google.cloud.spanner.Statement.Builder builder = + com.google.cloud.spanner.Statement.newBuilder(namedSql); + setDMLParameters(namedSql, builder); + + return builder; + } + + private void setDMLParameters(String sql, com.google.cloud.spanner.Statement.Builder builder) { + Character currentEndChar = null; + int i = 0; + int parIndex = 1; + + while (i < sql.length()) { + char c = sql.charAt(i); + if (currentEndChar == null) { + if (c == '\'' || c == '"' || c == '{') { + currentEndChar = c == '{' ? '}' : c; + } else if (c == '@') { + ValueBinder binder = + builder.bind("p" + parIndex); + setParamValue(binder, parIndex); + parIndex++; + } + } else if (c == currentEndChar) { + currentEndChar = null; + } + i++; + } + } + + private void setParamValue(ValueBinder binder, int parIndex) { + ValueBinderExpressionVisitorAdapter adapter = + new ValueBinderExpressionVisitorAdapter<>(getParameterStore(), binder, null); + adapter.setValue(getParameterStore().getParameter(parIndex), + getParameterStore().getType(parIndex)); + } + private com.google.cloud.spanner.Statement.Builder createSelectBuilder(Statement statement, String sql) { String namedSql = convertPositionalParametersToNamedParameters(sql); @@ -330,26 +371,52 @@ public void addBatch() throws SQLException { if (isSelectStatement(sqlTokens)) { throw new SQLFeatureNotSupportedException("SELECT statements may not be batched"); } - Mutations mutations = createMutations(); - batchMutations.add(mutations); + if (getConnection().isUseServerDML()) { + if (!batchMutations.isEmpty()) { + throw new CloudSpannerSQLException( + "Mixing batched mutations and dml statements is not allowed", Code.FAILED_PRECONDITION); + } + batchStatements.add(createDMLBuilder(sql).build()); + } else { + if (!batchStatements.isEmpty()) { + throw new CloudSpannerSQLException( + "Mixing batched mutations and dml statements is not allowed", Code.FAILED_PRECONDITION); + } + Mutations mutations = createMutations(); + batchMutations.add(mutations); + } getParameterStore().clearParameters(); } @Override public void clearBatch() throws SQLException { batchMutations.clear(); + batchStatements.clear(); getParameterStore().clearParameters(); } @Override public int[] executeBatch() throws SQLException { - int[] res = new int[batchMutations.size()]; - int index = 0; - for (Mutations mutation : batchMutations) { - res[index] = (int) writeMutations(mutation); - index++; + int[] res; + if (!batchMutations.isEmpty()) { + res = new int[batchMutations.size()]; + int index = 0; + for (Mutations mutation : batchMutations) { + res[index] = (int) writeMutations(mutation); + index++; + } + batchMutations.clear(); + } else if (!batchStatements.isEmpty()) { + res = new int[batchStatements.size()]; + int index = 0; + for (com.google.cloud.spanner.Statement statement : batchStatements) { + res[index] = (int) getConnection().getTransaction().executeUpdate(statement); + index++; + } + batchStatements.clear(); + } else { + res = new int[0]; } - batchMutations.clear(); getParameterStore().clearParameters(); return res; } @@ -364,8 +431,12 @@ public int executeUpdate() throws SQLException { String ddl = formatDDLStatement(sql); return executeDDL(ddl); } - Mutations mutations = createMutations(); - return (int) writeMutations(mutations); + if (getConnection().isUseServerDML()) { + return (int) getConnection().getTransaction().executeUpdate(createDMLBuilder(sql).build()); + } else { + Mutations mutations = createMutations(); + return (int) writeMutations(mutations); + } } private Mutations createMutations() throws SQLException { diff --git a/src/main/java/nl/topicus/jdbc/transaction/CloudSpannerTransaction.java b/src/main/java/nl/topicus/jdbc/transaction/CloudSpannerTransaction.java index 393ace9..9128fa6 100644 --- a/src/main/java/nl/topicus/jdbc/transaction/CloudSpannerTransaction.java +++ b/src/main/java/nl/topicus/jdbc/transaction/CloudSpannerTransaction.java @@ -234,6 +234,14 @@ public void buffer(Iterable mutations) { transactionThread.buffer(mutations); } + @Override + public long executeUpdate(Statement statement) { + checkTransaction(); + if (transactionThread == null) + throw new IllegalStateException("Updates are not allowed in read-only mode"); + return transactionThread.executeUpdate(statement); + } + @Override public ResultSet executeQuery(Statement statement, QueryOption... options) { checkTransaction(); diff --git a/src/main/java/nl/topicus/jdbc/transaction/TransactionThread.java b/src/main/java/nl/topicus/jdbc/transaction/TransactionThread.java index e2c78a6..9b44cd4 100644 --- a/src/main/java/nl/topicus/jdbc/transaction/TransactionThread.java +++ b/src/main/java/nl/topicus/jdbc/transaction/TransactionThread.java @@ -50,6 +50,44 @@ private enum TransactionStopStatement { COMMIT, ROLLBACK, PREPARE, COMMIT_PREPARED, ROLLBACK_PREPARED; } + private enum StatementType { + QUERY, UPDATE; + } + + private static class GeneralStatement { + private final Statement statement; + private final StatementType type; + + private GeneralStatement(Statement statement, StatementType type) { + this.statement = statement; + this.type = type; + } + } + + private static class StatementResult { + private final ResultSet resultSet; + private final Long updateCount; + private final RuntimeException exception; + + private static StatementResult of(ResultSet resultSet) { + return new StatementResult(resultSet, null, null); + } + + private static StatementResult of(Long updateCount) { + return new StatementResult(null, updateCount, null); + } + + private static StatementResult of(RuntimeException exception) { + return new StatementResult(null, null, exception); + } + + private StatementResult(ResultSet resultSet, Long updateCount, RuntimeException exception) { + this.resultSet = resultSet; + this.updateCount = updateCount; + this.exception = exception; + } + } + private final Logger logger; private final StackTraceElement[] stackTraceElements; @@ -83,9 +121,9 @@ private enum TransactionStopStatement { private Map savepoints = new HashMap<>(); - private BlockingQueue statements = new LinkedBlockingQueue<>(); + private BlockingQueue statements = new LinkedBlockingQueue<>(); - private BlockingQueue resultSets = new LinkedBlockingQueue<>(); + private BlockingQueue statementResults = new LinkedBlockingQueue<>(); private static int threadInitNumber; @@ -124,11 +162,28 @@ public TransactionStatus run(TransactionContext transaction) throws Exception { status = TransactionStatus.RUNNING; while (!stop) { try { - Statement statement = statements.poll(5, TimeUnit.SECONDS); + GeneralStatement statement = statements.poll(5, TimeUnit.SECONDS); if (statement != null) { - String sql = statement.getSql(); + String sql = statement.statement.getSql(); if (!stopStatementStrings.contains(sql)) { - resultSets.put(transaction.executeQuery(statement)); + try { + switch (statement.type) { + case QUERY: + statementResults.put( + StatementResult.of(transaction.executeQuery(statement.statement))); + break; + case UPDATE: + statementResults.put( + StatementResult.of(transaction.executeUpdate(statement.statement))); + break; + default: + throw new IllegalStateException( + "Unknown statement type: " + statement.type); + } + } catch (RuntimeException e) { + statementResults.put(StatementResult.of(e)); + throw e; + } } } else { // keep alive @@ -243,8 +298,15 @@ private void logStartStackTrace() { ResultSet executeQuery(Statement statement) { try { - statements.put(statement); - return resultSets.take(); + statements.put(new GeneralStatement(statement, StatementType.QUERY)); + StatementResult res = statementResults.take(); + if (res.exception != null) { + throw res.exception; + } else if (res.resultSet != null) { + return res.resultSet; + } else { + throw new IllegalStateException("Statement did not return a resultset"); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new QueryException("Query execution interrupted", e); @@ -271,6 +333,23 @@ void buffer(Iterable mutations) { buffer(it.next()); } + long executeUpdate(Statement statement) { + try { + statements.put(new GeneralStatement(statement, StatementType.UPDATE)); + StatementResult res = statementResults.take(); + if (res.exception != null) { + throw res.exception; + } else if (res.updateCount != null) { + return res.updateCount; + } else { + throw new IllegalStateException("Statement did not return an update count"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new QueryException("Update execution interrupted", e); + } + } + void setSavepoint(Savepoint savepoint) { Preconditions.checkNotNull(savepoint); savepoints.put(savepoint, mutations.size()); @@ -341,7 +420,7 @@ private void stopTransaction(TransactionStopStatement statement) throws SQLExcep stop = true; // Add a statement object in order to get the transaction thread to // proceed - statements.add(Statement.of(statement.name())); + statements.add(new GeneralStatement(Statement.of(statement.name()), StatementType.QUERY)); synchronized (monitor) { while (!stopped || status == TransactionStatus.NOT_STARTED || status == TransactionStatus.RUNNING) { diff --git a/src/test/java/nl/topicus/jdbc/CloudSpannerConnectionTest.java b/src/test/java/nl/topicus/jdbc/CloudSpannerConnectionTest.java index 121acf9..8954f4b 100644 --- a/src/test/java/nl/topicus/jdbc/CloudSpannerConnectionTest.java +++ b/src/test/java/nl/topicus/jdbc/CloudSpannerConnectionTest.java @@ -255,7 +255,8 @@ public void testIsValidAfterClose() throws SQLException { public void testGetDynamicConnectionProperties() throws SQLException { Properties properties = createDefaultProperties(); try (CloudSpannerConnection connection = createConnection(properties)) { - testGetDynamicConnectionProperty(connection, null, 5); + testGetDynamicConnectionProperty(connection, null, 6); + testGetDynamicConnectionProperty(connection, "USESERVERDML", 1); testGetDynamicConnectionProperty(connection, "ALLOWEXTENDEDMODE", 1); testGetDynamicConnectionProperty(connection, "ASYNCDDLOPERATIONS", 1); testGetDynamicConnectionProperty(connection, "AUTOBATCHDDLOPERATIONS", 1); diff --git a/src/test/java/nl/topicus/jdbc/CloudSpannerDriverTest.java b/src/test/java/nl/topicus/jdbc/CloudSpannerDriverTest.java index 154f79d..cfda655 100644 --- a/src/test/java/nl/topicus/jdbc/CloudSpannerDriverTest.java +++ b/src/test/java/nl/topicus/jdbc/CloudSpannerDriverTest.java @@ -159,7 +159,8 @@ public void driverPropertyInfoWithoutValues() throws SQLException { driver.getPropertyInfo("jdbc:cloudspanner://localhost", null); assertEquals(ConnectionProperties.NUMBER_OF_PROPERTIES, properties.length); for (DriverPropertyInfo property : properties) { - if (property.name.equals("AllowExtendedMode") || property.name.equals("AsyncDdlOperations") + if (property.name.equals("UseServerDML") || property.name.equals("AllowExtendedMode") + || property.name.equals("AsyncDdlOperations") || property.name.equals("AutoBatchDdlOperations") || property.name.equals("BatchReadOnlyMode") || property.name.equals("UseCustomHost")) assertEquals("false", property.value); diff --git a/src/test/java/nl/topicus/jdbc/CustomStatementsTest.java b/src/test/java/nl/topicus/jdbc/CustomStatementsTest.java index c876519..d8cce04 100644 --- a/src/test/java/nl/topicus/jdbc/CustomStatementsTest.java +++ b/src/test/java/nl/topicus/jdbc/CustomStatementsTest.java @@ -28,8 +28,8 @@ @Category(UnitTest.class) public class CustomStatementsTest { private static final List CONNECTION_PROPERTIES = - Arrays.asList("AllowExtendedMode", "AsyncDdlOperations", "AutoBatchDdlOperations", - "ReportDefaultSchemaAsNull", "BatchReadOnlyMode"); + Arrays.asList("UseServerDML", "AllowExtendedMode", "AsyncDdlOperations", + "AutoBatchDdlOperations", "ReportDefaultSchemaAsNull", "BatchReadOnlyMode"); private Connection connection; diff --git a/src/test/java/nl/topicus/jdbc/test/integration/specific/AbstractSpecificIntegrationTest.java b/src/test/java/nl/topicus/jdbc/test/integration/specific/AbstractSpecificIntegrationTest.java index ec27f33..3392fac 100644 --- a/src/test/java/nl/topicus/jdbc/test/integration/specific/AbstractSpecificIntegrationTest.java +++ b/src/test/java/nl/topicus/jdbc/test/integration/specific/AbstractSpecificIntegrationTest.java @@ -145,10 +145,13 @@ public void setupConnection() throws SQLException { url.append(";Database=").append(DATABASE_ID); url.append(";PvtKeyPath=").append(credentialsPath); url.append(";UseCustomHost=true"); + appendConnectionUrl(url); connection = DriverManager.getConnection(url.toString()); connection.setAutoCommit(false); } + protected void appendConnectionUrl(StringBuilder url) {} + @After public void closeConnection() throws SQLException { if (connection != null) { diff --git a/src/test/java/nl/topicus/jdbc/test/integration/specific/UseServerDmlIT.java b/src/test/java/nl/topicus/jdbc/test/integration/specific/UseServerDmlIT.java new file mode 100644 index 0000000..a67a2a1 --- /dev/null +++ b/src/test/java/nl/topicus/jdbc/test/integration/specific/UseServerDmlIT.java @@ -0,0 +1,147 @@ +package nl.topicus.jdbc.test.integration.specific; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; +import nl.topicus.jdbc.test.category.IntegrationTest; +import nl.topicus.jdbc.util.EnglishNumberToWords; + +@Category(IntegrationTest.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class UseServerDmlIT extends AbstractSpecificIntegrationTest { + + @Override + protected void appendConnectionUrl(StringBuilder url) { + url.append(";UseServerDML=true"); + } + + @Before + public void setupTable() throws SQLException { + getConnection().createStatement().execute( + "create table if not exists test (id int64 not null, name string(100)) primary key (id)"); + } + + @Test + public void test1_InsertWithoutParameters() throws SQLException { + int updated = getConnection().createStatement() + .executeUpdate("insert into test (id, name) values (1, 'one')"); + assertEquals(1, updated); + getConnection().commit(); + try (ResultSet rs = getConnection().createStatement().executeQuery("select * from test")) { + int count = 0; + while (rs.next()) { + count++; + } + assertEquals(1, count); + } + } + + @Test + public void test2_InsertWithParameters() throws SQLException { + try (PreparedStatement ps = + getConnection().prepareStatement("insert into test (id, name) values (?, ?)")) { + ps.setLong(1, 2L); + ps.setString(2, "two"); + ps.execute(); + assertEquals(1, ps.getUpdateCount()); + getConnection().commit(); + } + try (ResultSet rs = getConnection().createStatement().executeQuery("select * from test")) { + int count = 0; + while (rs.next()) { + count++; + } + assertEquals(2, count); + } + } + + @Test + public void test3_UpdateWithoutParameters() throws SQLException { + int updated = + getConnection().createStatement().executeUpdate("update test set name='to' where id=2"); + assertEquals(1, updated); + try (ResultSet rs = + getConnection().createStatement().executeQuery("select * from test where id=2")) { + assertTrue(rs.next()); + assertEquals("to", rs.getString("name")); + assertFalse(rs.next()); + } + getConnection().commit(); + } + + @Test + public void test4_UpdateWithParameters() throws SQLException { + try (PreparedStatement ps = + getConnection().prepareStatement("update test set name=? where id=?")) { + ps.setString(1, "en"); + ps.setLong(2, 1L); + int updated = ps.executeUpdate(); + assertEquals(1, updated); + try (ResultSet rs = + getConnection().createStatement().executeQuery("select * from test where id=1")) { + assertTrue(rs.next()); + assertEquals("en", rs.getString("name")); + assertFalse(rs.next()); + } + getConnection().commit(); + } + } + + @Test + public void test5_DeleteWithoutParameters() throws SQLException { + int updated = getConnection().createStatement().executeUpdate("delete from test where id=2"); + assertEquals(1, updated); + try (ResultSet rs = + getConnection().createStatement().executeQuery("select * from test where id=2")) { + assertFalse(rs.next()); + } + getConnection().commit(); + } + + @Test + public void test6_DeleteWithParameters() throws SQLException { + try (PreparedStatement ps = getConnection().prepareStatement("delete from test where id=?")) { + ps.setLong(1, 1L); + int updated = ps.executeUpdate(); + assertEquals(1, updated); + try (ResultSet rs = + getConnection().createStatement().executeQuery("select * from test where id=1")) { + assertFalse(rs.next()); + } + getConnection().commit(); + } + } + + @Test + public void test7_BatchedInsert() throws SQLException { + try (PreparedStatement ps = + getConnection().prepareStatement("insert into test (id, name) values (?, ?)")) { + for (long l = 1L; l <= 10L; l++) { + ps.setLong(1, l); + ps.setString(2, EnglishNumberToWords.convert(l)); + ps.addBatch(); + } + ps.executeBatch(); + } + try (ResultSet rs = + getConnection().createStatement().executeQuery("select * from test order by id")) { + int count = 0; + while (rs.next()) { + count++; + assertEquals(count, rs.getInt(1)); + assertEquals(EnglishNumberToWords.convert(count), rs.getString(2)); + } + assertEquals(10, count); + } + getConnection().commit(); + } + +} diff --git a/src/test/java/nl/topicus/jdbc/transaction/TransactionThreadTest.java b/src/test/java/nl/topicus/jdbc/transaction/TransactionThreadTest.java index 5c86c00..c00388f 100644 --- a/src/test/java/nl/topicus/jdbc/transaction/TransactionThreadTest.java +++ b/src/test/java/nl/topicus/jdbc/transaction/TransactionThreadTest.java @@ -55,6 +55,11 @@ public Timestamp getCommitTimestamp() { return commitTimestamp; } + @Override + public TransactionRunner allowNestedTransaction() { + return this; + } + } @FunctionalInterface diff --git a/src/test/java/nl/topicus/jdbc/util/EnglishNumberToWords.java b/src/test/java/nl/topicus/jdbc/util/EnglishNumberToWords.java new file mode 100644 index 0000000..345ec23 --- /dev/null +++ b/src/test/java/nl/topicus/jdbc/util/EnglishNumberToWords.java @@ -0,0 +1,103 @@ +package nl.topicus.jdbc.util; + +import java.text.DecimalFormat; + +public class EnglishNumberToWords { + + private static final String[] tensNames = {"", " ten", " twenty", " thirty", " forty", " fifty", + " sixty", " seventy", " eighty", " ninety"}; + + private static final String[] numNames = {"", " one", " two", " three", " four", " five", " six", + " seven", " eight", " nine", " ten", " eleven", " twelve", " thirteen", " fourteen", + " fifteen", " sixteen", " seventeen", " eighteen", " nineteen"}; + + private EnglishNumberToWords() {} + + private static String convertLessThanOneThousand(int number) { + String soFar; + + if (number % 100 < 20) { + soFar = numNames[number % 100]; + number /= 100; + } else { + soFar = numNames[number % 10]; + number /= 10; + + soFar = tensNames[number % 10] + soFar; + number /= 10; + } + if (number == 0) + return soFar; + return numNames[number] + " hundred" + soFar; + } + + + public static String convert(long number) { + // 0 to 999 999 999 999 + if (number == 0) { + return "zero"; + } + + String snumber = Long.toString(number); + + // pad with "0" + String mask = "000000000000"; + DecimalFormat df = new DecimalFormat(mask); + snumber = df.format(number); + + // XXXnnnnnnnnn + int billions = Integer.parseInt(snumber.substring(0, 3)); + // nnnXXXnnnnnn + int millions = Integer.parseInt(snumber.substring(3, 6)); + // nnnnnnXXXnnn + int hundredThousands = Integer.parseInt(snumber.substring(6, 9)); + // nnnnnnnnnXXX + int thousands = Integer.parseInt(snumber.substring(9, 12)); + + String tradBillions; + switch (billions) { + case 0: + tradBillions = ""; + break; + case 1: + tradBillions = convertLessThanOneThousand(billions) + " billion "; + break; + default: + tradBillions = convertLessThanOneThousand(billions) + " billion "; + } + String result = tradBillions; + + String tradMillions; + switch (millions) { + case 0: + tradMillions = ""; + break; + case 1: + tradMillions = convertLessThanOneThousand(millions) + " million "; + break; + default: + tradMillions = convertLessThanOneThousand(millions) + " million "; + } + result = result + tradMillions; + + String tradHundredThousands; + switch (hundredThousands) { + case 0: + tradHundredThousands = ""; + break; + case 1: + tradHundredThousands = "one thousand "; + break; + default: + tradHundredThousands = convertLessThanOneThousand(hundredThousands) + " thousand "; + } + result = result + tradHundredThousands; + + String tradThousand; + tradThousand = convertLessThanOneThousand(thousands); + result = result + tradThousand; + + // remove extra spaces! + return result.replaceAll("^\\s+", "").replaceAll("\\b\\s{2,}\\b", " "); + } +}