blob: b6be6ea525cf1e928347edd28dc95df8e2d49cfe [file] [log] [blame]
/* Copyright (c) 2001-2010, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hsqldb.test;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import org.hsqldb.error.ErrorCode;
import junit.framework.AssertionFailedError;
/**
* is a test which verifies the functionality of replacing asterisks in VIEW statements with column
* lists.
*
* During investigating http://www.openoffice.org/issues/show_bug.cgi?id=78296 (an issue raised
* in OpenOffice.org, which integrates HSQLDB), it rose that views did not behave to the standard
* in that they don't always reflect the table column list at the time of creation of the view.
*
* This was changed so that when you CREATE a VIEW, then any asterisks in the constituting
* statement are replaced with their column lists.
*
* This test verifies this functionality in a number of different flavours.
*
* @author frank.schoenheit@sun.com
*/
public class TestViewAsterisks extends TestBase {
java.sql.Statement m_statement;
java.sql.Connection m_connection;
/** Creates a new instance of TestViewAsterisks */
public TestViewAsterisks(String testName) {
super(testName, null, false, false);
}
/**
* creates the database tables needed for the test
*/
private void setupDatabase() {
try {
m_connection = newConnection();
m_statement = m_connection.createStatement();
executeStatement("DROP TABLE ABC IF EXISTS CASCADE");
executeStatement("DROP TABLE TABLE_A IF EXISTS CASCADE");
executeStatement("DROP TABLE TABLE_B IF EXISTS CASCADE");
executeStatement("DROP VIEW V1 IF EXISTS CASCADE"); // V1 is created by a previous test case
executeStatement(
"CREATE TABLE ABC (ID INTEGER NOT NULL PRIMARY KEY, A VARCHAR(50), B VARCHAR(50), C VARCHAR(50))");
executeStatement("INSERT INTO ABC VALUES (1, 'a', 'b', 'c')");
executeStatement("INSERT INTO ABC VALUES (2, 'd', 'e', 'f')");
executeStatement(
"CREATE TABLE TABLE_A (ID_A INTEGER NOT NULL PRIMARY KEY, NAME_A VARCHAR(50))");
executeStatement("INSERT INTO TABLE_A VALUES (1, 'first A')");
executeStatement("INSERT INTO TABLE_A VALUES (2, 'second A')");
executeStatement(
"CREATE TABLE TABLE_B (ID_B INTEGER NOT NULL PRIMARY KEY, NAME_B VARCHAR(50))");
executeStatement("INSERT INTO TABLE_B VALUES (1, 'first B')");
executeStatement("INSERT INTO TABLE_B VALUES (2, 'second B')");
} catch (SQLException ex) {
fail(ex.toString());
}
}
public void setUp() {
super.setUp();
setupDatabase();
}
protected void tearDown() {
executeStatement("SHUTDOWN");
super.tearDown();
}
/**
* executes a given m_statement
*
* <p>Basically, this method calls <code>m_statement.execute(sql)</code>,
* but wraps any <code>SQLException</code>s into a JUnit error.
*/
private void executeStatement(String sql) {
executeStatement(sql, 0);
}
private void executeStatement(String sql, int expectedVendorCode) {
try {
m_statement.execute(sql);
assertTrue(
"executing\n " + sql
+ "\nwas expected to fail, but it didn't", expectedVendorCode
== 0);
} catch (SQLException ex) {
if (expectedVendorCode == 0) {
fail(ex.toString());
}
assertEquals(
"executing\n " + sql
+ "\ndid not result in the expected error", expectedVendorCode, -ex
.getErrorCode());
}
}
/**
* creates a view with the given name, the given constituting statement, and an optional column list
*
* @param viewName
* specifies the name of the view to create
* @param columnList
* list of names of the columns of the view, will be specified in the CREATE VIEW statement. Might be <code>null</code>.
* @param viewStatement
* the statement of the view
*/
private void createView(String viewName, String[] columnList,
String viewStatement) throws SQLException {
StringBuffer colList = new StringBuffer();
if (columnList != null) {
colList.append(" (");
for (int i = 0; i < columnList.length; ++i) {
colList.append('"').append(columnList[i]).append('"');
if (i < columnList.length - 1) {
colList.append(',');
}
}
colList.append(")");
}
executeStatement("CREATE VIEW " + viewName + colList.toString()
+ " AS " + viewStatement);
if (columnList != null) {
ensureTableColumns(viewName, columnList);
}
}
/**
* retrieves the statement which defines a given view
*/
private String getViewStatement(String viewName) throws SQLException {
ResultSet res = m_statement.executeQuery(
"SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '"
+ viewName + "'");
res.next();
String statement = res.getString(1);
return statement;
}
/**
* ensures two tables (or views, that is) have the same content
*/
private void ensureEqualContent(String tableNameLHS,
String tableNameRHS) throws SQLException {
ResultSet lhs = m_statement.executeQuery("SELECT * FROM \""
+ tableNameLHS + "\"");
ResultSet rhs = m_statement.executeQuery("SELECT * FROM \""
+ tableNameRHS + "\"");
ResultSetMetaData meta = lhs.getMetaData();
while (lhs.next() && rhs.next()) {
for (int col = 1; col <= meta.getColumnCount(); ++col) {
assertEquals("table content does not match: cp. "
+ tableNameLHS + "-" + tableNameRHS + ", row "
+ lhs.getRow() + ", col "
+ col, lhs.getObject(col), rhs.getObject(col));
}
}
// lhs should be after last, rhs still on last
assertTrue("row count does not match: " + tableNameLHS + "-"
+ tableNameRHS, lhs.isAfterLast() && rhs.isLast());
}
/**
* ensures the content of a given table matches a given object array's content
*/
private void ensureTableContent(String tableName,
Object[][] tableData) throws SQLException {
ResultSet lhs = m_statement.executeQuery("SELECT * FROM \""
+ tableName + "\"");
ResultSetMetaData meta = lhs.getMetaData();
int colCount = meta.getColumnCount();
while (lhs.next()) {
int row = lhs.getRow();
assertEquals(colCount, tableData[row - 1].length);
for (int col = 1; col <= colCount; ++col) {
assertEquals(
"unexpected table content in " + tableName + " (row "
+ row + ", col " + col + ")", tableData[row - 1][col - 1],
lhs.getObject(col));
}
}
}
/**
* creates a view with a given name and statement, ensures that it's statement is translated as expected, and ensures
* that the content of the view is as expected
*
* @param viewName
* the name of the to-be-created view
* @param columnNames
* the names of the columns of the view, as to be specified in the CREATE VIEW statement. Might be null,
* in this case the view will be created without an explicit column list
* @param viewStatement
* the statement of the to-be-created view
* @param expectedTranslatedStatement
* the expected statement of the view, after it has been implicitly translated by HSQL. If the actual
* statement after creation does not match this expected statement, this is a failure condition which
* results in a AssertionFailedError being thrown.
* @param expectedContent
* the expected content of the view. If this is <code>null</code>, it is ignored. Else, if it is a
* string, it is interpreted as name of the table which must have the same content as a view. If
* it's no string either, it must be a two-dimensional Object array specifying the expected content.
*/
private void checkViewTranslationAndContent(String viewName,
String[] columnList, String viewStatement,
String expectedTranslatedStatement,
Object expectedContent) throws SQLException {
createView(viewName, columnList, viewStatement);
String actualTranslatedStatement = getViewStatement(viewName);
if (!actualTranslatedStatement.equals(expectedTranslatedStatement)) {
StringBuffer message = new StringBuffer();
message.append(viewName).append(
"'s statement not translated as expected\n");
message.append("original statement:\n ").append(
viewStatement).append('\n');
message.append("expected translated statement:\n ").append(
expectedTranslatedStatement).append('\n');
message.append("actual translated statement:\n ").append(
actualTranslatedStatement).append('\n');
throw new AssertionFailedError(message.toString());
}
if (expectedContent instanceof Object[][]) {
ensureTableContent(viewName, (Object[][]) expectedContent);
}
}
/**
* ensures that a given table has columns with a given name
*/
private void ensureTableColumns(String tableName,
String[] columnNames) throws SQLException {
ResultSet res = m_connection.getMetaData().getColumns(null, null,
tableName, "%");
while (res.next()) {
assertEquals(
"unexpected column name in table \"" + tableName
+ "\" at position "
+ (res.getRow() - 1), res.getString(
"COLUMN_NAME"), columnNames[res.getRow() - 1]);
}
res.previous();
assertEquals("not enough columns in table \"" + tableName + "\"",
columnNames.length, res.getRow());
}
/**
* checks views selecting an asterisk from a table, in different flavours
*/
private void checkSimpleViews() throws SQLException {
// ................................................................
// SELECT *
checkViewTranslationAndContent(
"S1", null, "SELECT * FROM ABC",
"SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC",
"ABC");
// adding a column to "ABC" should succeed, and still leave the view with the columns
// before the addition
executeStatement("ALTER TABLE ABC ADD COLUMN D VARCHAR(50)");
ensureTableColumns("ABC", new String[] {
"ID", "A", "B", "C", "D"
});
ensureTableColumns("S1", new String[] {
"ID", "A", "B", "C"
});
// dropping the column which is not part of the view should be possible
executeStatement("ALTER TABLE ABC DROP COLUMN D");
// dropping another column which *is* part of the view shouldn't
executeStatement("ALTER TABLE ABC DROP COLUMN C", ErrorCode.X_42536);
// finally, dropping the column with CASCADE should work, and also drop the view
//executeStatement("ALTER TABLE ABC DROP COLUMN C CASCADE");
// DROP COLUMN c CASCADE not implemented, yet
// ................................................................
// same as S1, but this time add a LIMIT clause to the statement
checkViewTranslationAndContent(
"S2", null, "SELECT LIMIT 0 2 * FROM ABC ORDER BY ID",
"SELECT LIMIT 0 2 PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC ORDER BY ID",
"ABC");
// ................................................................
// same as S1, but this time add a TOP clause to the statement
checkViewTranslationAndContent(
"S3", null, "SELECT TOP 2 * FROM ABC ORDER BY ID",
"SELECT TOP 2 PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC ORDER BY ID",
"ABC");
// ................................................................
// same as S1, but this time add a DISTINCT clause to the statement
checkViewTranslationAndContent(
"S4", null, "SELECT DISTINCT * FROM ABC",
"SELECT DISTINCT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC",
"ABC");
// ................................................................
// same as S1, but this time qualifying the asterisk
checkViewTranslationAndContent(
"S5", null, "SELECT ABC.* FROM ABC",
"SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC",
"ABC");
// ................................................................
// same as S5, but this time also giving the table an alias
checkViewTranslationAndContent(
"S6", null, "SELECT \"A\".* FROM ABC AS A",
"SELECT A.ID,A.A,A.B,A.C FROM PUBLIC.ABC AS A",
"ABC");
// ................................................................
// same as S1, but bracket the SELECT definition
checkViewTranslationAndContent(
"S7", null, "( SELECT * FROM ABC )",
"(SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC)",
"ABC");
}
/**
* checks views selecting an asterisk plus existing columns
*/
private void checkAsterisksCombined() throws SQLException {
// ................................................................
checkViewTranslationAndContent(
"C1", null, "SELECT * AS \"a2\" FROM ABC",
"SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C AS\"a2\" FROM PUBLIC.ABC",
new Object[][] {
new Object[] {
new Integer(1), "a", "b", "c"
}, new Object[] {
new Integer(2), "d", "e", "f"
}
});
// ................................................................
checkViewTranslationAndContent(
"C2", null, "SELECT B AS \"b2\", ABC.* FROM ABC",
"SELECT B AS\"b2\" , PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC",
new Object[][] {
new Object[] {
"b", new Integer(1), "a", "b", "c"
}, new Object[] {
"e", new Integer(2), "d", "e", "f"
}
});
}
/**
* checks views selecting asterisks from multiple tables
*/
private void checkMultipleTables() throws SQLException {
// ................................................................
checkViewTranslationAndContent(
"M1", null, "SELECT * FROM TABLE_A, TABLE_B",
"SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A,PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B FROM PUBLIC.TABLE_A,PUBLIC.TABLE_B",
new Object[][] {
new Object[] {
new Integer(1), "first A", new Integer(1), "first B"
}, new Object[] {
new Integer(1), "first A", new Integer(2), "second B"
}, new Object[] {
new Integer(2), "second A", new Integer(1), "first B"
}, new Object[] {
new Integer(2), "second A", new Integer(2), "second B"
}
});
// ................................................................
checkViewTranslationAndContent(
"M2", null, "SELECT TABLE_B.*, TABLE_A.* FROM TABLE_A, TABLE_B",
"SELECT PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B , PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A,PUBLIC.TABLE_B",
new Object[][] {
new Object[] {
new Integer(1), "first B", new Integer(1), "first A"
}, new Object[] {
new Integer(2), "second B", new Integer(1), "first A"
}, new Object[] {
new Integer(1), "first B", new Integer(2), "second A"
}, new Object[] {
new Integer(2), "second B", new Integer(2), "second A"
}
});
// ................................................................
checkViewTranslationAndContent(
"M3", null, "SELECT \"TABLE_A\".* FROM TABLE_A, TABLE_B",
"SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A,PUBLIC.TABLE_B",
new Object[][] {
new Object[] {
new Integer(1), "first A"
}, new Object[] {
new Integer(1), "first A"
}, new Object[] {
new Integer(2), "second A"
}, new Object[] {
new Integer(2), "second A"
}
});
}
/**
* checks views selecting from sub selects
*/
private void checkSubSelects() throws SQLException {
// ................................................................
checkViewTranslationAndContent(
"Q1", null, "SELECT * FROM ( SELECT * FROM ABC )",
"SELECT ID,A,B,C FROM(SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC)",
null);
// ................................................................
checkViewTranslationAndContent(
"Q2", null,
"SELECT * FROM ( SELECT * FROM TABLE_A ), ( SELECT * FROM TABLE_B )",
"SELECT ID_A,NAME_A,ID_B,NAME_B FROM(SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A),(SELECT PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B FROM PUBLIC.TABLE_B)",
null);
// ................................................................
checkViewTranslationAndContent(
"Q3", null, "SELECT A.* FROM ( SELECT * FROM TABLE_A ) AS A",
"SELECT A.ID_A,A.NAME_A FROM(SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A)AS A",
null);
// ................................................................
checkViewTranslationAndContent(
"Q4", null,
"SELECT A.*, B.* FROM ( SELECT * FROM TABLE_A ) AS A, ( SELECT * FROM TABLE_B ) AS B",
"SELECT A.ID_A,A.NAME_A , B.ID_B,B.NAME_B FROM(SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A)AS A,(SELECT PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B FROM PUBLIC.TABLE_B)AS B",
null);
}
/**
* checks views which are defined using a column list
*/
private void checkColumnLists() throws SQLException {
// just to ensure the column count handling is as expected, else below tests might be useless
executeStatement("CREATE VIEW IMPOSSIBLE (\"A\") AS SELECT * FROM ABC",
ErrorCode.X_42593);
// ................................................................
// not that it should make any difference to S1, but who knows
checkViewTranslationAndContent("L1", new String[] {
"C1", "C2", "C3", "C4"
}, "SELECT * FROM ABC", "SELECT PUBLIC.ABC.ID,PUBLIC.ABC.A,PUBLIC.ABC.B,PUBLIC.ABC.C FROM PUBLIC.ABC",
"ABC");
}
/**
* checks views based on other views
*/
private void checkViewsOnViews() throws SQLException {
// ................................................................
// not that it should make any difference whether we SELECT FROM a table or view, but who knows
checkViewTranslationAndContent(
"V1", null, "SELECT * FROM S1",
"SELECT PUBLIC.S1.ID,PUBLIC.S1.A,PUBLIC.S1.B,PUBLIC.S1.C FROM PUBLIC.S1", "L1");
}
/**
* checks views based on a UNION statement
*/
private void checkUnionViews() throws SQLException {
checkViewTranslationAndContent(
"U1", null, "SELECT * FROM TABLE_A UNION SELECT * FROM TABLE_B",
"SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A UNION SELECT PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B FROM PUBLIC.TABLE_B",
new Object[][] {
new Object[] {
new Integer(1), "first A"
}, new Object[] {
new Integer(1), "first B"
}, new Object[] {
new Integer(2), "second A"
}, new Object[] {
new Integer(2), "second B"
}
});
checkViewTranslationAndContent(
"U2", null,
"SELECT * FROM ( SELECT * FROM TABLE_A UNION SELECT * FROM TABLE_B )",
"SELECT ID_A,NAME_A FROM(SELECT PUBLIC.TABLE_A.ID_A,PUBLIC.TABLE_A.NAME_A FROM PUBLIC.TABLE_A UNION SELECT PUBLIC.TABLE_B.ID_B,PUBLIC.TABLE_B.NAME_B FROM PUBLIC.TABLE_B)",
new Object[][] {
new Object[] {
new Integer(1), "first A"
}, new Object[] {
new Integer(1), "first B"
}, new Object[] {
new Integer(2), "second A"
}, new Object[] {
new Integer(2), "second B"
}
});
}
/**
* main test method of this class
*/
public void test() {
try {
checkSimpleViews();
checkAsterisksCombined();
checkMultipleTables();
checkSubSelects();
checkColumnLists();
checkViewsOnViews();
checkUnionViews();
} catch (SQLException ex) {
fail(ex.toString());
}
}
/**
* entry point to run the test directly
*/
public static void main(String[] argv) {
runWithResult(TestViewAsterisks.class, "test");
}
}