| /* |
| * |
| * Derby - Class SURQueryMixTest |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.derbyTesting.functionTests.tests.jdbcapi; |
| import java.sql.DatabaseMetaData; |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.ResultSetMetaData; |
| import java.sql.SQLException; |
| import java.sql.Statement; |
| import java.sql.Types; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| import java.util.Set; |
| import junit.extensions.TestSetup; |
| import junit.framework.Test; |
| import org.apache.derbyTesting.junit.BaseTestSuite; |
| import org.apache.derbyTesting.junit.TestConfiguration; |
| |
| /** |
| * Tests for Scrollable Updatable ResultSet (SUR). This TestCase tests |
| * scrolling (navigation), updates (using updateXXX() and updateRow() or |
| * positioned updates), deletion of records (using deleteRow() or positioned |
| * deletes) of ResultSets. |
| */ |
| public class SURQueryMixTest extends SURBaseTest |
| { |
| /** |
| * Constructor |
| * @param model name of data model for this TestCase |
| * @param query to use for producing the resultset |
| * @param cursorName name of cursor |
| * @param positioned flag to determine if the Test should use positioned |
| * updates/deletes instead of updateRow() and deleteRow() |
| */ |
| public SURQueryMixTest(final String model, final String query, |
| final String cursorName, final boolean positioned) |
| { |
| super("SURQueryMixTest{Model=" + model + ",Query=" +query + ",Cursor=" |
| + cursorName + ",Positioned=" + positioned + "}"); |
| this.query = query; |
| this.cursorName = cursorName; |
| this.positioned = positioned; |
| this.checkRowUpdated = false; |
| this.checkRowDeleted = false; |
| } |
| |
| /** |
| * Test SUR properties of the query |
| */ |
| public void runTest() |
| throws SQLException |
| { |
| println(query); |
| DatabaseMetaData dbMeta = getConnection().getMetaData(); |
| |
| if (dbMeta.ownDeletesAreVisible(ResultSet.TYPE_SCROLL_INSENSITIVE)) { |
| checkRowDeleted = true; |
| } |
| |
| Statement s = createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, |
| ResultSet.CONCUR_UPDATABLE); |
| |
| s.setCursorName(cursorName); |
| ResultSet rs = s.executeQuery(query); |
| |
| checkRowUpdated = dbMeta.ownUpdatesAreVisible(rs.getType()); |
| checkRowDeleted = dbMeta.ownDeletesAreVisible(rs.getType()); |
| |
| // Create map with rows |
| Map<Integer, String> rows = createRowMap(rs); |
| |
| // Set of rows which are updated (contains Integer with position in RS) |
| final Set<Integer> updatedRows = new HashSet<Integer>(); |
| |
| // Set of rows which are deleted (contains Integer with position in RS) |
| final Set<Integer> deletedRows = new HashSet<Integer>(); |
| |
| // Test navigation |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // Only test updatability if the ResultSet is updatable: |
| // (Note: this enables the test do run successfully even if |
| // scrollable updatable resultsets are not implemented. |
| // If SUR is not implemented, a well behaved JDBC driver will |
| // downgrade the concurrency mode to READ_ONLY). |
| // SUR may be implemented incrementally, i.e first in embedded mode |
| // then in the network driver.) |
| if (rs.getConcurrency()==ResultSet.CONCUR_UPDATABLE) { |
| |
| // update a random sample of 2 records |
| updateRandomSampleOfNRecords(rs, rows, updatedRows, 2); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // update a random sample of 5 records |
| updateRandomSampleOfNRecords(rs, rows, updatedRows, 5); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // update a random sample of 10 records |
| updateRandomSampleOfNRecords(rs, rows, updatedRows, 10); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // delete a random sample of 2 records |
| deleteRandomSampleOfNRecords(rs, rows, deletedRows, 2); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // delete a random sample of 5 records |
| deleteRandomSampleOfNRecords(rs, rows, deletedRows, 5); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| |
| // delete a random sample of 10 records |
| deleteRandomSampleOfNRecords(rs, rows, deletedRows, 10); |
| testNavigation(rs, rows, updatedRows, deletedRows); |
| } else { |
| assertTrue("ResultSet concurrency downgraded to CONCUR_READ_ONLY", |
| false); |
| } |
| |
| rs.close(); |
| s.close(); |
| } |
| |
| /** |
| * Creates a Map of the values in the ResultSet. |
| * The key object in the map, is the postion in the |
| * ResultSet (Integer 1..n), while the value is a |
| * concatenation of the strings for all columns in the row. |
| */ |
| private Map<Integer, String> createRowMap(final ResultSet rs) |
| throws SQLException |
| { |
| final Map<Integer, String> rows = new HashMap<Integer, String>(); |
| rs.beforeFirst(); |
| assertTrue("Unexpected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| |
| int i = 0; |
| int sum = 0; |
| int expectedSum = 0; |
| boolean checkSum = true; |
| while (rs.next()) { |
| expectedSum += i; |
| i++; |
| String row = getRowString(rs); |
| println(row); |
| rows.put(i, row); |
| sum += rs.getInt(1); |
| if (rs.getInt(1) < 0) { |
| checkSum = false; |
| } |
| } |
| if (i<SURDataModelSetup.recordCount) { |
| checkSum = false; |
| } |
| |
| assertTrue("Unexpected return from isAfterLast()", rs.isAfterLast()); |
| |
| if (checkSum) { |
| assertEquals("Sum for column 1 is not correct", expectedSum, sum); |
| } |
| |
| return rows; |
| } |
| |
| /** |
| * Create a random sample of rows |
| * @param rows Map to create sample from |
| * @param k number of rows in the sample |
| * @return a list containing k elements of rows |
| **/ |
| private List createRandomSample(final Map<Integer, String> rows, int k) { |
| Random r = new Random(); |
| ArrayList<Integer> sampledKeys = new ArrayList<Integer>(); |
| int n = 0; |
| for (Integer key : rows.keySet()) { |
| n++; |
| if (n<=k) { |
| sampledKeys.add(key); |
| } else { |
| // sampledKeys now has a size of k |
| double d = r.nextDouble(); |
| // p = probability of going into the sample |
| double p = (double) k / (double) n; |
| if (d<p) { |
| // Replace a random value from the sample with the new value |
| int keyToReplace = r.nextInt(k); |
| sampledKeys.set(keyToReplace, key); |
| } |
| } |
| } |
| return sampledKeys; |
| } |
| |
| /** |
| * Delete a random sample of n records in the resultset |
| * @param rs result set to be updated |
| * @param rows map of rows, will also be updated |
| * @param deletedRows set of rows being deleted (position in RS) |
| * @param k number of records to be deleted |
| */ |
| private void deleteRandomSampleOfNRecords(final ResultSet rs, |
| final Map<Integer, String> rows, |
| final Set<Integer> deletedRows, |
| final int k) |
| throws SQLException |
| { |
| List sampledKeys = createRandomSample(rows, k); |
| println("Sampled keys:" + sampledKeys); |
| ResultSetMetaData meta = rs.getMetaData(); |
| for (Iterator i = sampledKeys.iterator(); i.hasNext();) { |
| Integer key = (Integer) i.next(); |
| rs.absolute(key.intValue()); |
| if (rs.rowDeleted()) continue; // skip deleting row if already deleted |
| if (positioned) { |
| createStatement().executeUpdate |
| ("DELETE FROM T1 WHERE CURRENT OF \"" + cursorName + |
| "\""); |
| } else { |
| rs.deleteRow(); |
| } |
| rs.relative(0); |
| println("Deleted row " + key); |
| // Update the rows table |
| rows.put(key, getRowString(rs)); |
| |
| // Update the updatedRows set |
| deletedRows.add(key); |
| } |
| } |
| |
| /** |
| * Update a random sample of n records in the resultset |
| * @param rs result set to be updated |
| * @param rows map of rows, will also be updated |
| * @param updatedRows set of being updated (position in RS) |
| * @param k number of records to be updated |
| */ |
| private void updateRandomSampleOfNRecords(final ResultSet rs, |
| final Map<Integer, String> rows, |
| final Set<Integer> updatedRows, |
| final int k) |
| throws SQLException |
| { |
| List sampledKeys = createRandomSample(rows, k); |
| println("Sampled keys:" + sampledKeys); |
| ResultSetMetaData meta = rs.getMetaData(); |
| for (Iterator i = sampledKeys.iterator(); i.hasNext();) { |
| Integer key = (Integer) i.next(); |
| rs.absolute(key.intValue()); |
| |
| if (positioned) { |
| updatePositioned(rs, meta); |
| rs.relative(0); // If this call is not here, the old values are |
| // returned in rs.getXXX calls |
| } else { |
| updateRow(rs, meta); |
| } |
| // Update the rows table |
| rows.put(key, getRowString(rs)); |
| |
| // Update the updatedRows set |
| updatedRows.add(key); |
| } |
| } |
| |
| /** |
| * Updates the current row in the ResultSet using updateRow() |
| * @param rs ResultSet to be updated |
| * @param meta meta for the ResultSet |
| **/ |
| private void updateRow(final ResultSet rs, final ResultSetMetaData meta) |
| throws SQLException |
| { |
| for (int column = 1; column<=meta.getColumnCount(); column++) { |
| if (meta.getColumnType(column)==Types.INTEGER) { |
| // Set to negative value |
| rs.updateInt(column, -rs.getInt(column)); |
| } else { |
| rs.updateString(column, "UPDATED_" + rs.getString(column)); |
| } |
| } |
| rs.updateRow(); |
| } |
| |
| /** |
| * Updates the current row in the ResultSet using updateRow() |
| * @param rs ResultSet to be updated |
| * @param meta meta for the ResultSet |
| **/ |
| private void updatePositioned(final ResultSet rs, |
| final ResultSetMetaData meta) |
| throws SQLException |
| { |
| StringBuffer sb = new StringBuffer(); |
| sb.append("UPDATE T1 SET "); |
| for (int column = 1; column<=meta.getColumnCount(); column++) { |
| sb.append(meta.getColumnName(column)); |
| sb.append("=?"); |
| if (column<meta.getColumnCount()) { |
| sb.append(","); |
| } |
| } |
| sb.append(" WHERE CURRENT OF \""); |
| sb.append(cursorName); |
| sb.append("\""); |
| println(sb.toString()); |
| PreparedStatement ps = prepareStatement(sb.toString()); |
| |
| for (int column = 1; column<=meta.getColumnCount(); column++) { |
| if (meta.getColumnType(column)==Types.INTEGER) { |
| // Set to negative value |
| ps.setInt(column, -rs.getInt(column)); |
| } else { |
| ps.setString(column, "UPDATED_" + rs.getString(column)); |
| } |
| } |
| assertEquals("Expected one row to be updated", 1, ps.executeUpdate()); |
| } |
| |
| |
| /** |
| * Tests navigation in ResultSet. |
| * @param rs ResultSet to test navigation of. |
| * Needs to be scrollable |
| * @param rows a sample of the rows which are in the ResultSet. Maps |
| * position to a concatenation of the string values |
| * @param updatedRows a integer set of which rows that have been |
| * updated. Used to test rowUpdated() |
| * @param deletedRows a integer set of which rows that have been |
| * deleted. Used to test rowDeleted() |
| */ |
| private void testNavigation(final ResultSet rs, final Map rows, |
| final Set updatedRows, final Set deletedRows) |
| throws SQLException |
| { |
| rs.afterLast(); |
| { |
| int i = rows.size(); |
| while (rs.previous()) { |
| String rowString = getRowString(rs); |
| assertEquals("Navigating with rs.previous(). The row is " + |
| "different compared to the value when navigating " + |
| "forward.", rows.get(i), rowString); |
| |
| |
| if (checkRowUpdated && updatedRows.contains(i)) { |
| assertTrue("Expected rs.rowUpdated() to return true on " + |
| "updated row " + rowString, rs.rowUpdated()); |
| } |
| if (checkRowDeleted && deletedRows.contains(i)) { |
| assertTrue("Expected rs.rowDeleted() to return true on " + |
| "deleted row " + rowString, rs.rowDeleted()); |
| } |
| i--; |
| } |
| } |
| // Test absolute |
| for (int i = 1; i <= rows.size(); i++) { |
| assertTrue("Unexpected return from absolute()", rs.absolute(i)); |
| String rowString = getRowString(rs); |
| assertEquals("Navigating with rs.absolute(). The row is " + |
| "different compared to the value" + |
| " when navigating forward.", |
| rows.get(i), |
| rowString); |
| if (checkRowUpdated && updatedRows.contains(i)) { |
| assertTrue("Expected rs.rowUpdated() to return true on " + |
| "updated row " + rowString, rs.rowUpdated()); |
| } |
| if (checkRowDeleted && deletedRows.contains(i)) { |
| assertTrue("Expected rs.rowDeleted() to return true on " + |
| "deleted row " + rowString, rs.rowDeleted()); |
| } |
| } |
| assertFalse("Unexpected return from absolute()", rs.absolute(0)); |
| assertTrue("Unexpected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| assertFalse("Unexpected return from absolute()", |
| rs.absolute(rows.size() + 1)); |
| assertTrue("Unexpected return from isAfterLast()", rs.isAfterLast()); |
| assertTrue("Unexpected return from absolute()", rs.absolute(-1)); |
| assertTrue("Unexpected return from isLast()", rs.isLast()); |
| assertTrue("Unexpected return from absolute()", rs.absolute(1)); |
| assertTrue("Unexpected return from isFirst()", rs.isFirst()); |
| |
| // Test relative |
| { |
| rs.beforeFirst(); |
| assertTrue("Unexptected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| |
| int relativePos = rows.size(); |
| assertTrue("Unexpected return from relative()", |
| rs.relative(relativePos)); |
| |
| // Should now be on the last row |
| assertTrue("Unexptected return from isLast()", rs.isLast()); |
| assertEquals("Navigating with rs.relative(+). " + |
| "A tuple was different compared to the value" + |
| " when navigating forward.", |
| rows.get(relativePos), |
| getRowString(rs)); |
| |
| assertTrue("Unexpected return from relative()", |
| rs.relative((-relativePos + 1))); |
| |
| // Should now be on the first row |
| assertTrue("Unexptected return from isFirst()", rs.isFirst()); |
| |
| assertEquals("Navigating with rs.relative(-). " + |
| "A tuple was different compared to the value" + |
| " when navigating forward.", |
| rows.get(1), |
| getRowString(rs)); |
| |
| } |
| // Test navigation in the end of the ResultSet |
| rs.afterLast(); |
| assertTrue("Unexpected return from isAfterLast()", rs.isAfterLast()); |
| assertTrue("Unexpected return from previous()", rs.previous()); |
| assertTrue("Unexpected return from isLast()", rs.isLast()); |
| assertFalse("Unexpected return from next()", rs.next()); |
| assertTrue("Unexpected return from isAfterLast()", rs.isAfterLast()); |
| rs.last(); |
| assertTrue("Unexpected return from isLast()", rs.isLast()); |
| assertFalse("Unexpected return from next()", rs.next()); |
| assertTrue("Unexpected return from isAfterLast()", rs.isAfterLast()); |
| |
| |
| // Test navigation in the beginning of the ResultSet |
| rs.beforeFirst(); |
| assertTrue("Unexpected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| assertTrue("Unexpected return from next()", rs.next()); |
| assertTrue("Unexpected return from isFirst", rs.isFirst()); |
| assertFalse("Unexpected return from previous()", rs.previous()); |
| assertTrue("Unexpected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| |
| rs.first(); |
| assertTrue("Unexpected return from isFirst", rs.isFirst()); |
| assertFalse("Unexpected return from previous()", rs.previous()); |
| assertTrue("Unexpected return from isBeforeFirst()", |
| rs.isBeforeFirst()); |
| } |
| |
| /** |
| * Get a concatenation of the values of the |
| * current Row in the ResultSet |
| */ |
| private String getRowString(final ResultSet rs) |
| throws SQLException |
| { |
| int numberOfColumns = rs.getMetaData().getColumnCount(); |
| StringBuffer sb = new StringBuffer(); |
| if (rs.rowDeleted()) return ""; |
| for (int i = 1; i <= numberOfColumns; i++) { |
| sb.append(rs.getString(i)); |
| if (i < numberOfColumns) { |
| sb.append(','); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| private final String query; |
| private final String cursorName; |
| private final boolean positioned; |
| private boolean checkRowUpdated; |
| private boolean checkRowDeleted; |
| |
| private final static String[] selectConditions = new String[] { |
| "WHERE c like 'T%'", |
| " ", |
| "WHERE b > 5", |
| "WHERE id >= a", |
| "WHERE id > 1 and id < 900", |
| "WHERE id = 1", |
| "WHERE id in (1,3,4,600,900,955,966,977,978)", |
| "WHERE a in (1,3,4,600,9200,955,966,977,978)", |
| "WHERE a>2 and a<9000" |
| }; |
| |
| private final static String[] projectConditions = new String[] { |
| "id,c,a,b", |
| "id,c", |
| "a,b", |
| "*", |
| "id,a,b,c", |
| "id,a", |
| "a,b,c", |
| "a,c" |
| }; |
| |
| private static BaseTestSuite createTestCases(final String modelName) { |
| BaseTestSuite suite = new BaseTestSuite(); |
| for (int doPos = 0; doPos<2; doPos++) { |
| boolean positioned = doPos>0; // true if to use positioned updates |
| |
| for (int i = 0; i < selectConditions.length; i++) { |
| for (int j = 0; j < projectConditions.length; j++) { |
| final String cursorName = "cursor_" + i + "_" + j; |
| |
| final String stmtString = "SELECT " + projectConditions[j] + |
| " FROM T1 " + selectConditions[i]; |
| suite.addTest(new SURQueryMixTest(modelName, stmtString, cursorName, |
| positioned)); |
| } |
| } |
| } |
| return suite; |
| } |
| |
| /** |
| * Run in client and embedded. |
| */ |
| public static Test suite() |
| { |
| BaseTestSuite mainSuite = new BaseTestSuite("SURQueryMixTest suite"); |
| |
| mainSuite.addTest(baseSuite("SURQueryMixTest:embedded")); |
| mainSuite.addTest( |
| TestConfiguration.clientServerDecorator( |
| baseSuite("SURQueryMixTest:client"))); |
| |
| return mainSuite; |
| |
| } |
| |
| /** |
| * The suite contains all testcases in this class running on different data models |
| */ |
| private static Test baseSuite(String name) { |
| BaseTestSuite mainSuite = new BaseTestSuite(name); |
| |
| // Iterate over all data models and decorate the tests: |
| for (Iterator i = SURDataModelSetup.SURDataModel.values().iterator(); |
| i.hasNext();) { |
| |
| SURDataModelSetup.SURDataModel model = |
| (SURDataModelSetup.SURDataModel) i.next(); |
| |
| BaseTestSuite suite = createTestCases(model.toString()); |
| TestSetup decorator = new SURDataModelSetup(suite, model); |
| mainSuite.addTest(decorator); |
| } |
| return mainSuite; |
| } |
| } |