diff --git a/src/adql/db/DBChecker.java b/src/adql/db/DBChecker.java index 90841974a4f220bb4070c6ce71cbdce06d1e77ac..26de9e19f4139f350bce3a8bb8be49524f0693d0 100644 --- a/src/adql/db/DBChecker.java +++ b/src/adql/db/DBChecker.java @@ -16,7 +16,8 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2011 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2011,2013-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomishes Rechen Institute (ARI) */ import java.util.ArrayList; @@ -26,10 +27,8 @@ import java.util.HashMap; import adql.db.exception.UnresolvedColumnException; import adql.db.exception.UnresolvedIdentifiersException; import adql.db.exception.UnresolvedTableException; - import adql.parser.ParseException; import adql.parser.QueryChecker; - import adql.query.ADQLObject; import adql.query.ADQLQuery; import adql.query.ClauseSelect; @@ -38,9 +37,7 @@ import adql.query.IdentifierField; import adql.query.SelectAllColumns; import adql.query.SelectItem; import adql.query.from.ADQLTable; - import adql.query.operand.ADQLColumn; - import adql.search.ISearchHandler; import adql.search.SearchColumnHandler; import adql.search.SimpleSearchHandler; @@ -62,8 +59,8 @@ import adql.search.SimpleSearchHandler; * can be replaced in SQL by their DB name, if different. This mapping is done automatically by {@link adql.translator.PostgreSQLTranslator}. * </i></p> * - * @author Grégory Mantelet (CDS) - * @version 08/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public class DBChecker implements QueryChecker { @@ -162,6 +159,7 @@ public class DBChecker implements QueryChecker { * @see #resolveColumn(ADQLColumn, SearchColumnList) * @see #checkColumnReference(ColumnReference, ClauseSelect, SearchColumnList) */ + @Override public void check(final ADQLQuery query) throws ParseException{ UnresolvedIdentifiersException errors = new UnresolvedIdentifiersException(); HashMap<DBTable,ADQLTable> mapTables = new HashMap<DBTable,ADQLTable>(); @@ -215,14 +213,13 @@ public class DBChecker implements QueryChecker { } } - SearchColumnList list = query.getFrom().getDBColumns(); - - // // DEBUG - // System.out.println("\n*** FROM COLUMNS ***"); - // for(DBColumn dbCol : list){ - // System.out.println("\t- "+dbCol.getADQLName()+" in "+((dbCol.getTable()==null)?"<NULL>":dbCol.getTable().getADQLName())+" (= "+dbCol.getDBName()+" in "+((dbCol.getTable()==null)?"<NULL>":dbCol.getTable().getDBName())+")"); - // } - // System.out.println(); + SearchColumnList list; + try{ + list = query.getFrom().getDBColumns(); + }catch(ParseException pe){ + errors.addException(pe); + list = new SearchColumnList(); + } // Check the existence of all columns: sHandler = new SearchColumnHandler(); diff --git a/src/adql/db/DBCommonColumn.java b/src/adql/db/DBCommonColumn.java new file mode 100644 index 0000000000000000000000000000000000000000..8a5edab1b00da140912df15a854b1dabd2309c73 --- /dev/null +++ b/src/adql/db/DBCommonColumn.java @@ -0,0 +1,159 @@ +package adql.db; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2014 - Astronomishes Rechen Institute (ARI) + */ + +import java.util.ArrayList; +import java.util.Iterator; + +import adql.query.ADQLQuery; + +/** + * This is a special column which exists only after a NATURAL JOIN or a JOIN ... USING between two tables. + * It lets unify several columns of the joined tables in a single column. + * + * Thus, the writer of an ADQL query can use the column name without table prefix (since after the join there will be only one) + * or with a prefix table of the joined tables. The list of all covered tables is stored in this object and can be extended + * in case of several JOINs. + * + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 1.1 (11/2013) + */ +public class DBCommonColumn implements DBColumn { + + protected DBColumn generalColumnDesc; + protected ArrayList<DBTable> lstCoveredTables = new ArrayList<DBTable>(); + + /** + * Create a column which merges both of the given columns. + * + * This special {@link DBColumn} implementation is not associated with one table, + * and can be listed in a {@link DBTable} ONLY IF the latter is the result of a sub-query (see {@link ADQLQuery#getResultingColumns()}). + * + * A column resulting from a tables join is common only to the joined tables. That's why a list of all tables covered + * by this column is created or update at each merge. It can be accessed thanks to {@link #getCoveredTables()}. + * + * Note: In the case one or both of the columns to join are {@link DBCommonColumn}, the list of their covered tables are also merged. + * + * @param leftCol Column of the left join table. May be a {@link DBCommonColumn}. + * @param rightCol Column of the right join table. May be a {@link DBCommonColumn}. + */ + public DBCommonColumn(final DBColumn leftCol, final DBColumn rightCol){ + + // LEFT COLUMN: + if (leftCol instanceof DBCommonColumn){ + // set the general column description: + generalColumnDesc = ((DBCommonColumn)leftCol).generalColumnDesc; + + // add all covered tables of the left common column: + Iterator<DBTable> it = ((DBCommonColumn)leftCol).getCoveredTables(); + while(it.hasNext()) + addCoveredTable(it.next()); + }else{ + // set the general column description: + generalColumnDesc = leftCol.copy(leftCol.getDBName(), leftCol.getADQLName(), null); + // add the table to cover: + addCoveredTable(leftCol.getTable()); + } + + // RIGHT COLUMN: + if (rightCol instanceof DBCommonColumn){ + // add all covered tables of the left common column: + Iterator<DBTable> it = ((DBCommonColumn)rightCol).getCoveredTables(); + while(it.hasNext()) + addCoveredTable(it.next()); + }else{ + // add the table to cover: + addCoveredTable(rightCol.getTable()); + } + + } + + /** + * Constructor by copy. + * It returns a copy of this instance of {@link DBCommonColumn}. + * + * Note: The list of covered tables is NOT deeply copied! + * + * @param toCopy The {@link DBCommonColumn} to copy. + * @param dbName The new DB name of this {@link DBCommonColumn}. + * @param adqlName The new ADQL name of this {@link DBCommonColumn}. + */ + @SuppressWarnings("unchecked") + public DBCommonColumn(final DBCommonColumn toCopy, final String dbName, final String adqlName){ + generalColumnDesc = toCopy.generalColumnDesc.copy(dbName, adqlName, null); + lstCoveredTables = (ArrayList<DBTable>)toCopy.lstCoveredTables.clone(); + } + + @Override + public final String getADQLName(){ + return generalColumnDesc.getADQLName(); + } + + @Override + public final String getDBName(){ + return generalColumnDesc.getDBName(); + } + + @Override + public final DBTable getTable(){ + return null; + } + + /** + * Get an iterator over the list of all tables covered by this common column. + * + * @return Iterator over all covered tables. + */ + public final Iterator<DBTable> getCoveredTables(){ + return lstCoveredTables.iterator(); + } + + /** + * Add a table that this common column must cover from now. + * + * Warning: no unicity check is never done ! + * + * @param table Table to add in the covered tables list. + */ + protected void addCoveredTable(final DBTable table){ + if (table != null) + lstCoveredTables.add(table); + } + + /** + * WARNING: This copy function does not make a real copy of this DBCommonColumn ! + * It returns a modified copy of the general column description it contains. + * + * Note: To make a real copy of this DBCommonColumn use the Constructor by copy {@link #DBCommonColumn(DBCommonColumn, String, String)}. + * + * @param dbName Its new DB name. + * @param adqlName Its new ADQL name. + * @param dbTable Its new DBTable + * + * @return A modified copy of the general column description this common column represents. + * + * @see adql.db.DBColumn#copy(java.lang.String, java.lang.String, adql.db.DBTable) + */ + @Override + public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable){ + return generalColumnDesc.copy(dbName, adqlName, dbTable); + } + +} diff --git a/src/adql/db/DefaultDBTable.java b/src/adql/db/DefaultDBTable.java index b26c668ddb5d2d16d493ce5cc737abf032ae206d..4bb7e6aec5d46910eb184719774a477513276918 100644 --- a/src/adql/db/DefaultDBTable.java +++ b/src/adql/db/DefaultDBTable.java @@ -16,7 +16,8 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.Collection; @@ -26,8 +27,8 @@ import java.util.Iterator; /** * Default implementation of {@link DBTable}. * - * @author Grégory Mantelet (CDS) - * @version 08/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public class DefaultDBTable implements DBTable { @@ -125,18 +126,22 @@ public class DefaultDBTable implements DBTable { adqlCatalogName = adqlCatName; } + @Override public final String getDBName(){ return dbName; } + @Override public final String getDBSchemaName(){ return dbSchemaName; } + @Override public final String getDBCatalogName(){ return dbCatalogName; } + @Override public final String getADQLName(){ return adqlName; } @@ -145,6 +150,7 @@ public class DefaultDBTable implements DBTable { adqlName = (name != null) ? name : dbName; } + @Override public final String getADQLSchemaName(){ return adqlSchemaName; } @@ -153,6 +159,7 @@ public class DefaultDBTable implements DBTable { adqlSchemaName = (name != null) ? name : dbSchemaName; } + @Override public final String getADQLCatalogName(){ return adqlCatalogName; } @@ -167,6 +174,7 @@ public class DefaultDBTable implements DBTable { * * @see adql.db.DBTable#getColumn(java.lang.String, boolean) */ + @Override public DBColumn getColumn(String colName, boolean byAdqlName){ if (byAdqlName) return columns.get(colName); @@ -183,6 +191,7 @@ public class DefaultDBTable implements DBTable { return (getColumn(colName, byAdqlName) != null); } + @Override public Iterator<DBColumn> iterator(){ return columns.values().iterator(); } @@ -238,10 +247,15 @@ public class DefaultDBTable implements DBTable { return splitRes; } + @Override public DBTable copy(final String dbName, final String adqlName){ DefaultDBTable copy = new DefaultDBTable(dbName, adqlName); - for(DBColumn col : this) - copy.addColumn(col.copy(col.getDBName(), col.getADQLName(), copy)); + for(DBColumn col : this){ + if (col instanceof DBCommonColumn) + copy.addColumn(new DBCommonColumn((DBCommonColumn)col, col.getDBName(), col.getADQLName())); + else + copy.addColumn(col.copy(col.getDBName(), col.getADQLName(), copy)); + } return copy; } diff --git a/src/adql/db/SearchColumnList.java b/src/adql/db/SearchColumnList.java index 1149f379ad637815a8e48f5293c3380c5a6d02c9..7d13a72f97a58d2fe657cbec6a8c6413f70b3fa8 100644 --- a/src/adql/db/SearchColumnList.java +++ b/src/adql/db/SearchColumnList.java @@ -16,18 +16,20 @@ package adql.db; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomishes Rechen Institute (ARI) */ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.NoSuchElementException; import adql.query.IdentifierField; - +import adql.query.from.ADQLJoin; import adql.query.operand.ADQLColumn; - import cds.utils.TextualSearchList; /** @@ -43,8 +45,8 @@ import cds.utils.TextualSearchList; * Table aliases can be listed here with their corresponding table name. Consequently, a table alias can be given as table name in the search parameters. * </i></p> * - * @author Grégory Mantelet (CDS) - * @version 09/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public class SearchColumnList extends TextualSearchList<DBColumn> { private static final long serialVersionUID = 1L; @@ -216,69 +218,124 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { * @see IdentifierField */ public ArrayList<DBColumn> search(final String catalog, final String schema, final String table, final String column, final byte caseSensitivity){ + ArrayList<DBColumn> tmpResult = get(column, IdentifierField.COLUMN.isCaseSensitive(caseSensitivity)); + /* WITH TABLE PREFIX */ if (table != null){ - ArrayList<DBColumn> result = new ArrayList<DBColumn>(); + /* 1. Figure out the table alias */ + String tableName = null; + ArrayList<String> aliasMatches = null; + + // Case sensitive => tableName is set , aliasMatches = null + if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)){ + tableName = tableAliases.get(table); + if (tableName == null) + tableName = table; + } + // Case INsensitive + // a) Alias is found => tableName = null , aliasMatches contains the list of all tables matching the alias + // b) No alias => tableName = table , aliasMatches = null + else{ + aliasMatches = mapAliases.get(table.toLowerCase()); + if (aliasMatches == null || aliasMatches.isEmpty()) + tableName = table; + } + /* 2. For each found column, test whether its table, schema and catalog names match. + * If it matches, keep the column aside. */ + ArrayList<DBColumn> result = new ArrayList<DBColumn>(); for(DBColumn match : tmpResult){ - DBTable dbTable = match.getTable(); - if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)){ - String tableName = tableAliases.get(table); - if (tableName == null) - tableName = table; - if (!dbTable.getADQLName().equals(tableName)) - continue; - }else{ - ArrayList<String> aliases = mapAliases.get(table.toLowerCase()); - if (aliases == null){ - if (!dbTable.getADQLName().equalsIgnoreCase(table)) - continue; - }else{ + + // Get the list of all tables covered by this column: + // - only 1 if it is a normal column + // - several if it is a common column (= result of table join) + Iterator<DBTable> itMatchTables; + if (ADQLJoin.isCommonColumn(match)) + itMatchTables = ((DBCommonColumn)match).getCoveredTables(); + else + itMatchTables = new SingleIterator<DBTable>(match.getTable()); + + // Test the matching with every covered tables: + DBTable matchTable; + while(itMatchTables.hasNext()){ + // get the table: + matchTable = itMatchTables.next(); + + // test the table name: + if (aliasMatches == null){ // case table name is (sensitive) or (INsensitive with no alias found) + if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)){ + if (!matchTable.getADQLName().equals(tableName)) + continue; + }else{ + if (!matchTable.getADQLName().equalsIgnoreCase(tableName)) + continue; + } + }else{ // case INsensitive with at least one alias found boolean foundAlias = false; String temp; - for(int a = 0; !foundAlias && a < aliases.size(); a++){ - temp = tableAliases.get(aliases.get(a)); + for(int a = 0; !foundAlias && a < aliasMatches.size(); a++){ + temp = tableAliases.get(aliasMatches.get(a)); if (temp != null) - foundAlias = dbTable.getADQLName().equalsIgnoreCase(temp); + foundAlias = matchTable.getADQLName().equalsIgnoreCase(temp); } if (!foundAlias) continue; } - } - - if (schema != null){ - if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ - if (!dbTable.getADQLSchemaName().equals(schema)) - continue; - }else{ - if (!dbTable.getADQLSchemaName().equalsIgnoreCase(schema)) - continue; - } - if (catalog != null){ - if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ - if (!dbTable.getADQLCatalogName().equals(catalog)) + // test the schema name: + if (schema != null){ + if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ + if (!matchTable.getADQLSchemaName().equals(schema)) continue; }else{ - if (!dbTable.getADQLCatalogName().equalsIgnoreCase(catalog)) + if (!matchTable.getADQLSchemaName().equalsIgnoreCase(schema)) continue; } + + // test the catalog name: + if (catalog != null){ + if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ + if (!matchTable.getADQLCatalogName().equals(catalog)) + continue; + }else{ + if (!matchTable.getADQLCatalogName().equalsIgnoreCase(catalog)) + continue; + } + } } - } - result.add(match); + // if here, all prefixes are matching and so the column is a good match: + DBColumn goodMatch = matchTable.getColumn(match.getADQLName(), true); + System.out.println("Good match for \"" + catalog + "." + schema + "." + table + "." + column + "\" found: " + goodMatch); + result.add(goodMatch); + } } return result; - }else{ + } + /* NO TABLE PREFIX */ + else{ // Special case: the columns merged by a NATURAL JOIN or a USING may have no table reference: if (tmpResult.size() > 1){ + // List all common columns. If there are several, only the list of matching normal columns must be returned. + // This list must not contain common columns. + // Instead, it must contains all normal columns covered by the common columns. ArrayList<DBColumn> result = new ArrayList<DBColumn>(tmpResult.size()); for(int i = 0; i < tmpResult.size(); i++){ - if (tmpResult.get(i).getTable() == null) - result.add(tmpResult.remove(i)); + if (ADQLJoin.isCommonColumn(tmpResult.get(i))){ + // this common column is a good match + // => add it into the list of matching common columns + // AND remove it from the normal columns list + DBCommonColumn commonColumn = (DBCommonColumn)tmpResult.remove(i); + result.add(commonColumn); + // then, add all normal columns covered by this common columns: + Iterator<DBTable> itCoveredTables = commonColumn.getCoveredTables(); + while(itCoveredTables.hasNext()) + tmpResult.add(itCoveredTables.next().getColumn(column, true)); + } } + if (result.size() == 1) return result; } @@ -335,9 +392,47 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { * @version 09/2011 */ private static class DBColumnKeyExtractor implements KeyExtractor<DBColumn> { + @Override public String getKey(DBColumn obj){ return obj.getADQLName(); } } + /** + * Iterator that iterates over only one item, given in the constructor. + * + * @param <E> Type of the item that this Iterator must return. + * + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 1.1 (11/2013) + * @since 1.1 + */ + private static class SingleIterator< E > implements Iterator<E> { + private final E item; + private boolean done = false; + + public SingleIterator(final E singleItem){ + item = singleItem; + } + + @Override + public boolean hasNext(){ + return !done; + } + + @Override + public E next(){ + if (!done){ + done = true; + return item; + }else + throw new NoSuchElementException(); + } + + @Override + public void remove(){ + throw new UnsupportedOperationException(); + } + } + } diff --git a/src/adql/db/exception/UnresolvedJoin.java b/src/adql/db/exception/UnresolvedJoin.java new file mode 100644 index 0000000000000000000000000000000000000000..e4789b70dadf64a33eb0034526d972c8b7d3115e --- /dev/null +++ b/src/adql/db/exception/UnresolvedJoin.java @@ -0,0 +1,55 @@ +package adql.db.exception; + +/* + * This file is part of ADQLLibrary. + * + * ADQLLibrary is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ADQLLibrary is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright 2013-2014 - Astronomishes Rechen Institute (ARI) + */ + +import adql.parser.ParseException; +import adql.query.TextPosition; + +/** + * This exception is thrown when a table between 2 tables can not be resolved, + * and particularly because of the join condition (i.e. column names not found, ...). + * + * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de + * @version 1.1 (11/2013) + */ +public class UnresolvedJoin extends ParseException { + private static final long serialVersionUID = 1L; + + /** + * Build a simple UnresolvedJoin. + * It is generally used when a column can not be resolved (linked to one of the joined tables). + * + * @param message Message to display explaining why the join can't be resolved. + */ + public UnresolvedJoin(String message){ + super(message); + } + + /** + * Build an UnresolvedJoin and specify, in addition of the error message, the position of the column not resolved. + * + * @param message Message to display explaining why the join can't be resolved. + * @param errorPosition Position of the wrong part of the join. + */ + public UnresolvedJoin(String message, TextPosition errorPosition){ + super(message, errorPosition); + } + +} diff --git a/src/adql/query/ADQLQuery.java b/src/adql/query/ADQLQuery.java index f868009b49c25e03b3ac96e3f9db248f39c84ffc..58513b45779c90862940a1368573e27a5e98b615 100644 --- a/src/adql/query/ADQLQuery.java +++ b/src/adql/query/ADQLQuery.java @@ -16,7 +16,8 @@ package adql.query; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomishes Rechen Institute (ARI) */ import java.util.ArrayList; @@ -25,22 +26,19 @@ import java.util.NoSuchElementException; import adql.db.DBColumn; import adql.db.DefaultDBColumn; - import adql.parser.ADQLParser; - +import adql.parser.ParseException; import adql.query.from.FromContent; - import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; - import adql.search.ISearchHandler; /** * <p>Object representation of an ADQL query or sub-query.</p> * <p>The resulting object of the {@link ADQLParser} is an object of this class.</p> * - * @author Grégory Mantelet (CDS) - * @version 01/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public class ADQLQuery implements ADQLObject { @@ -240,10 +238,12 @@ public class ADQLQuery implements ADQLObject { orderBy = newOrderBy; } + @Override public ADQLObject getCopy() throws Exception{ return new ADQLQuery(this); } + @Override public String getName(){ return "{ADQL query}"; } @@ -261,7 +261,11 @@ public class ADQLQuery implements ADQLObject { for(SelectItem item : select){ ADQLOperand operand = item.getOperand(); if (item instanceof SelectAllColumns){ - columns.addAll(from.getDBColumns()); + try{ + columns.addAll(from.getDBColumns()); + }catch(ParseException pe){ + // Here, this error should not occur any more, since it must have been caught by the DBChecker! + } }else{ DBColumn col = null; if (item.hasAlias()){ @@ -296,12 +300,14 @@ public class ADQLQuery implements ADQLObject { return sHandler.iterator(); } + @Override public ADQLIterator adqlIterator(){ return new ADQLIterator(){ private int index = -1; private ClauseADQL<?> currentClause = null; + @Override public ADQLObject next(){ index++; switch(index){ @@ -329,10 +335,12 @@ public class ADQLQuery implements ADQLObject { return currentClause; } + @Override public boolean hasNext(){ return index + 1 < 6; } + @Override @SuppressWarnings("unchecked") public void replace(ADQLObject replacer) throws UnsupportedOperationException, IllegalStateException{ if (index <= -1) @@ -382,6 +390,7 @@ public class ADQLQuery implements ADQLObject { } } + @Override public void remove(){ if (index <= -1) throw new IllegalStateException("remove() impossible: next() has not yet been called !"); @@ -394,6 +403,7 @@ public class ADQLQuery implements ADQLObject { }; } + @Override public String toADQL(){ StringBuffer adql = new StringBuffer(select.toADQL()); adql.append("\nFROM ").append(from.toADQL()); diff --git a/src/adql/query/from/ADQLJoin.java b/src/adql/query/from/ADQLJoin.java index 3f4736233eeac52a03e334fe0c6dea8f02ec30bb..e9bdbb42ed6f4f4f5d7390d139d878612f419ab3 100644 --- a/src/adql/query/from/ADQLJoin.java +++ b/src/adql/query/from/ADQLJoin.java @@ -16,26 +16,32 @@ package adql.query.from; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomishes Rechen Institute (ARI) */ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.NoSuchElementException; import adql.db.DBColumn; +import adql.db.DBCommonColumn; import adql.db.SearchColumnList; +import adql.db.exception.UnresolvedJoin; import adql.query.ADQLIterator; import adql.query.ADQLObject; import adql.query.ClauseConstraints; +import adql.query.IdentifierField; import adql.query.operand.ADQLColumn; /** * Defines a join between two "tables". * - * @author Grégory Mantelet (CDS) - * @version 08/2011 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public abstract class ADQLJoin implements ADQLObject, FromContent { @@ -179,14 +185,17 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { public final Iterator<ADQLColumn> getJoinedColumns(){ if (lstColumns == null){ return new Iterator<ADQLColumn>(){ + @Override public boolean hasNext(){ return false; } + @Override public ADQLColumn next(){ return null; } + @Override public void remove(){ ; } @@ -225,10 +234,12 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { /* ***************** */ /* INHERITED METHODS */ /* ***************** */ + @Override public String getName(){ return getJoinType(); } + @Override public ADQLIterator adqlIterator(){ return new ADQLIterator(){ @@ -237,6 +248,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { private final int offset = 2 + ((condition == null) ? 0 : 1); private Iterator<ADQLColumn> itCol = null; + @Override public ADQLObject next(){ index++; @@ -254,10 +266,12 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { throw new NoSuchElementException(); } + @Override public boolean hasNext(){ return (itCol != null && itCol.hasNext()) || index + 1 < nbItems; } + @Override public void replace(ADQLObject replacer) throws UnsupportedOperationException, IllegalStateException{ if (index <= -1) throw new IllegalStateException("replace(ADQLObject) impossible: next() has not yet been called !"); @@ -288,6 +302,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { } + @Override public void remove(){ if (index <= -1) throw new IllegalStateException("remove() impossible: next() has not yet been called !"); @@ -305,6 +320,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { }; } + @Override public String toADQL(){ StringBuffer adql = new StringBuffer(leftTable.toADQL()); @@ -323,38 +339,111 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { return adql.toString(); } - public SearchColumnList getDBColumns(){ + @Override + public SearchColumnList getDBColumns() throws UnresolvedJoin{ SearchColumnList list = new SearchColumnList(); + SearchColumnList leftList = leftTable.getDBColumns(); + SearchColumnList rightList = rightTable.getDBColumns(); - SearchColumnList leftList = leftTable.getDBColumns(), rightList = rightTable.getDBColumns(); - list.addAll(leftList); - list.addAll(rightList); - + /* 1. Figure out duplicated columns */ + HashMap<String,DBCommonColumn> mapDuplicated = new HashMap<String,DBCommonColumn>(); + // CASE: NATURAL if (natural){ - SearchColumnList doublons = new SearchColumnList(leftList); - doublons.retainAll(rightList); - for(DBColumn dbCol : doublons) - list.add(dbCol.copy(dbCol.getDBName(), dbCol.getADQLName(), null)); // makes a copy which keeps the same DB and ADQL name but set to NULL the dbTable - }else if (lstColumns != null){ - for(ADQLColumn col : lstColumns){ - ArrayList<DBColumn> dbCols = list.search(col.getCatalogName(), col.getSchemaName(), col.getTableName(), col.getColumnName(), col.getCaseSensitive()); - if (dbCols.size() > 0){ - DBColumn match = dbCols.get(0); - list.add(match.copy(match.getDBName(), match.getADQLName(), null)); // makes a copy which keeps the same DB and ADQL name but set to NULL the dbTable + // Find duplicated items between the two lists and add one common column in mapDuplicated for each + DBColumn rightCol; + for(DBColumn leftCol : leftList){ + // search for at most one column with the same name in the RIGHT list + // and throw an exception is there are several matches: + rightCol = findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false); + // if there is one... + if (rightCol != null){ + // ...check there is only one column with this name in the LEFT list, + // and throw an exception if it is not the case: + findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true); + // ...create a common column: + mapDuplicated.put(leftCol.getADQLName().toLowerCase(), new DBCommonColumn(leftCol, rightCol)); } } } + // CASE: USING + else if (lstColumns != null && !lstColumns.isEmpty()){ + // For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated + DBColumn leftCol, rightCol; + for(ADQLColumn usingCol : lstColumns){ + // search for exactly one column with the same name in the LEFT list + // and throw an exception if there is none, or if there are several matches: + leftCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true); + // idem in the RIGHT list: + rightCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); + // create a common column: + mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), new DBCommonColumn(leftCol, rightCol)); + } + + } + // CASE: NO DUPLICATION TO FIGURE OUT + else{ + // Return the union of both lists: + list.addAll(leftList); + list.addAll(rightList); + return list; + } + + /* 2. Add all columns of the left list except the ones identified as duplications */ + addAllExcept(leftList, list, mapDuplicated); + + /* 3. Add all columns of the right list except the ones identified as duplications */ + addAllExcept(rightList, list, mapDuplicated); + + /* 4. Add all common columns of mapDuplicated */ + list.addAll(mapDuplicated.values()); return list; } + public final static void addAllExcept(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map<String,DBCommonColumn> exception){ + for(DBColumn col : itemsToAdd){ + if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\"")) + target.add(col); + } + } + + public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + DBColumn result = findAtMostOneColumn(columnName, caseSensitive, list, leftList); + if (result == null) + throw new UnresolvedJoin("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); + else + return result; + } + + public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + ArrayList<DBColumn> result = list.search(null, null, null, columnName, caseSensitive); + if (result.isEmpty()) + return null; + else if (result.size() > 1) + throw new UnresolvedJoin("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); + else + return result.get(0); + } + + /** + * Tells whether the given column is a common column (that's to say, a unification of several columns of the same name). + * + * @param col A DBColumn. + * @return true if the given column is a common column, false otherwise (particularly if col = null). + */ + public static final boolean isCommonColumn(final DBColumn col){ + return (col != null && col instanceof DBCommonColumn); + } + + @Override public ArrayList<ADQLTable> getTables(){ ArrayList<ADQLTable> tables = leftTable.getTables(); tables.addAll(rightTable.getTables()); return tables; } + @Override public ArrayList<ADQLTable> getTablesByAlias(final String alias, final boolean caseSensitive){ ArrayList<ADQLTable> tables = leftTable.getTablesByAlias(alias, caseSensitive); tables.addAll(rightTable.getTablesByAlias(alias, caseSensitive)); @@ -371,6 +460,7 @@ public abstract class ADQLJoin implements ADQLObject, FromContent { */ public abstract String getJoinType(); + @Override public abstract ADQLObject getCopy() throws Exception; } \ No newline at end of file diff --git a/src/adql/query/from/FromContent.java b/src/adql/query/from/FromContent.java index 4d29d033a517cef3ad5d1cd7f77818f83f1a4026..2a502f0566c1868ae84d03ad361202909b0c855c 100644 --- a/src/adql/query/from/FromContent.java +++ b/src/adql/query/from/FromContent.java @@ -16,22 +16,23 @@ package adql.query.from; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomishes Rechen Institute (ARI) */ import java.util.ArrayList; import adql.db.DBColumn; import adql.db.SearchColumnList; - +import adql.db.exception.UnresolvedJoin; import adql.query.ADQLObject; /** * Represents the content of the whole or a part of the clause FROM. * It could be either a table ({@link ADQLTable}) or a join ({@link ADQLJoin}). * - * @author Grégory Mantelet (CDS) - * @version 01/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) */ public interface FromContent extends ADQLObject { @@ -41,8 +42,9 @@ public interface FromContent extends ADQLObject { * <p><i><u>Note:</u> In the most cases, this list is generated on the fly !</i></p> * * @return All the available {@link DBColumn}s. + * @throws UnresolvedJoin If a join is not possible. */ - public SearchColumnList getDBColumns(); + public SearchColumnList getDBColumns() throws UnresolvedJoin; /** * Gets all {@link ADQLTable} instances contained in this FROM part (itself included, if it is an {@link ADQLTable}). diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java index e49348089a44a6ee2cf38821ce0131df49215c90..5769fb72c5c330f1aae7df1490c45d63f17b162a 100644 --- a/src/adql/translator/PostgreSQLTranslator.java +++ b/src/adql/translator/PostgreSQLTranslator.java @@ -16,7 +16,8 @@ package adql.translator; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS) + * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institute (ARI) */ import java.util.ArrayList; @@ -25,7 +26,7 @@ import java.util.Iterator; import adql.db.DBColumn; import adql.db.DBTable; - +import adql.db.exception.UnresolvedJoin; import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ADQLOrder; @@ -36,7 +37,6 @@ import adql.query.ColumnReference; import adql.query.IdentifierField; import adql.query.SelectAllColumns; import adql.query.SelectItem; - import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -44,11 +44,9 @@ import adql.query.constraint.Exists; import adql.query.constraint.In; import adql.query.constraint.IsNull; import adql.query.constraint.NotConstraint; - -import adql.query.from.FromContent; import adql.query.from.ADQLJoin; import adql.query.from.ADQLTable; - +import adql.query.from.FromContent; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; import adql.query.operand.Concatenation; @@ -57,13 +55,11 @@ import adql.query.operand.NumericConstant; import adql.query.operand.Operation; import adql.query.operand.StringConstant; import adql.query.operand.WrappedOperand; - import adql.query.operand.function.ADQLFunction; import adql.query.operand.function.MathFunction; import adql.query.operand.function.SQLFunction; import adql.query.operand.function.SQLFunctionType; import adql.query.operand.function.UserDefinedFunction; - import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; import adql.query.operand.function.geometry.CentroidFunction; @@ -73,11 +69,11 @@ import adql.query.operand.function.geometry.DistanceFunction; import adql.query.operand.function.geometry.ExtractCoord; import adql.query.operand.function.geometry.ExtractCoordSys; import adql.query.operand.function.geometry.GeometryFunction; +import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; import adql.query.operand.function.geometry.IntersectsFunction; import adql.query.operand.function.geometry.PointFunction; import adql.query.operand.function.geometry.PolygonFunction; import adql.query.operand.function.geometry.RegionFunction; -import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; /** * <p>Translates all ADQL objects into the SQL adaptation of Postgres.</p> @@ -86,8 +82,8 @@ import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; * You will probably need to extend this translator to correctly manage the geometrical functions. * An extension is already available for PgSphere: {@link PgSphereTranslator}.</b></p> * - * @author Grégory Mantelet (CDS) - * @version 01/2012 + * @author Grégory Mantelet (CDS;ARI) + * @version 1.1 (11/2013) * * @see PgSphereTranslator */ @@ -178,6 +174,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return str.append(id); } + @Override @SuppressWarnings("unchecked") public String translate(ADQLObject obj) throws TranslationException{ if (obj instanceof ADQLQuery) @@ -200,6 +197,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return obj.toADQL(); } + @Override public String translate(ADQLQuery query) throws TranslationException{ StringBuffer sql = new StringBuffer(translate(query.getSelect())); @@ -226,6 +224,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* *************************** */ /* ****** LIST & CLAUSE ****** */ /* *************************** */ + @Override public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException{ if (list instanceof ClauseSelect) return translate((ClauseSelect)list); @@ -260,6 +259,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return sql; } + @Override public String translate(ClauseSelect clause) throws TranslationException{ String sql = null; @@ -275,10 +275,12 @@ public class PostgreSQLTranslator implements ADQLTranslator { return sql; } + @Override public String translate(ClauseConstraints clause) throws TranslationException{ return getDefaultADQLList(clause); } + @Override public String translate(SelectItem item) throws TranslationException{ if (item instanceof SelectAllColumns) return translate((SelectAllColumns)item); @@ -293,6 +295,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return translation.toString(); } + @Override public String translate(SelectAllColumns item) throws TranslationException{ HashMap<String,String> mapAlias = new HashMap<String,String>(); @@ -306,7 +309,11 @@ public class PostgreSQLTranslator implements ADQLTranslator { mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias()); } }else if (item.getQuery() != null){ - dbCols = item.getQuery().getFrom().getDBColumns(); + try{ + dbCols = item.getQuery().getFrom().getDBColumns(); + }catch(UnresolvedJoin pe){ + throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe); + } ArrayList<ADQLTable> tables = item.getQuery().getFrom().getTables(); for(ADQLTable table : tables){ if (table.hasAlias()){ @@ -332,12 +339,13 @@ public class PostgreSQLTranslator implements ADQLTranslator { appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); cols.append(" AS \"").append(col.getADQLName()).append('\"'); } - return cols.toString(); + return (cols.length() > 0) ? cols.toString() : item.toADQL(); }else{ return item.toADQL(); } } + @Override public String translate(ColumnReference ref) throws TranslationException{ if (ref instanceof ADQLOrder) return translate((ADQLOrder)ref); @@ -378,6 +386,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { } } + @Override public String translate(ADQLOrder order) throws TranslationException{ return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC"); } @@ -385,6 +394,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* ************************** */ /* ****** TABLE & JOIN ****** */ /* ************************** */ + @Override public String translate(FromContent content) throws TranslationException{ if (content instanceof ADQLTable) return translate((ADQLTable)content); @@ -394,6 +404,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return content.toADQL(); } + @Override public String translate(ADQLTable table) throws TranslationException{ StringBuffer sql = new StringBuffer(); @@ -420,6 +431,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return sql.toString(); } + @Override public String translate(ADQLJoin join) throws TranslationException{ StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); @@ -453,6 +465,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* ********************* */ /* ****** OPERAND ****** */ /* ********************* */ + @Override public String translate(ADQLOperand op) throws TranslationException{ if (op instanceof ADQLColumn) return translate((ADQLColumn)op); @@ -474,6 +487,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return op.toADQL(); } + @Override public String translate(ADQLColumn column) throws TranslationException{ // Use its DB name if known: if (column.getDBLink() != null){ @@ -500,26 +514,32 @@ public class PostgreSQLTranslator implements ADQLTranslator { return column.getFullColumnName(); } + @Override public String translate(Concatenation concat) throws TranslationException{ return translate((ADQLList<ADQLOperand>)concat); } + @Override public String translate(NegativeOperand negOp) throws TranslationException{ return "-" + translate(negOp.getOperand()); } + @Override public String translate(NumericConstant numConst) throws TranslationException{ return numConst.getValue(); } + @Override public String translate(StringConstant strConst) throws TranslationException{ return "'" + strConst.getValue() + "'"; } + @Override public String translate(WrappedOperand op) throws TranslationException{ return "(" + translate(op.getOperand()) + ")"; } + @Override public String translate(Operation op) throws TranslationException{ return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand()); } @@ -527,6 +547,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* ************************ */ /* ****** CONSTRAINT ****** */ /* ************************ */ + @Override public String translate(ADQLConstraint cons) throws TranslationException{ if (cons instanceof Comparison) return translate((Comparison)cons); @@ -544,26 +565,32 @@ public class PostgreSQLTranslator implements ADQLTranslator { return cons.toADQL(); } + @Override public String translate(Comparison comp) throws TranslationException{ return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand()); } + @Override public String translate(Between comp) throws TranslationException{ return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand()); } + @Override public String translate(Exists exists) throws TranslationException{ return "EXISTS(" + translate(exists.getSubQuery()) + ")"; } + @Override public String translate(In in) throws TranslationException{ return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")"; } + @Override public String translate(IsNull isNull) throws TranslationException{ return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL"; } + @Override public String translate(NotConstraint notCons) throws TranslationException{ return "NOT " + translate(notCons.getConstraint()); } @@ -571,6 +598,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* *********************** */ /* ****** FUNCTIONS ****** */ /* *********************** */ + @Override public String translate(ADQLFunction fct) throws TranslationException{ if (fct instanceof GeometryFunction) return translate((GeometryFunction)fct); @@ -602,6 +630,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return sql + ")"; } + @Override public String translate(SQLFunction fct) throws TranslationException{ if (fct.getType() == SQLFunctionType.COUNT_ALL) return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)"; @@ -609,6 +638,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")"; } + @Override public String translate(MathFunction fct) throws TranslationException{ switch(fct.getType()){ case LOG: @@ -624,6 +654,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { } } + @Override public String translate(UserDefinedFunction fct) throws TranslationException{ return getDefaultADQLFunction(fct); } @@ -631,6 +662,7 @@ public class PostgreSQLTranslator implements ADQLTranslator { /* *********************************** */ /* ****** GEOMETRICAL FUNCTIONS ****** */ /* *********************************** */ + @Override public String translate(GeometryFunction fct) throws TranslationException{ if (fct instanceof AreaFunction) return translate((AreaFunction)fct); @@ -678,54 +710,67 @@ public class PostgreSQLTranslator implements ADQLTranslator { return getDefaultADQLFunction(fct); } + @Override public String translate(GeometryValue<? extends GeometryFunction> geomValue) throws TranslationException{ return translate(geomValue.getValue()); } + @Override public String translate(ExtractCoord extractCoord) throws TranslationException{ return getDefaultGeometryFunction(extractCoord); } + @Override public String translate(ExtractCoordSys extractCoordSys) throws TranslationException{ return getDefaultGeometryFunction(extractCoordSys); } + @Override public String translate(AreaFunction areaFunction) throws TranslationException{ return getDefaultGeometryFunction(areaFunction); } + @Override public String translate(CentroidFunction centroidFunction) throws TranslationException{ return getDefaultGeometryFunction(centroidFunction); } + @Override public String translate(DistanceFunction fct) throws TranslationException{ return getDefaultGeometryFunction(fct); } + @Override public String translate(ContainsFunction fct) throws TranslationException{ return getDefaultGeometryFunction(fct); } + @Override public String translate(IntersectsFunction fct) throws TranslationException{ return getDefaultGeometryFunction(fct); } + @Override public String translate(BoxFunction box) throws TranslationException{ return getDefaultGeometryFunction(box); } + @Override public String translate(CircleFunction circle) throws TranslationException{ return getDefaultGeometryFunction(circle); } + @Override public String translate(PointFunction point) throws TranslationException{ return getDefaultGeometryFunction(point); } + @Override public String translate(PolygonFunction polygon) throws TranslationException{ return getDefaultGeometryFunction(polygon); } + @Override public String translate(RegionFunction region) throws TranslationException{ return getDefaultGeometryFunction(region); } diff --git a/test/adql/SearchColumnListTest.java b/test/adql/SearchColumnListTest.java new file mode 100644 index 0000000000000000000000000000000000000000..776a3435b09af5b21a2a71623e5594e28db1fbe6 --- /dev/null +++ b/test/adql/SearchColumnListTest.java @@ -0,0 +1,240 @@ +package adql; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import adql.db.DBColumn; +import adql.db.DBCommonColumn; +import adql.db.DBTable; +import adql.db.SearchColumnList; +import adql.db.exception.UnresolvedJoin; +import adql.parser.ParseException; +import adql.query.IdentifierField; +import adql.query.operand.ADQLColumn; +import tap.metadata.TAPColumn; +import tap.metadata.TAPSchema; +import tap.metadata.TAPTable; + +public class SearchColumnListTest { + + public static void main(String[] args) throws ParseException{ + + /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ + // Describe the available table: + TAPTable tableA = new TAPTable("A", "TABLE", "NATURAL JOIN Test table", null); + TAPTable tableB = new TAPTable("B", "TABLE", "NATURAL JOIN Test table", null); + TAPTable tableC = new TAPTable("C", "TABLE", "NATURAL JOIN Test table", null); + TAPTable tableD = new TAPTable("D", "TABLE", "NATURAL JOIN Test table", null); + + // Describe its columns: + tableA.addColumn(new TAPColumn("id", "Object ID")); + tableA.addColumn(new TAPColumn("txta", "Text of table A")); + tableB.addColumn(new TAPColumn("id", "Object ID")); + tableB.addColumn(new TAPColumn("txtb", "Text of table B")); + tableC.addColumn(new TAPColumn("Id", "Object ID")); + tableC.addColumn(new TAPColumn("txta", "Text of table A")); + tableC.addColumn(new TAPColumn("txtc", "Text of table C")); + tableD.addColumn(new TAPColumn("id", "Object ID")); + tableD.addColumn(new TAPColumn("txta", "Text of table A")); + tableD.addColumn(new TAPColumn("txtd", "Text of table D")); + + // List all available tables: + TAPSchema schema = new TAPSchema("public"); + schema.addTable(tableA); + schema.addTable(tableB); + schema.addTable(tableC); + schema.addTable(tableD); + + // Build the corresponding SearchColumnList: + SearchColumnList listA = new SearchColumnList(); + for(DBColumn col : tableA) + listA.add(col); + SearchColumnList listB = new SearchColumnList(); + for(DBColumn col : tableB) + listB.add(col); + SearchColumnList listC = new SearchColumnList(); + for(DBColumn col : tableC) + listC.add(col); + SearchColumnList listD = new SearchColumnList(); + for(DBColumn col : tableD) + listD.add(col); + + /* TEST OF NATURAL JOIN */ + System.out.println("### CROSS JOIN ###"); + SearchColumnList crossJoin = join(listA, listB, false, null); + + // DEBUG + for(DBColumn dbCol : crossJoin){ + if (dbCol instanceof DBCommonColumn){ + System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); + Iterator<DBTable> it = ((DBCommonColumn)dbCol).getCoveredTables(); + DBTable table; + while(it.hasNext()){ + table = it.next(); + System.out.print((table == null) ? "<NULL>" : table.getDBName() + ", "); + } + System.out.println(")"); + }else + System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getDBName()) + ")"); + } + System.out.println(); + + /* TEST OF NATURAL JOIN */ + System.out.println("### NATURAL JOIN ###"); + SearchColumnList join1 = join(listA, listB, true, null); + SearchColumnList join2 = join(listC, listD, true, null); + //SearchColumnList join3 = join(join1, join2, true, null); + + // DEBUG + for(DBColumn dbCol : join2){ + if (dbCol instanceof DBCommonColumn){ + System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); + Iterator<DBTable> it = ((DBCommonColumn)dbCol).getCoveredTables(); + DBTable table; + while(it.hasNext()){ + table = it.next(); + System.out.print((table == null) ? "<NULL>" : table.getDBName() + ", "); + } + System.out.println(")"); + }else + System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getDBName()) + ")"); + } + System.out.println(); + + /* TEST OF JOIN USING 1 */ + System.out.println("\n### USING JOIN 1 ###"); + ArrayList<ADQLColumn> usingList = new ArrayList<ADQLColumn>(); + usingList.add(new ADQLColumn("id")); + SearchColumnList joinUsing1 = join(join1, join2, false, usingList); + + // DEBUG + for(DBColumn dbCol : joinUsing1){ + if (dbCol instanceof DBCommonColumn){ + System.out.print("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in "); + Iterator<DBTable> it = ((DBCommonColumn)dbCol).getCoveredTables(); + DBTable table; + while(it.hasNext()){ + table = it.next(); + System.out.print((table == null) ? "<NULL>" : table.getDBName() + ", "); + } + System.out.println(")"); + }else + System.out.println("\t- " + dbCol.getADQLName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getADQLName()) + " (= " + dbCol.getDBName() + " in " + ((dbCol.getTable() == null) ? "<NULL>" : dbCol.getTable().getDBName()) + ")"); + } + System.out.println(); + + /* TEST OF JOIN USING 1 * + System.out.println("\n### USING JOIN 2 ###"); + usingList.clear(); + usingList.add(new TAPColumn("id")); + SearchColumnList joinUsing2 = joinUsing(listA, join3, usingList); + + // DEBUG + for(DBColumn dbCol : joinUsing2){ + System.out.println("\t- "+dbCol.getADQLName()+" in "+((dbCol.getTable()==null)?"<NULL>":dbCol.getTable().getADQLName())+" (= "+dbCol.getDBName()+" in "+((dbCol.getTable()==null)?"<NULL>":dbCol.getTable().getDBName())+")"); + } + System.out.println();*/ + + } + + public static final SearchColumnList join(final SearchColumnList leftList, final SearchColumnList rightList, final boolean natural, final ArrayList<ADQLColumn> usingList) throws UnresolvedJoin{ + + SearchColumnList list = new SearchColumnList(); + /*SearchColumnList leftList = leftTable.getDBColumns(); + SearchColumnList rightList = rightTable.getDBColumns();*/ + + /* 1. Figure out duplicated columns */ + HashMap<String,DBCommonColumn> mapDuplicated = new HashMap<String,DBCommonColumn>(); + // CASE: NATURAL + if (natural){ + // Find duplicated items between the two lists and add one common column in mapDuplicated for each + DBColumn rightCol; + for(DBColumn leftCol : leftList){ + // search for at most one column with the same name in the RIGHT list + // and throw an exception is there are several matches: + rightCol = findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false); + // if there is one... + if (rightCol != null){ + // ...check there is only one column with this name in the LEFT list, + // and throw an exception if it is not the case: + findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true); + // ...create a common column: + mapDuplicated.put(leftCol.getADQLName().toLowerCase(), new DBCommonColumn(leftCol, rightCol)); + } + } + + } + // CASE: USING + else if (usingList != null && !usingList.isEmpty()){ + // For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated + DBColumn leftCol, rightCol; + for(ADQLColumn usingCol : usingList){ + // search for exactly one column with the same name in the LEFT list + // and throw an exception if there is none, or if there are several matches: + leftCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true); + // idem in the RIGHT list: + rightCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); + // create a common column: + mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), new DBCommonColumn(leftCol, rightCol)); + } + + } + // CASE: NO DUPLICATION TO FIGURE OUT + else{ + // Return the union of both lists: + list.addAll(leftList); + list.addAll(rightList); + return list; + } + + /* 2. Add all columns of the left list except the ones identified as duplications */ + addAllExcept(leftList, list, mapDuplicated); + + /* 3. Add all columns of the right list except the ones identified as duplications */ + addAllExcept(rightList, list, mapDuplicated); + + /* 4. Add all common columns of mapDuplicated */ + list.addAll(mapDuplicated.values()); + + return list; + + } + + public final static void addAllExcept(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map<String,DBCommonColumn> exception){ + for(DBColumn col : itemsToAdd){ + if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\"")) + target.add(col); + } + } + + public final static DBColumn findExactlyOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + DBColumn result = findAtMostOneColumn(columnName, caseSensitive, list, leftList); + if (result == null) + throw new UnresolvedJoin("Column \"" + columnName + "\" specified in USING clause does not exist in " + (leftList ? "left" : "right") + " table!"); + else + return result; + } + + public final static DBColumn findAtMostOneColumn(final String columnName, final byte caseSensitive, final SearchColumnList list, final boolean leftList) throws UnresolvedJoin{ + ArrayList<DBColumn> result = list.search(null, null, null, columnName, caseSensitive); + if (result.isEmpty()) + return null; + else if (result.size() > 1) + throw new UnresolvedJoin("Common column name \"" + columnName + "\" appears more than once in " + (leftList ? "left" : "right") + " table!"); + else + return result.get(0); + } + + /** + * Tells whether the given column is a common column (that's to say, a unification of several columns of the same name). + * + * @param col A DBColumn. + * @return true if the given column is a common column, false otherwise (particularly if col = null). + */ + public static final boolean isCommonColumn(final DBColumn col){ + return (col != null && col instanceof DBCommonColumn); + } + +}