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)); + } +}