/* 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"); } }