Commit 9acc3d8d authored by gmantele's avatar gmantele
Browse files

ADQL: Completely change/improve Joins management, and particularly NATURAL...

ADQL: Completely change/improve Joins management, and particularly NATURAL JOIN and USING JOIN. Now, every joined columns are represented by a DBCommonColumn instance which has a given table coverage.
Before, there was a problem while using at least 3 or 4 NATURAL JOINs. Bug raised by Menelaos Perdikeas (ESAC).
parent 2c18b1b3
Loading
Loading
Loading
Loading
+12 −15
Original line number Diff line number Diff line
@@ -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&eacute;gory Mantelet (CDS)
 * @version 08/2011
 * @author Gr&eacute;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();
+159 −0
Original line number Diff line number Diff line
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&eacute;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);
	}

}
+19 −5
Original line number Diff line number Diff line
@@ -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&eacute;gory Mantelet (CDS)
 * @version 08/2011
 * @author Gr&eacute;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)
		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;
	}

+136 −41
Original line number Diff line number Diff line
@@ -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&eacute;gory Mantelet (CDS)
 * @version 09/2011
 * @author Gr&eacute;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;

			for(DBColumn match : tmpResult){
				DBTable dbTable = match.getTable();
			// Case sensitive => tableName is set , aliasMatches = null
			if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)){
					String tableName = tableAliases.get(table);
				tableName = tableAliases.get(table);
				if (tableName == null)
					tableName = table;
					if (!dbTable.getADQLName().equals(tableName))
			}
			// 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){

				// 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{
					ArrayList<String> aliases = mapAliases.get(table.toLowerCase());
					if (aliases == null){
						if (!dbTable.getADQLName().equalsIgnoreCase(table))
							if (!matchTable.getADQLName().equalsIgnoreCase(tableName))
								continue;
					}else{
						}
					}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;
					}
				}

					// test the schema name:
					if (schema != null){
						if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){
						if (!dbTable.getADQLSchemaName().equals(schema))
							if (!matchTable.getADQLSchemaName().equals(schema))
								continue;
						}else{
						if (!dbTable.getADQLSchemaName().equalsIgnoreCase(schema))
							if (!matchTable.getADQLSchemaName().equalsIgnoreCase(schema))
								continue;
						}

						// test the catalog name:
						if (catalog != null){
							if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){
							if (!dbTable.getADQLCatalogName().equals(catalog))
								if (!matchTable.getADQLCatalogName().equals(catalog))
									continue;
							}else{
							if (!dbTable.getADQLCatalogName().equalsIgnoreCase(catalog))
								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&eacute;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();
		}
	}

}
+55 −0
Original line number Diff line number Diff line
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&eacute;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);
	}

}
Loading