diff --git a/README.md b/README.md
index ba514c12b..7c21c1a03 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,13 @@ For composite benchmarks like `chbenchmark`, which require multiple schemas to b
java -jar benchbase.jar -b tpcc,chbenchmark -c config/postgres/sample_chbenchmark_config.xml --create=true --load=true --execute=true
```
+To execute the `tpcc` benchmark with every transaction issued as a single stored procedure call (PostgreSQL only):
+```bash
+java -jar benchbase.jar -b tpcc -c config/postgres/sample_tpcc_stored_procedures_config.xml --create=true --load=true --execute=true
+```
+
+Setting `true` makes `--create=true` install `procedures-postgres.sql` alongside the schema and makes each terminal issue one `CALL` per transaction instead of the usual sequence of statements. The database does the same work either way; what changes is that a transaction costs one round trip plus the commit rather than roughly 25, which matters when the client/server round trip rather than the database is the limit.
+
The following options are provided:
```text
diff --git a/config/postgres/sample_tpcc_stored_procedures_config.xml b/config/postgres/sample_tpcc_stored_procedures_config.xml
new file mode 100644
index 000000000..a5043bf34
--- /dev/null
+++ b/config/postgres/sample_tpcc_stored_procedures_config.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ POSTGRES
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/benchbase?sslmode=disable&ApplicationName=tpcc&reWriteBatchedInserts=true
+ admin
+ password
+ true
+ TRANSACTION_SERIALIZABLE
+ 128
+
+
+ true
+
+
+ 1
+
+
+ 1
+
+
+
+ 10000
+ 45,43,4,4,4
+
+
+
+
+
+
+ NewOrder
+
+
+ Payment
+
+
+ OrderStatus
+
+
+ Delivery
+
+
+ StockLevel
+
+
+
diff --git a/src/main/java/com/oltpbenchmark/api/BenchmarkModule.java b/src/main/java/com/oltpbenchmark/api/BenchmarkModule.java
index ceb632719..1550e2a40 100644
--- a/src/main/java/com/oltpbenchmark/api/BenchmarkModule.java
+++ b/src/main/java/com/oltpbenchmark/api/BenchmarkModule.java
@@ -232,6 +232,25 @@ public final void createDatabase() throws SQLException, IOException {
}
}
+ /**
+ * True if every procedure of this benchmark performs one complete transaction on its own, in
+ * which case workers run on an autocommit connection and do not issue their own commit or
+ * rollback.
+ */
+ public boolean usesAutoCommit() {
+ return false;
+ }
+
+ /**
+ * Return the classpath resource holding statements that must run right after the DDL, such as
+ * stored procedure definitions. Benchmarks that do not need one return null.
+ *
+ * @param db_type
+ */
+ public String getPostDDLScriptPath(DatabaseType db_type) {
+ return null;
+ }
+
/**
* Create the Benchmark Database This is the main method used to create all the database objects
* (e.g., table, indexes, etc) needed for this benchmark
@@ -251,6 +270,12 @@ public final void createDatabase(DatabaseType dbType, Connection conn)
LOG.debug("Executing script [{}] for database type [{}]", ddlPath, dbType);
runner.runScript(ddlPath);
}
+
+ String postDDLPath = this.getPostDDLScriptPath(dbType);
+ if (postDDLPath != null) {
+ LOG.debug("Executing post-DDL script [{}] for database type [{}]", postDDLPath, dbType);
+ runner.runScript(postDDLPath);
+ }
}
public final void runScript(String scriptPath) throws SQLException, IOException {
diff --git a/src/main/java/com/oltpbenchmark/api/Worker.java b/src/main/java/com/oltpbenchmark/api/Worker.java
index e50cbcea6..9459a4fcc 100644
--- a/src/main/java/com/oltpbenchmark/api/Worker.java
+++ b/src/main/java/com/oltpbenchmark/api/Worker.java
@@ -83,7 +83,7 @@ public Worker(T benchmark, int id) {
if (!this.configuration.getNewConnectionPerTxn()) {
try {
this.conn = this.benchmark.makeConnection();
- this.conn.setAutoCommit(false);
+ this.conn.setAutoCommit(this.benchmark.usesAutoCommit());
this.conn.setTransactionIsolation(this.configuration.getIsolationMode());
} catch (SQLException ex) {
throw new RuntimeException("Failed to connect to database", ex);
@@ -424,7 +424,7 @@ protected final void doWork(DatabaseType databaseType, TransactionType transacti
}
}
this.conn = this.benchmark.makeConnection();
- this.conn.setAutoCommit(false);
+ this.conn.setAutoCommit(this.benchmark.usesAutoCommit());
this.conn.setTransactionIsolation(this.configuration.getIsolationMode());
} catch (SQLException ex) {
if (LOG.isDebugEnabled()) {
@@ -453,13 +453,17 @@ protected final void doWork(DatabaseType databaseType, TransactionType transacti
LOG.debug(String.format("%s %s committing...", this, transactionType));
}
- conn.commit();
+ if (!conn.getAutoCommit()) {
+ conn.commit();
+ }
break;
} catch (UserAbortException ex) {
try {
- conn.rollback();
+ if (!conn.getAutoCommit()) {
+ conn.rollback();
+ }
} catch (SQLException ex2) {
LOG.error("SQLException caught while rolling back transaction.", ex2);
// force a reconnection
@@ -517,7 +521,9 @@ protected final void doWork(DatabaseType databaseType, TransactionType transacti
ex.getErrorCode()),
ex);
try {
- conn.rollback();
+ if (!conn.getAutoCommit()) {
+ conn.rollback();
+ }
} catch (SQLException ex2) {
LOG.error("SQLException caught while attempting to rollback transaction.", ex2);
// force a reconnection
@@ -550,7 +556,9 @@ protected final void doWork(DatabaseType databaseType, TransactionType transacti
ex.getErrorCode()),
ex);
try {
- conn.rollback();
+ if (!conn.getAutoCommit()) {
+ conn.rollback();
+ }
} catch (SQLException ex2) {
LOG.error("SQLException caught while attempting to rollback transaction.", ex2);
// force a reconnection
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/TPCCBenchmark.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/TPCCBenchmark.java
index 4d79e405f..b6f123534 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/TPCCBenchmark.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/TPCCBenchmark.java
@@ -22,6 +22,7 @@
import com.oltpbenchmark.api.Loader;
import com.oltpbenchmark.api.Worker;
import com.oltpbenchmark.benchmarks.tpcc.procedures.NewOrder;
+import com.oltpbenchmark.types.DatabaseType;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -32,8 +33,41 @@
public final class TPCCBenchmark extends BenchmarkModule {
private static final Logger LOG = LoggerFactory.getLogger(TPCCBenchmark.class);
+ private final boolean useStoredProcedures;
+
public TPCCBenchmark(WorkloadConfiguration workConf) {
super(workConf);
+ this.useStoredProcedures =
+ workConf.getXmlConfig() != null
+ && workConf.getXmlConfig().getBoolean("useStoredProcedures", false);
+ }
+
+ /**
+ * When true each transaction is a single CALL of a server-side function instead of the statement
+ * sequence issued by the procedure classes. The database work is the same; only the number of
+ * client/server round trips changes.
+ */
+ public boolean useStoredProcedures() {
+ return this.useStoredProcedures;
+ }
+
+ @Override
+ public boolean usesAutoCommit() {
+ // A stored procedure call is a complete transaction, so there is nothing for the worker to
+ // commit afterwards, and skipping the commit saves the round trip it costs.
+ return this.useStoredProcedures;
+ }
+
+ @Override
+ public String getPostDDLScriptPath(DatabaseType dbType) {
+ if (!this.useStoredProcedures) {
+ return null;
+ }
+ if (dbType != DatabaseType.POSTGRES) {
+ throw new UnsupportedOperationException(
+ "TPC-C stored procedures are currently implemented for PostgreSQL only, not " + dbType);
+ }
+ return "/benchmarks/" + this.getBenchmarkName() + "/procedures-postgres.sql";
}
@Override
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Delivery.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Delivery.java
index 2cf7acee7..641a568ed 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Delivery.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Delivery.java
@@ -108,6 +108,8 @@ SELECT SUM(OL_AMOUNT) AS OL_TOTAL
"""
.formatted(TPCCConstants.TABLENAME_CUSTOMER));
+ public SQLStmt stmtDeliveryProcSQL = new SQLStmt("SELECT tpcc_delivery(?,?,?)");
+
public void run(
Connection conn,
Random gen,
@@ -120,6 +122,11 @@ public void run(
int o_carrier_id = TPCCUtil.randomNumber(1, 10, gen);
+ if (w.getBenchmark().useStoredProcedures()) {
+ deliveryStoredProcedure(conn, w_id, o_carrier_id, terminalDistrictUpperID);
+ return;
+ }
+
int d_id;
int[] orderIDs = new int[10];
@@ -173,6 +180,23 @@ public void run(
}
}
+ private void deliveryStoredProcedure(
+ Connection conn, int w_id, int o_carrier_id, int terminalDistrictUpperID)
+ throws SQLException {
+
+ try (PreparedStatement stmt = this.getPreparedStatement(conn, stmtDeliveryProcSQL)) {
+ stmt.setInt(1, w_id);
+ stmt.setInt(2, o_carrier_id);
+ stmt.setInt(3, terminalDistrictUpperID);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ throw new RuntimeException("tpcc_delivery returned no row");
+ }
+ }
+ }
+ }
+
private Integer getOrderId(Connection conn, int w_id, int d_id) throws SQLException {
try (PreparedStatement delivGetOrderId = this.getPreparedStatement(conn, delivGetOrderIdSQL)) {
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/NewOrder.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/NewOrder.java
index e98f9ebe1..28246010f 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/NewOrder.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/NewOrder.java
@@ -131,6 +131,11 @@ public class NewOrder extends TPCCProcedure {
"""
.formatted(TPCCConstants.TABLENAME_ORDERLINE));
+ public SQLStmt stmtNewOrderProcSQL = new SQLStmt("SELECT * FROM tpcc_new_order(?,?,?,?,?,?)");
+
+ /** SQLSTATE raised by tpcc_new_order for the 1% of orders that must roll back. */
+ private static final String SQLSTATE_USER_ABORT = "TPCC1";
+
public void run(
Connection conn,
Random gen,
@@ -168,16 +173,68 @@ public void run(
itemIDs[numItems - 1] = TPCCConfig.INVALID_ITEM_ID;
}
- newOrderTransaction(
- terminalWarehouseID,
- districtID,
- customerID,
- numItems,
- allLocal,
- itemIDs,
- supplierWarehouseIDs,
- orderQuantities,
- conn);
+ if (w.getBenchmark().useStoredProcedures()) {
+ newOrderStoredProcedure(
+ terminalWarehouseID,
+ districtID,
+ customerID,
+ numItems,
+ itemIDs,
+ supplierWarehouseIDs,
+ orderQuantities,
+ conn);
+ } else {
+ newOrderTransaction(
+ terminalWarehouseID,
+ districtID,
+ customerID,
+ numItems,
+ allLocal,
+ itemIDs,
+ supplierWarehouseIDs,
+ orderQuantities,
+ conn);
+ }
+ }
+
+ private void newOrderStoredProcedure(
+ int w_id,
+ int d_id,
+ int c_id,
+ int o_ol_cnt,
+ int[] itemIDs,
+ int[] supplierWarehouseIDs,
+ int[] orderQuantities,
+ Connection conn)
+ throws SQLException {
+
+ try (PreparedStatement stmt = this.getPreparedStatement(conn, stmtNewOrderProcSQL)) {
+ stmt.setInt(1, w_id);
+ stmt.setInt(2, d_id);
+ stmt.setInt(3, c_id);
+ stmt.setArray(4, conn.createArrayOf("integer", box(itemIDs, o_ol_cnt)));
+ stmt.setArray(5, conn.createArrayOf("integer", box(supplierWarehouseIDs, o_ol_cnt)));
+ stmt.setArray(6, conn.createArrayOf("integer", box(orderQuantities, o_ol_cnt)));
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ throw new RuntimeException("tpcc_new_order returned no row");
+ }
+ }
+ } catch (SQLException e) {
+ if (SQLSTATE_USER_ABORT.equals(e.getSQLState())) {
+ throw new UserAbortException(e.getMessage());
+ }
+ throw e;
+ }
+ }
+
+ private static Integer[] box(int[] values, int length) {
+ Integer[] boxed = new Integer[length];
+ for (int i = 0; i < length; i++) {
+ boxed[i] = values[i];
+ }
+ return boxed;
}
private void newOrderTransaction(
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/OrderStatus.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/OrderStatus.java
index eeedcfd2f..757d62839 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/OrderStatus.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/OrderStatus.java
@@ -27,6 +27,7 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@@ -87,6 +88,8 @@ public class OrderStatus extends TPCCProcedure {
"""
.formatted(TPCCConstants.TABLENAME_CUSTOMER));
+ public SQLStmt stmtOrderStatusProcSQL = new SQLStmt("SELECT * FROM tpcc_order_status(?,?,?,?)");
+
public void run(
Connection conn,
Random gen,
@@ -112,6 +115,11 @@ public void run(
c_id = TPCCUtil.getCustomerID(gen);
}
+ if (w.getBenchmark().useStoredProcedures()) {
+ orderStatusStoredProcedure(conn, w_id, d_id, c_by_name ? null : c_id, c_last);
+ return;
+ }
+
Customer c;
if (c_by_name) {
@@ -172,6 +180,28 @@ public void run(
}
}
+ private void orderStatusStoredProcedure(
+ Connection conn, int w_id, int d_id, Integer c_id, String c_last) throws SQLException {
+
+ try (PreparedStatement stmt = this.getPreparedStatement(conn, stmtOrderStatusProcSQL)) {
+ stmt.setInt(1, w_id);
+ stmt.setInt(2, d_id);
+ if (c_id == null) {
+ stmt.setNull(3, Types.INTEGER);
+ stmt.setString(4, c_last);
+ } else {
+ stmt.setInt(3, c_id);
+ stmt.setNull(4, Types.VARCHAR);
+ }
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ // drain the order lines, as the statement-per-call path does
+ }
+ }
+ }
+ }
+
private Oorder getOrderDetails(Connection conn, int w_id, int d_id, Customer c)
throws SQLException {
try (PreparedStatement ordStatGetNewestOrd =
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Payment.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Payment.java
index 7f3ea5c88..08326baae 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Payment.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/Payment.java
@@ -148,6 +148,9 @@ public class Payment extends TPCCProcedure {
"""
.formatted(TPCCConstants.TABLENAME_CUSTOMER));
+ public SQLStmt stmtPaymentProcSQL =
+ new SQLStmt("SELECT * FROM tpcc_payment(?,?,?,?,?,?,cast(? as decimal(6,2)))");
+
public void run(
Connection conn,
Random gen,
@@ -162,6 +165,15 @@ public void run(
float paymentAmount = (float) (TPCCUtil.randomNumber(100, 500000, gen) / 100.0);
+ if (worker.getBenchmark().useStoredProcedures()) {
+ int spX = TPCCUtil.randomNumber(1, 100, gen);
+ int spCustomerDistrictID = getCustomerDistrictId(gen, districtID, spX);
+ int spCustomerWarehouseID = getCustomerWarehouseID(gen, w_id, numWarehouses, spX);
+ paymentStoredProcedure(
+ conn, gen, w_id, districtID, spCustomerWarehouseID, spCustomerDistrictID, paymentAmount);
+ return;
+ }
+
updateWarehouse(conn, w_id, paymentAmount);
Warehouse w = getWarehouse(conn, w_id);
@@ -342,6 +354,41 @@ private Warehouse getWarehouse(Connection conn, int w_id) throws SQLException {
}
}
+ private void paymentStoredProcedure(
+ Connection conn,
+ Random gen,
+ int w_id,
+ int districtID,
+ int customerWarehouseID,
+ int customerDistrictID,
+ float paymentAmount)
+ throws SQLException {
+
+ // 60% of the payments look the customer up by last name, as in getCustomer().
+ boolean byName = TPCCUtil.randomNumber(1, 100, gen) <= 60;
+
+ try (PreparedStatement stmt = this.getPreparedStatement(conn, stmtPaymentProcSQL)) {
+ stmt.setInt(1, w_id);
+ stmt.setInt(2, districtID);
+ stmt.setInt(3, customerWarehouseID);
+ stmt.setInt(4, customerDistrictID);
+ if (byName) {
+ stmt.setNull(5, Types.INTEGER);
+ stmt.setString(6, TPCCUtil.getNonUniformRandomLastNameForRun(gen));
+ } else {
+ stmt.setInt(5, TPCCUtil.getCustomerID(gen));
+ stmt.setNull(6, Types.VARCHAR);
+ }
+ stmt.setDouble(7, paymentAmount);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ throw new RuntimeException("tpcc_payment returned no row");
+ }
+ }
+ }
+ }
+
private Customer getCustomer(
Connection conn,
Random gen,
diff --git a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/StockLevel.java b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/StockLevel.java
index 184ee7379..bd095779c 100644
--- a/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/StockLevel.java
+++ b/src/main/java/com/oltpbenchmark/benchmarks/tpcc/procedures/StockLevel.java
@@ -58,6 +58,8 @@ SELECT COUNT(DISTINCT (S_I_ID)) AS STOCK_COUNT
"""
.formatted(TPCCConstants.TABLENAME_ORDERLINE, TPCCConstants.TABLENAME_STOCK));
+ public SQLStmt stmtStockLevelProcSQL = new SQLStmt("SELECT tpcc_stock_level(?,?,?)");
+
public void run(
Connection conn,
Random gen,
@@ -71,6 +73,11 @@ public void run(
int threshold = TPCCUtil.randomNumber(10, 20, gen);
int d_id = TPCCUtil.randomNumber(terminalDistrictLowerID, terminalDistrictUpperID, gen);
+ if (w.getBenchmark().useStoredProcedures()) {
+ stockLevelStoredProcedure(conn, w_id, d_id, threshold);
+ return;
+ }
+
int o_id = getOrderId(conn, w_id, d_id);
int stock_count = getStockCount(conn, w_id, threshold, d_id, o_id);
@@ -91,6 +98,22 @@ public void run(
}
}
+ private void stockLevelStoredProcedure(Connection conn, int w_id, int d_id, int threshold)
+ throws SQLException {
+
+ try (PreparedStatement stmt = this.getPreparedStatement(conn, stmtStockLevelProcSQL)) {
+ stmt.setInt(1, w_id);
+ stmt.setInt(2, d_id);
+ stmt.setInt(3, threshold);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (!rs.next()) {
+ throw new RuntimeException("tpcc_stock_level returned no row");
+ }
+ }
+ }
+ }
+
private int getOrderId(Connection conn, int w_id, int d_id) throws SQLException {
try (PreparedStatement stockGetDistOrderId =
this.getPreparedStatement(conn, stockGetDistOrderIdSQL)) {
diff --git a/src/main/java/com/oltpbenchmark/util/ScriptRunner.java b/src/main/java/com/oltpbenchmark/util/ScriptRunner.java
index bc25b66ac..d53a0b137 100644
--- a/src/main/java/com/oltpbenchmark/util/ScriptRunner.java
+++ b/src/main/java/com/oltpbenchmark/util/ScriptRunner.java
@@ -78,6 +78,43 @@ private void runScript(Reader reader) throws IOException, SQLException {
}
}
+ /**
+ * Returns the dollar quote tag left open at the end of {@code line}, given the tag that was open
+ * when the line started, or null if none is open. PostgreSQL spells function bodies as {@code $$
+ * ... $$} or {@code $tag$ ... $tag$} blocks whose contents must not be split on the statement
+ * delimiter.
+ */
+ static String scanDollarQuote(String line, String openTag) {
+ int i = 0;
+ while (i < line.length()) {
+ if (openTag == null) {
+ int start = line.indexOf('$', i);
+ if (start < 0) {
+ return null;
+ }
+ int end = start + 1;
+ while (end < line.length()
+ && (Character.isLetterOrDigit(line.charAt(end)) || line.charAt(end) == '_')) {
+ end++;
+ }
+ if (end < line.length() && line.charAt(end) == '$') {
+ openTag = line.substring(start, end + 1);
+ i = end + 1;
+ } else {
+ i = start + 1;
+ }
+ } else {
+ int close = line.indexOf(openTag, i);
+ if (close < 0) {
+ return openTag;
+ }
+ i = close + openTag.length();
+ openTag = null;
+ }
+ }
+ return openTag;
+ }
+
/**
* Runs an SQL script (read in using the Reader parameter) using the connection passed in
*
@@ -88,6 +125,7 @@ private void runScript(Reader reader) throws IOException, SQLException {
*/
private void runScript(Connection conn, Reader reader) throws IOException, SQLException {
StringBuffer command = null;
+ String dollarTag = null;
try (LineNumberReader lineReader = new LineNumberReader(reader)) {
String line = null;
while ((line = lineReader.readLine()) != null) {
@@ -98,13 +136,17 @@ private void runScript(Connection conn, Reader reader) throws IOException, SQLEx
command = new StringBuffer();
}
String trimmedLine = line.trim();
- line = line.replaceAll("\\-\\-.*$", ""); // remove comments in line;
+ boolean insideDollarQuote = dollarTag != null;
+ dollarTag = scanDollarQuote(line, dollarTag);
+ if (!insideDollarQuote && dollarTag == null) {
+ line = line.replaceAll("\\-\\-.*$", ""); // remove comments in line;
+ }
- if (trimmedLine.startsWith("--") || trimmedLine.startsWith("//")) {
+ if (!insideDollarQuote && (trimmedLine.startsWith("--") || trimmedLine.startsWith("//"))) {
LOG.debug(trimmedLine);
- } else if (trimmedLine.length() < 1) {
+ } else if (!insideDollarQuote && trimmedLine.length() < 1) {
// Do nothing
- } else if (trimmedLine.endsWith(getDelimiter())) {
+ } else if (dollarTag == null && trimmedLine.endsWith(getDelimiter())) {
command.append(line, 0, line.lastIndexOf(getDelimiter()));
command.append(" ");
@@ -154,7 +196,7 @@ private void runScript(Connection conn, Reader reader) throws IOException, SQLEx
}
} else {
command.append(line);
- command.append(" ");
+ command.append(insideDollarQuote || dollarTag != null ? "\n" : " ");
}
}
if (!autoCommit) {
diff --git a/src/main/resources/benchmarks/tpcc/procedures-postgres.sql b/src/main/resources/benchmarks/tpcc/procedures-postgres.sql
new file mode 100644
index 000000000..42bfe8f51
--- /dev/null
+++ b/src/main/resources/benchmarks/tpcc/procedures-postgres.sql
@@ -0,0 +1,426 @@
+-- TPC-C stored procedures for PostgreSQL.
+--
+-- Loaded right after ddl-postgres.sql when the workload configuration sets
+-- true. Each function issues
+-- exactly the statement sequence of the matching Java procedure in
+-- com.oltpbenchmark.benchmarks.tpcc.procedures, so the two modes differ only
+-- in how many client/server round trips a transaction costs.
+--
+-- Errors that the Java procedures signal with UserAbortException are raised
+-- with SQLSTATE 'TPCC1' so the client can tell an expected abort (the 1% of
+-- new orders that reference an unused item id) from a real failure.
+
+CREATE OR REPLACE FUNCTION tpcc_new_order(
+ in_w_id int,
+ in_d_id int,
+ in_c_id int,
+ in_ol_i_id int[],
+ in_ol_supply_w_id int[],
+ in_ol_quantity int[])
+RETURNS TABLE (
+ out_o_id int,
+ out_o_entry_d timestamp,
+ out_w_tax decimal(4, 4),
+ out_d_tax decimal(4, 4),
+ out_c_discount decimal(4, 4),
+ out_c_last varchar(16),
+ out_c_credit char(2),
+ out_total decimal(12, 2))
+LANGUAGE plpgsql AS $$
+DECLARE
+ v_w_tax decimal(4, 4);
+ v_d_tax decimal(4, 4);
+ v_d_next_o_id int;
+ v_c_discount decimal(4, 4);
+ v_c_last varchar(16);
+ v_c_credit char(2);
+ v_o_entry_d timestamp := CURRENT_TIMESTAMP;
+ v_ol_cnt int := coalesce(array_length(in_ol_i_id, 1), 0);
+ v_all_local int := 1;
+ v_total decimal(12, 2) := 0;
+ v_n int;
+ v_i_id int;
+ v_supply_w_id int;
+ v_quantity int;
+ v_i_price decimal(5, 2);
+ v_i_name varchar(24);
+ v_i_data varchar(50);
+ v_s_quantity int;
+ v_s_data varchar(50);
+ v_s_dist char(24);
+ v_ol_amount decimal(6, 2);
+BEGIN
+ SELECT c_discount, c_last, c_credit
+ INTO v_c_discount, v_c_last, v_c_credit
+ FROM customer
+ WHERE c_w_id = in_w_id AND c_d_id = in_d_id AND c_id = in_c_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'C_D_ID=% C_ID=% not found!', in_d_id, in_c_id;
+ END IF;
+
+ SELECT w_tax INTO v_w_tax FROM warehouse WHERE w_id = in_w_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'W_ID=% not found!', in_w_id;
+ END IF;
+
+ SELECT d_next_o_id, d_tax INTO v_d_next_o_id, v_d_tax
+ FROM district
+ WHERE d_w_id = in_w_id AND d_id = in_d_id
+ FOR UPDATE;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'D_ID=% D_W_ID=% not found!', in_d_id, in_w_id;
+ END IF;
+
+ UPDATE district SET d_next_o_id = d_next_o_id + 1
+ WHERE d_w_id = in_w_id AND d_id = in_d_id;
+
+ FOR v_n IN 1 .. v_ol_cnt LOOP
+ IF in_ol_supply_w_id[v_n] <> in_w_id THEN
+ v_all_local := 0;
+ END IF;
+ END LOOP;
+
+ INSERT INTO oorder (o_id, o_d_id, o_w_id, o_c_id, o_entry_d, o_ol_cnt, o_all_local)
+ VALUES (v_d_next_o_id, in_d_id, in_w_id, in_c_id, v_o_entry_d, v_ol_cnt, v_all_local);
+
+ INSERT INTO new_order (no_o_id, no_d_id, no_w_id)
+ VALUES (v_d_next_o_id, in_d_id, in_w_id);
+
+ FOR v_n IN 1 .. v_ol_cnt LOOP
+ v_i_id := in_ol_i_id[v_n];
+ v_supply_w_id := in_ol_supply_w_id[v_n];
+ v_quantity := in_ol_quantity[v_n];
+
+ SELECT i_price, i_name, i_data
+ INTO v_i_price, v_i_name, v_i_data
+ FROM item WHERE i_id = v_i_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'EXPECTED new order rollback: I_ID=% not found!', v_i_id
+ USING ERRCODE = 'TPCC1';
+ END IF;
+
+ v_ol_amount := v_quantity * v_i_price;
+ v_total := v_total + v_ol_amount;
+
+ SELECT s_quantity, s_data,
+ CASE in_d_id
+ WHEN 1 THEN s_dist_01 WHEN 2 THEN s_dist_02
+ WHEN 3 THEN s_dist_03 WHEN 4 THEN s_dist_04
+ WHEN 5 THEN s_dist_05 WHEN 6 THEN s_dist_06
+ WHEN 7 THEN s_dist_07 WHEN 8 THEN s_dist_08
+ WHEN 9 THEN s_dist_09 WHEN 10 THEN s_dist_10
+ END
+ INTO v_s_quantity, v_s_data, v_s_dist
+ FROM stock
+ WHERE s_i_id = v_i_id AND s_w_id = v_supply_w_id
+ FOR UPDATE;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'S_I_ID=% not found!', v_i_id;
+ END IF;
+
+ IF v_s_quantity - v_quantity >= 10 THEN
+ v_s_quantity := v_s_quantity - v_quantity;
+ ELSE
+ v_s_quantity := v_s_quantity - v_quantity + 91;
+ END IF;
+
+ INSERT INTO order_line (ol_o_id, ol_d_id, ol_w_id, ol_number, ol_i_id,
+ ol_supply_w_id, ol_quantity, ol_amount, ol_dist_info)
+ VALUES (v_d_next_o_id, in_d_id, in_w_id, v_n, v_i_id,
+ v_supply_w_id, v_quantity, v_ol_amount, v_s_dist);
+
+ UPDATE stock
+ SET s_quantity = v_s_quantity,
+ s_ytd = s_ytd + v_quantity,
+ s_order_cnt = s_order_cnt + 1,
+ s_remote_cnt = s_remote_cnt
+ + CASE WHEN v_supply_w_id = in_w_id THEN 0 ELSE 1 END
+ WHERE s_i_id = v_i_id AND s_w_id = v_supply_w_id;
+ END LOOP;
+
+ RETURN QUERY SELECT v_d_next_o_id, v_o_entry_d, v_w_tax, v_d_tax,
+ v_c_discount, v_c_last, v_c_credit, v_total;
+END;
+$$;
+
+-- Either in_c_id or in_c_last is supplied; the client picks by name for 60% of
+-- the payments, exactly as the Java procedure does.
+CREATE OR REPLACE FUNCTION tpcc_payment(
+ in_w_id int,
+ in_d_id int,
+ in_c_w_id int,
+ in_c_d_id int,
+ in_c_id int,
+ in_c_last varchar(16),
+ in_h_amount decimal(6, 2))
+RETURNS TABLE (
+ out_c_id int,
+ out_c_first varchar(16),
+ out_c_middle char(2),
+ out_c_last varchar(16),
+ out_c_street_1 varchar(20),
+ out_c_street_2 varchar(20),
+ out_c_city varchar(20),
+ out_c_state char(2),
+ out_c_zip char(9),
+ out_c_phone char(16),
+ out_c_credit char(2),
+ out_c_credit_lim decimal(12, 2),
+ out_c_discount decimal(4, 4),
+ out_c_balance decimal(12, 2),
+ out_c_since timestamp,
+ out_w_street_1 varchar(20),
+ out_w_street_2 varchar(20),
+ out_w_city varchar(20),
+ out_w_state char(2),
+ out_w_zip char(9),
+ out_d_street_1 varchar(20),
+ out_d_street_2 varchar(20),
+ out_d_city varchar(20),
+ out_d_state char(2),
+ out_d_zip char(9))
+LANGUAGE plpgsql AS $$
+DECLARE
+ v_w warehouse%ROWTYPE;
+ v_d district%ROWTYPE;
+ v_c customer%ROWTYPE;
+ v_c_data varchar(500);
+ v_h_date timestamp := CURRENT_TIMESTAMP;
+BEGIN
+ UPDATE warehouse SET w_ytd = w_ytd + in_h_amount WHERE w_id = in_w_id;
+
+ SELECT * INTO v_w FROM warehouse WHERE w_id = in_w_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'W_ID=% not found!', in_w_id;
+ END IF;
+
+ UPDATE district SET d_ytd = d_ytd + in_h_amount
+ WHERE d_w_id = in_w_id AND d_id = in_d_id;
+
+ SELECT * INTO v_d FROM district WHERE d_w_id = in_w_id AND d_id = in_d_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'D_ID=% D_W_ID=% not found!', in_d_id, in_w_id;
+ END IF;
+
+ IF in_c_id IS NULL THEN
+ -- The middle customer of those sharing the last name, ordered by c_first.
+ SELECT c.* INTO v_c
+ FROM (SELECT cust.*,
+ row_number() OVER (ORDER BY cust.c_first) AS rn,
+ count(*) OVER () AS cnt
+ FROM customer cust
+ WHERE cust.c_w_id = in_c_w_id
+ AND cust.c_d_id = in_c_d_id
+ AND cust.c_last = in_c_last) c
+ WHERE c.rn = (c.cnt + 1) / 2;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'C_LAST=% C_D_ID=% C_W_ID=% not found!',
+ in_c_last, in_c_d_id, in_c_w_id;
+ END IF;
+ ELSE
+ SELECT * INTO v_c FROM customer
+ WHERE c_w_id = in_c_w_id AND c_d_id = in_c_d_id AND c_id = in_c_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'C_ID=% C_D_ID=% C_W_ID=% not found!',
+ in_c_id, in_c_d_id, in_c_w_id;
+ END IF;
+ END IF;
+
+ v_c.c_balance := v_c.c_balance - in_h_amount;
+ v_c.c_ytd_payment := v_c.c_ytd_payment + in_h_amount;
+ v_c.c_payment_cnt := v_c.c_payment_cnt + 1;
+
+ IF v_c.c_credit = 'BC' THEN
+ SELECT c_data INTO v_c_data FROM customer
+ WHERE c_w_id = in_c_w_id AND c_d_id = in_c_d_id AND c_id = v_c.c_id;
+
+ v_c_data := left(v_c.c_id || ' ' || in_c_d_id || ' ' || in_c_w_id || ' '
+ || in_d_id || ' ' || in_w_id || ' ' || in_h_amount
+ || ' | ' || v_c_data, 500);
+ v_c.c_data := v_c_data;
+
+ UPDATE customer
+ SET c_balance = v_c.c_balance,
+ c_ytd_payment = v_c.c_ytd_payment,
+ c_payment_cnt = v_c.c_payment_cnt,
+ c_data = v_c_data
+ WHERE c_w_id = in_c_w_id AND c_d_id = in_c_d_id AND c_id = v_c.c_id;
+ ELSE
+ UPDATE customer
+ SET c_balance = v_c.c_balance,
+ c_ytd_payment = v_c.c_ytd_payment,
+ c_payment_cnt = v_c.c_payment_cnt
+ WHERE c_w_id = in_c_w_id AND c_d_id = in_c_d_id AND c_id = v_c.c_id;
+ END IF;
+
+ INSERT INTO history (h_c_d_id, h_c_w_id, h_c_id, h_d_id, h_w_id,
+ h_date, h_amount, h_data)
+ VALUES (in_c_d_id, in_c_w_id, v_c.c_id, in_d_id, in_w_id,
+ v_h_date, in_h_amount, v_w.w_name || ' ' || v_d.d_name);
+
+ RETURN QUERY SELECT v_c.c_id, v_c.c_first, v_c.c_middle, v_c.c_last,
+ v_c.c_street_1, v_c.c_street_2, v_c.c_city, v_c.c_state,
+ v_c.c_zip, v_c.c_phone, v_c.c_credit, v_c.c_credit_lim,
+ v_c.c_discount, v_c.c_balance, v_c.c_since,
+ v_w.w_street_1, v_w.w_street_2, v_w.w_city, v_w.w_state,
+ v_w.w_zip,
+ v_d.d_street_1, v_d.d_street_2, v_d.d_city, v_d.d_state,
+ v_d.d_zip;
+END;
+$$;
+
+-- One row per order line of the customer's most recent order; the customer and
+-- order header columns repeat on every row so the whole result the Java
+-- procedure assembles still crosses the wire.
+CREATE OR REPLACE FUNCTION tpcc_order_status(
+ in_w_id int,
+ in_d_id int,
+ in_c_id int,
+ in_c_last varchar(16))
+RETURNS TABLE (
+ out_c_id int,
+ out_c_first varchar(16),
+ out_c_middle char(2),
+ out_c_last varchar(16),
+ out_c_balance decimal(12, 2),
+ out_o_id int,
+ out_o_entry_d timestamp,
+ out_o_carrier_id int,
+ out_ol_i_id int,
+ out_ol_supply_w_id int,
+ out_ol_quantity decimal(6, 2),
+ out_ol_amount decimal(6, 2),
+ out_ol_delivery_d timestamp)
+LANGUAGE plpgsql AS $$
+DECLARE
+ v_c customer%ROWTYPE;
+ v_o_id int;
+ v_o_entry_d timestamp;
+ v_o_carrier_id int;
+BEGIN
+ IF in_c_id IS NULL THEN
+ SELECT c.* INTO v_c
+ FROM (SELECT cust.*,
+ row_number() OVER (ORDER BY cust.c_first) AS rn,
+ count(*) OVER () AS cnt
+ FROM customer cust
+ WHERE cust.c_w_id = in_w_id
+ AND cust.c_d_id = in_d_id
+ AND cust.c_last = in_c_last) c
+ WHERE c.rn = (c.cnt + 1) / 2;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'C_LAST=% C_D_ID=% C_W_ID=% not found!',
+ in_c_last, in_d_id, in_w_id;
+ END IF;
+ ELSE
+ SELECT * INTO v_c FROM customer
+ WHERE c_w_id = in_w_id AND c_d_id = in_d_id AND c_id = in_c_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'C_ID=% C_D_ID=% C_W_ID=% not found!',
+ in_c_id, in_d_id, in_w_id;
+ END IF;
+ END IF;
+
+ SELECT o_id, o_carrier_id, o_entry_d
+ INTO v_o_id, v_o_carrier_id, v_o_entry_d
+ FROM oorder
+ WHERE o_w_id = in_w_id AND o_d_id = in_d_id AND o_c_id = v_c.c_id
+ ORDER BY o_id DESC
+ LIMIT 1;
+
+ RETURN QUERY
+ SELECT v_c.c_id, v_c.c_first, v_c.c_middle, v_c.c_last, v_c.c_balance,
+ v_o_id, v_o_entry_d, v_o_carrier_id,
+ ol.ol_i_id, ol.ol_supply_w_id, ol.ol_quantity, ol.ol_amount,
+ ol.ol_delivery_d
+ FROM order_line ol
+ WHERE ol.ol_o_id = v_o_id AND ol.ol_d_id = in_d_id AND ol.ol_w_id = in_w_id;
+END;
+$$;
+
+-- Returns the delivered order id per district, or NULL where the district had
+-- no undelivered order (the Java procedure records -1 for those).
+CREATE OR REPLACE FUNCTION tpcc_delivery(
+ in_w_id int,
+ in_o_carrier_id int,
+ in_d_id_max int)
+RETURNS int[]
+LANGUAGE plpgsql AS $$
+DECLARE
+ v_result int[] := array_fill(NULL::int, ARRAY[in_d_id_max]);
+ v_d_id int;
+ v_no_o_id int;
+ v_c_id int;
+ v_ol_total decimal(12, 2);
+ v_deliv_d timestamp := CURRENT_TIMESTAMP;
+BEGIN
+ FOR v_d_id IN 1 .. in_d_id_max LOOP
+ SELECT no_o_id INTO v_no_o_id
+ FROM new_order
+ WHERE no_d_id = v_d_id AND no_w_id = in_w_id
+ ORDER BY no_o_id ASC
+ LIMIT 1;
+ CONTINUE WHEN NOT FOUND;
+
+ DELETE FROM new_order
+ WHERE no_o_id = v_no_o_id AND no_d_id = v_d_id AND no_w_id = in_w_id;
+
+ SELECT o_c_id INTO v_c_id
+ FROM oorder
+ WHERE o_id = v_no_o_id AND o_d_id = v_d_id AND o_w_id = in_w_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'O_ID=% O_D_ID=% O_W_ID=% not found!',
+ v_no_o_id, v_d_id, in_w_id;
+ END IF;
+
+ UPDATE oorder SET o_carrier_id = in_o_carrier_id
+ WHERE o_id = v_no_o_id AND o_d_id = v_d_id AND o_w_id = in_w_id;
+
+ UPDATE order_line SET ol_delivery_d = v_deliv_d
+ WHERE ol_o_id = v_no_o_id AND ol_d_id = v_d_id AND ol_w_id = in_w_id;
+
+ SELECT sum(ol_amount) INTO v_ol_total
+ FROM order_line
+ WHERE ol_o_id = v_no_o_id AND ol_d_id = v_d_id AND ol_w_id = in_w_id;
+
+ UPDATE customer
+ SET c_balance = c_balance + v_ol_total,
+ c_delivery_cnt = c_delivery_cnt + 1
+ WHERE c_w_id = in_w_id AND c_d_id = v_d_id AND c_id = v_c_id;
+
+ v_result[v_d_id] := v_no_o_id;
+ END LOOP;
+
+ RETURN v_result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION tpcc_stock_level(
+ in_w_id int,
+ in_d_id int,
+ in_threshold int)
+RETURNS int
+LANGUAGE plpgsql AS $$
+DECLARE
+ v_d_next_o_id int;
+ v_count int;
+BEGIN
+ SELECT d_next_o_id INTO v_d_next_o_id
+ FROM district WHERE d_w_id = in_w_id AND d_id = in_d_id;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'D_W_ID=% D_ID=% not found!', in_w_id, in_d_id;
+ END IF;
+
+ SELECT count(DISTINCT s_i_id) INTO v_count
+ FROM order_line, stock
+ WHERE ol_w_id = in_w_id
+ AND ol_d_id = in_d_id
+ AND ol_o_id < v_d_next_o_id
+ AND ol_o_id >= v_d_next_o_id - 20
+ AND s_w_id = in_w_id
+ AND s_i_id = ol_i_id
+ AND s_quantity < in_threshold;
+
+ RETURN v_count;
+END;
+$$;
diff --git a/src/test/java/com/oltpbenchmark/util/TestScriptRunner.java b/src/test/java/com/oltpbenchmark/util/TestScriptRunner.java
new file mode 100644
index 000000000..08743303b
--- /dev/null
+++ b/src/test/java/com/oltpbenchmark/util/TestScriptRunner.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015 by OLTPBenchmark Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.oltpbenchmark.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class TestScriptRunner {
+
+ @Test
+ public void testNoDollarQuote() {
+ assertNull(ScriptRunner.scanDollarQuote("CREATE TABLE foo (a int);", null));
+ assertNull(ScriptRunner.scanDollarQuote("SELECT 'a $ b';", null));
+ }
+
+ @Test
+ public void testOpensAndStaysOpen() {
+ assertEquals("$$", ScriptRunner.scanDollarQuote("CREATE FUNCTION f() AS $$", null));
+ assertEquals("$body$", ScriptRunner.scanDollarQuote("CREATE FUNCTION f() AS $body$", null));
+ assertEquals("$$", ScriptRunner.scanDollarQuote(" x := 1; y := 2;", "$$"));
+ }
+
+ @Test
+ public void testCloses() {
+ assertNull(ScriptRunner.scanDollarQuote("$$;", "$$"));
+ assertNull(ScriptRunner.scanDollarQuote("$body$ LANGUAGE plpgsql;", "$body$"));
+ // a differently tagged quote does not close the open one
+ assertEquals("$body$", ScriptRunner.scanDollarQuote("SELECT $$inner$$;", "$body$"));
+ }
+
+ @Test
+ public void testOpensAndClosesOnOneLine() {
+ assertNull(ScriptRunner.scanDollarQuote("CREATE FUNCTION f() AS $$ BEGIN END $$;", null));
+ assertEquals(
+ "$$", ScriptRunner.scanDollarQuote("SELECT $a$one$a$; CREATE FUNCTION f() AS $$", null));
+ }
+}