diff --git a/src/adql/parser/SQLServer_ADQLQueryFactory.java b/src/adql/parser/SQLServer_ADQLQueryFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..18b2b54d66103eff46d86d66d26ac5ba3bfa2661 --- /dev/null +++ b/src/adql/parser/SQLServer_ADQLQueryFactory.java @@ -0,0 +1,75 @@ +package adql.parser; + +/* + * 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 2016 - Astronomisches Rechen Institut (ARI) + */ + +import adql.query.from.ADQLJoin; +import adql.query.from.CrossJoin; +import adql.query.from.FromContent; +import adql.query.from.InnerJoin; +import adql.query.from.OuterJoin; +import adql.query.from.SQLServer_InnerJoin; +import adql.query.from.SQLServer_OuterJoin; +import adql.query.from.OuterJoin.OuterType; +import adql.translator.SQLServerTranslator; + +/** + * <p>Special extension of {@link ADQLQueryFactory} for MS SQL Server.</p> + * + * <p><b>Important:</b> + * This class is generally used when an ADQL translator for MS SQL Server is needed. + * See {@link SQLServerTranslator} for more details. + * </p> + * + * <p> + * The only difference with {@link ADQLQueryFactory} is the creation of an + * {@link ADQLJoin}. Instead of creating {@link InnerJoin} and {@link OuterJoin}, + * {@link SQLServer_InnerJoin} and {@link SQLServer_OuterJoin} are respectively created. + * The only difference between these last classes and the first ones is in the processing + * of NATURAL JOINs and JOINs using the keyword USING. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 1.4 (03/2016) + * @since 1.4 + * + * @see SQLServer_InnerJoin + * @see SQLServer_OuterJoin + * @see SQLServerTranslator + */ +public class SQLServer_ADQLQueryFactory extends ADQLQueryFactory { + + public ADQLJoin createJoin(JoinType type, FromContent leftTable, FromContent rightTable) throws Exception{ + switch(type){ + case CROSS: + return new CrossJoin(leftTable, rightTable); + case INNER: + return new SQLServer_InnerJoin(leftTable, rightTable); + case OUTER_LEFT: + return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.LEFT); + case OUTER_RIGHT: + return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.RIGHT); + case OUTER_FULL: + return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.FULL); + default: + throw new Exception("Unknown join type: " + type); + } + } + +} \ No newline at end of file diff --git a/src/adql/query/from/SQLServer_InnerJoin.java b/src/adql/query/from/SQLServer_InnerJoin.java new file mode 100644 index 0000000000000000000000000000000000000000..432e1ffcc1fc085a0788ec8d4f4505536673430f --- /dev/null +++ b/src/adql/query/from/SQLServer_InnerJoin.java @@ -0,0 +1,206 @@ +package adql.query.from; + +/* + * 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 2016 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import adql.db.DBColumn; +import adql.db.DBCommonColumn; +import adql.db.SearchColumnList; +import adql.db.exception.UnresolvedJoinException; +import adql.parser.SQLServer_ADQLQueryFactory; +import adql.query.ClauseConstraints; +import adql.query.IdentifierField; +import adql.query.operand.ADQLColumn; + +/** + * <p>Special implementation of {@link InnerJoin} for MS SQL Server.</p> + * + * <p><b>Important:</b> + * Instances of this class are created only by {@link SQLServer_ADQLQueryFactory}. + * </p> + * + * <p> + * This implementation just changes the behavior the {@link #getDBColumns()}. + * In MS SQL Server, there is no keyword NATURAL and USING. That's why the {@link DBColumn}s + * returned by {@link DBColumn} can not contain any {@link DBCommonColumn}. Instead, + * the {@link DBColumn} of the first joined table (i.e. the left one) is returned. + * </p> + * + * <p> + * Since this special behavior is also valid for {@link OuterJoin}, a special implementation + * of this class has been also created: {@link SQLServer_OuterJoin}. Both must have exactly the + * same behavior when {@link #getDBColumns()} is called. That's why the static function + * {@link #getDBColumns(ADQLJoin)} has been created. It is called by {@link SQLServer_InnerJoin} + * and {@link SQLServer_OuterJoin}. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 1.4 (03/2016) + * @since 1.4 + * + * @see SQLServer_ADQLQueryFactory + */ +public class SQLServer_InnerJoin extends InnerJoin { + + /** + * Builds a NATURAL INNER JOIN between the two given "tables". + * + * @param left Left "table". + * @param right Right "table". + * + * @see InnerJoin#InnerJoin(FromContent, FromContent) + */ + public SQLServer_InnerJoin(FromContent left, FromContent right) { + super(left, right); + } + + /** + * Builds an INNER JOIN between the two given "tables" with the given condition. + * + * @param left Left "table". + * @param right Right "table". + * @param condition Join condition. + * + * @see InnerJoin#InnerJoin(FromContent, FromContent, ClauseConstraints) + */ + public SQLServer_InnerJoin(FromContent left, FromContent right, ClauseConstraints condition) { + super(left, right, condition); + } + + /** + * Builds an INNER JOIN between the two given "tables" with the given condition. + * + * @param left Left "table". + * @param right Right "table". + * @param condition Join condition. + * + * @see InnerJoin#InnerJoin(FromContent, FromContent, Collection) + */ + public SQLServer_InnerJoin(FromContent left, FromContent right, Collection<ADQLColumn> lstColumns) { + super(left, right, lstColumns); + } + + /** + * Builds a copy of the given INNER join. + * + * @param toCopy The INNER join to copy. + * + * @throws Exception If there is an error during the copy. + * + * @see InnerJoin#InnerJoin(InnerJoin) + */ + public SQLServer_InnerJoin(InnerJoin toCopy) throws Exception { + super(toCopy); + } + + @Override + public SearchColumnList getDBColumns() throws UnresolvedJoinException { + return getDBColumns(this); + } + + /** + * <p>Gets the list of all columns (~ database metadata) available in this FROM part. + * Columns implied in a NATURAL join or in a USING list, are not returned as a {@link DBCommonColumn} ; + * actually, just the corresponding {@link DBColumn} of the left table is returned.</p> + * + * @return All the available {@link DBColumn}s. + * @throws UnresolvedJoinException If a join is not possible. + */ + public static SearchColumnList getDBColumns(final ADQLJoin join) throws UnresolvedJoinException{ + try{ + SearchColumnList list = new SearchColumnList(); + SearchColumnList leftList = join.getLeftTable().getDBColumns(); + SearchColumnList rightList = join.getRightTable().getDBColumns(); + + /* 1. Figure out duplicated columns */ + HashMap<String,DBColumn> mapDuplicated = new HashMap<String,DBColumn>(); + // CASE: NATURAL + if (join.isNatural()){ + // 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); + // ...add the left column: + mapDuplicated.put(leftCol.getADQLName().toLowerCase(), leftCol); + } + } + + } + // CASE: USING + else if (join.hasJoinedColumns()){ + // For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated + DBColumn leftCol; + ADQLColumn usingCol; + Iterator<ADQLColumn> itCols = join.getJoinedColumns(); + while(itCols.hasNext()){ + usingCol = itCols.next(); + // 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: + findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); + // add the left column: + mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), leftCol); + } + + } + // 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 */ + addAllExcept2(leftList, list, mapDuplicated); + + /* 3. Add all columns of the right list except the ones identified as duplications */ + addAllExcept2(rightList, list, mapDuplicated); + + /* 4. Add all common columns of mapDuplicated */ + list.addAll(0, mapDuplicated.values()); + + return list; + }catch(UnresolvedJoinException uje){ + uje.setPosition(join.getPosition()); + throw uje; + } + } + + public final static void addAllExcept2(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map<String,DBColumn> exception){ + for(DBColumn col : itemsToAdd){ + if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\"")) + target.add(col); + } + } + +} diff --git a/src/adql/query/from/SQLServer_OuterJoin.java b/src/adql/query/from/SQLServer_OuterJoin.java new file mode 100644 index 0000000000000000000000000000000000000000..8f24e49fbb6719e11634e9c26d53c8826dff8d08 --- /dev/null +++ b/src/adql/query/from/SQLServer_OuterJoin.java @@ -0,0 +1,122 @@ +package adql.query.from; + +/* + * 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 2016 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.Collection; + +import adql.db.DBColumn; +import adql.db.DBCommonColumn; +import adql.db.SearchColumnList; +import adql.db.exception.UnresolvedJoinException; +import adql.parser.SQLServer_ADQLQueryFactory; +import adql.query.ClauseConstraints; +import adql.query.operand.ADQLColumn; + +/** + * <p>Special implementation of {@link OuterJoin} for MS SQL Server.</p> + * + * <p><b>Important:</b> + * Instances of this class are created only by {@link SQLServer_ADQLQueryFactory}. + * </p> + * + * <p> + * This implementation just changes the behavior the {@link #getDBColumns()}. + * In MS SQL Server, there is no keyword NATURAL and USING. That's why the {@link DBColumn}s + * returned by {@link DBColumn} can not contain any {@link DBCommonColumn}. Instead, + * the {@link DBColumn} of the first joined table (i.e. the left one) is returned. + * </p> + * + * <p> + * Since this special behavior is also valid for {@link InnerJoin}, a special implementation + * of this class has been also created: {@link SQLServer_InnerJoin}. Both must have exactly the + * same behavior when {@link #getDBColumns()} is called. That's why the static function + * {@link InnerJoin#getDBColumns(ADQLJoin)} has been created. It is called by {@link SQLServer_InnerJoin} + * and {@link SQLServer_OuterJoin}. + * </p> + * + * @author Grégory Mantelet (ARI) + * @version 1.4 (03/2016) + * @since 1.4 + * + * @see SQLServer_ADQLQueryFactory + * @see SQLServer_InnerJoin + */ +public class SQLServer_OuterJoin extends OuterJoin { + + /** + * Builds a NATURAL OUTER join between the two given "tables". + * + * @param left Left "table". + * @param right Right "table". + * @param type OUTER join type (left, right or full). + * + * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType) + */ + public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type) { + super(left, right, type); + } + + /** + * Builds an OUTER join between the two given "tables" with the given condition. + * + * @param left Left "table". + * @param right Right "table". + * @param type Outer join type (left, right or full). + * @param condition Join condition. + * + * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType, ClauseConstraints) + */ + public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type, ClauseConstraints condition) { + super(left, right, type, condition); + } + + /** + * Builds an OUTER join between the two given "tables" with a list of columns to join. + * + * @param left Left "table". + * @param right Right "table". + * @param type Outer join type. + * @param lstColumns List of columns to join. + * + * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType, Collection) + */ + public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type, Collection<ADQLColumn> lstColumns) { + super(left, right, type, lstColumns); + } + + /** + * Builds a copy of the given OUTER join. + * + * @param toCopy The OUTER join to copy. + * + * @throws Exception If there is an error during the copy. + * + * @see OuterJoin#OuterJoin(OuterJoin) + */ + public SQLServer_OuterJoin(OuterJoin toCopy) throws Exception { + super(toCopy); + } + + @Override + public SearchColumnList getDBColumns() throws UnresolvedJoinException{ + return SQLServer_InnerJoin.getDBColumns(this); + } + +} diff --git a/src/adql/translator/SQLServerTranslator.java b/src/adql/translator/SQLServerTranslator.java new file mode 100644 index 0000000000000000000000000000000000000000..775f1b40aa343cbc4342a951cb2b238e702d6f34 --- /dev/null +++ b/src/adql/translator/SQLServerTranslator.java @@ -0,0 +1,398 @@ +package adql.translator; + +/* + * 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 2016 - Astronomisches Rechen Institut (ARI) + */ + +import java.util.ArrayList; +import java.util.Iterator; + +import adql.db.DBChecker; +import adql.db.DBColumn; +import adql.db.DBTable; +import adql.db.DBType; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; +import adql.db.SearchColumnList; +import adql.db.DBType.DBDatatype; +import adql.db.STCS.Region; +import adql.db.exception.UnresolvedJoinException; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.parser.SQLServer_ADQLQueryFactory; +import adql.query.ADQLQuery; +import adql.query.IdentifierField; +import adql.query.from.ADQLJoin; +import adql.query.operand.ADQLColumn; +import adql.query.operand.function.geometry.AreaFunction; +import adql.query.operand.function.geometry.BoxFunction; +import adql.query.operand.function.geometry.CentroidFunction; +import adql.query.operand.function.geometry.CircleFunction; +import adql.query.operand.function.geometry.ContainsFunction; +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.IntersectsFunction; +import adql.query.operand.function.geometry.PointFunction; +import adql.query.operand.function.geometry.PolygonFunction; +import adql.query.operand.function.geometry.RegionFunction; + +/** + * <p>MS SQL Server translator.</p> + * + * <p><b>Important:</b> + * This translator works correctly ONLY IF {@link SQLServer_ADQLQueryFactory} has been used + * to create any ADQL query this translator is asked to translate. + * </p> + * + * TODO See how case sensitivity is supported by MS SQL Server and modify this translator accordingly. + * + * TODO Extend this class for each MS SQL Server extension supporting geometry and particularly + * {@link #translateGeometryFromDB(Object)}, {@link #translateGeometryToDB(Region)} and all this other + * translate(...) functions for the ADQL's geometrical functions. + * + * TODO Check MS SQL Server datatypes (see {@link #convertTypeFromDB(int, String, String, String[])}, + * {@link #convertTypeToDB(DBType)}). + * + * <p><i><b>Important note:</b> + * Geometrical functions are not translated ; the translation returned for them is their ADQL expression. + * </i></p> + * + * @author Grégory Mantelet (ARI) + * @version 1.4 (03/2016) + * @since 1.4 + * + * @see SQLServer_ADQLQueryFactory + */ +public class SQLServerTranslator extends JDBCTranslator { + + /* TODO Temporary MAIN function. + * TO REMOVE for the release. */ + public final static void main(final String[] args) throws Exception { + final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;"; + System.out.println("ADQL Query:\n"+adqlquery); + + ArrayList<DBTable> tables = new ArrayList<DBTable>(2); + DefaultDBTable t = new DefaultDBTable("aTable"); + t.addColumn(new DefaultDBColumn("id", t)); + t.addColumn(new DefaultDBColumn("name", t)); + t.addColumn(new DefaultDBColumn("aColumn", t)); + tables.add(t); + t = new DefaultDBTable("anotherTable"); + t.addColumn(new DefaultDBColumn("id", t)); + t.addColumn(new DefaultDBColumn("name", t)); + t.addColumn(new DefaultDBColumn("anotherColumn", t)); + tables.add(t); + + ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery); + + SQLServerTranslator translator = new SQLServerTranslator(); + System.out.println("\nIn MS SQL Server:\n"+translator.translate(query)); + } + + /** <p>Indicate the case sensitivity to apply to each SQL identifier (only SCHEMA, TABLE and COLUMN).</p> + * + * <p><i>Note: + * In this implementation, this field is set by the constructor and never modified elsewhere. + * It would be better to never modify it after the construction in order to keep a certain consistency. + * </i></p> + */ + protected byte caseSensitivity = 0x00; + + /** + * Builds an SQLServerTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ; + * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation. + */ + public SQLServerTranslator(){ + caseSensitivity = 0x0F; + } + + /** + * Builds an SQLServerTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ; + * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation. + * + * @param allCaseSensitive <i>true</i> to translate all identifiers in a case sensitive manner (surrounded by double quotes), <i>false</i> for case insensitivity. + */ + public SQLServerTranslator(final boolean allCaseSensitive){ + caseSensitivity = allCaseSensitive ? (byte)0x0F : (byte)0x00; + } + + /** + * Builds an SQLServerTranslator which will always translate in SQL identifiers with the defined case sensitivity. + * + * @param catalog <i>true</i> to translate catalog names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param schema <i>true</i> to translate schema names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param table <i>true</i> to translate table names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + * @param column <i>true</i> to translate column names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise. + */ + public SQLServerTranslator(final boolean catalog, final boolean schema, final boolean table, final boolean column){ + caseSensitivity = IdentifierField.CATALOG.setCaseSensitive(caseSensitivity, catalog); + caseSensitivity = IdentifierField.SCHEMA.setCaseSensitive(caseSensitivity, schema); + caseSensitivity = IdentifierField.TABLE.setCaseSensitive(caseSensitivity, table); + caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column); + } + + @Override + public boolean isCaseSensitive(final IdentifierField field) { + return field == null ? false : field.isCaseSensitive(caseSensitivity); + } + + @Override + public String translate(final ADQLJoin join) throws TranslationException { + StringBuffer sql = new StringBuffer(translate(join.getLeftTable())); + + sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' '); + + // CASE: NATURAL + if (join.isNatural()){ + try{ + StringBuffer buf = new StringBuffer(); + + // Find duplicated items between the two lists and translate them as ON conditions: + DBColumn rightCol; + SearchColumnList leftList = join.getLeftTable().getDBColumns(); + SearchColumnList rightList = join.getRightTable().getDBColumns(); + 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 = ADQLJoin.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: + ADQLJoin.findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true); + // ...append the corresponding join condition: + if (buf.length() > 0) + buf.append(" AND "); + buf.append(getQualifiedTableName(leftCol.getTable())).append('.').append(getColumnName(leftCol)); + buf.append("="); + buf.append(getQualifiedTableName(rightCol.getTable())).append('.').append(getColumnName(rightCol)); + } + } + + sql.append("ON ").append(buf.toString()); + }catch(UnresolvedJoinException uje){ + throw new TranslationException("Impossible to resolve the NATURAL JOIN between "+join.getLeftTable().toADQL()+" and "+join.getRightTable().toADQL()+"!", uje); + } + } + // CASE: USING + else if (join.hasJoinedColumns()){ + try{ + StringBuffer buf = new StringBuffer(); + + // For each columns of usingList, check there is in each list exactly one matching column, and then, translate it as ON condition: + DBColumn leftCol, rightCol; + ADQLColumn usingCol; + SearchColumnList leftList = join.getLeftTable().getDBColumns(); + SearchColumnList rightList = join.getRightTable().getDBColumns(); + Iterator<ADQLColumn> itCols = join.getJoinedColumns(); + while(itCols.hasNext()){ + usingCol = itCols.next(); + // 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 = ADQLJoin.findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true); + // idem in the RIGHT list: + rightCol = ADQLJoin.findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false); + // append the corresponding join condition: + if (buf.length() > 0) + buf.append(" AND "); + buf.append(getQualifiedTableName(leftCol.getTable())).append('.').append(getColumnName(leftCol)); + buf.append("="); + buf.append(getQualifiedTableName(rightCol.getTable())).append('.').append(getColumnName(rightCol)); + } + + sql.append("ON ").append(buf.toString()); + }catch(UnresolvedJoinException uje){ + throw new TranslationException("Impossible to resolve the JOIN USING between "+join.getLeftTable().toADQL()+" and "+join.getRightTable().toADQL()+"!", uje); + } + } + // DEFAULT CASE: + else + sql.append(translate(join.getJoinCondition())); + + return sql.toString(); + } + + @Override + public String translate(final ExtractCoord extractCoord) throws TranslationException { + return getDefaultADQLFunction(extractCoord); + } + + @Override + public String translate(final ExtractCoordSys extractCoordSys) throws TranslationException { + return getDefaultADQLFunction(extractCoordSys); + } + + @Override + public String translate(final AreaFunction areaFunction) throws TranslationException { + return getDefaultADQLFunction(areaFunction); + } + + @Override + public String translate(final CentroidFunction centroidFunction) throws TranslationException { + return getDefaultADQLFunction(centroidFunction); + } + + @Override + public String translate(final DistanceFunction fct) throws TranslationException { + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(final ContainsFunction fct) throws TranslationException { + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(final IntersectsFunction fct) throws TranslationException { + return getDefaultADQLFunction(fct); + } + + @Override + public String translate(final PointFunction point) throws TranslationException { + return getDefaultADQLFunction(point); + } + + @Override + public String translate(final CircleFunction circle) throws TranslationException { + return getDefaultADQLFunction(circle); + } + + @Override + public String translate(final BoxFunction box) throws TranslationException { + return getDefaultADQLFunction(box); + } + + @Override + public String translate(final PolygonFunction polygon) throws TranslationException { + return getDefaultADQLFunction(polygon); + } + + @Override + public String translate(final RegionFunction region) throws TranslationException { + return getDefaultADQLFunction(region); + } + + @Override + public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){ + // If no type is provided return VARCHAR: + if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0) + return null; + + // Put the dbmsTypeName in lower case for the following comparisons: + dbmsTypeName = dbmsTypeName.toLowerCase(); + + // Extract the length parameter (always the first one): + int lengthParam = DBType.NO_LENGTH; + if (params != null && params.length > 0){ + try{ + lengthParam = Integer.parseInt(params[0]); + }catch(NumberFormatException nfe){} + } + + // SMALLINT + if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("tinyint") || dbmsTypeName.equals("bit")) + return new DBType(DBDatatype.SMALLINT); + // INTEGER + else if (dbmsTypeName.equals("int")) + return new DBType(DBDatatype.INTEGER); + // BIGINT + else if (dbmsTypeName.equals("bigint")) + return new DBType(DBDatatype.BIGINT); + // REAL (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx) + else if (dbmsTypeName.equals("real") || (dbmsTypeName.equals("float") && lengthParam >= 1 && lengthParam <= 24)) + return new DBType(DBDatatype.REAL); + // DOUBLE (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx) + else if (dbmsTypeName.equals("float") || dbmsTypeName.equals("decimal") || dbmsTypeName.equals("numeric")) + return new DBType(DBDatatype.DOUBLE); + // BINARY + else if (dbmsTypeName.equals("binary")) + return new DBType(DBDatatype.BINARY, lengthParam); + // VARBINARY + else if (dbmsTypeName.equals("varbinary")) + return new DBType(DBDatatype.VARBINARY, lengthParam); + // CHAR + else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("nchar")) + return new DBType(DBDatatype.CHAR, lengthParam); + // VARCHAR + else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("nvarchar")) + return new DBType(DBDatatype.VARCHAR, lengthParam); + // BLOB + else if (dbmsTypeName.equals("image")) + return new DBType(DBDatatype.BLOB); + // CLOB + else if (dbmsTypeName.equals("text") || dbmsTypeName.equals("ntext")) + return new DBType(DBDatatype.CLOB); + // TIMESTAMP + else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("datetime") || dbmsTypeName.equals("datetime2") || dbmsTypeName.equals("datetimeoffset") || dbmsTypeName.equals("smalldatetime") || dbmsTypeName.equals("time") || dbmsTypeName.equals("date") || dbmsTypeName.equals("date")) + return new DBType(DBDatatype.TIMESTAMP); + // Default: + else + return null; + } + + @Override + public String convertTypeToDB(final DBType type){ + if (type == null) + return "varchar"; + + switch(type.type){ + + case SMALLINT: + case REAL: + case BIGINT: + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + return type.type.toString().toLowerCase(); + + case INTEGER: + return "int"; + + // (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx) + case DOUBLE: + return "float(53)"; + + case TIMESTAMP: + return "datetime"; + + case BLOB: + return "image"; + + case CLOB: + return "text"; + + case POINT: + case REGION: + default: + return "varchar"; + } + } + + @Override + public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{ + throw new ParseException("Unsupported geometrical value! The value \"" + jdbcColValue + "\" can not be parsed as a region."); + } + + @Override + public Object translateGeometryToDB(final Region region) throws ParseException{ + throw new ParseException("Geometries can not be uploaded in the database in this implementation!"); + } + +} diff --git a/test/adql/query/from/TestSQLServer_InnerJoin.java b/test/adql/query/from/TestSQLServer_InnerJoin.java new file mode 100644 index 0000000000000000000000000000000000000000..70be938cad22480f5fe969dc1bfc166c602d45ed --- /dev/null +++ b/test/adql/query/from/TestSQLServer_InnerJoin.java @@ -0,0 +1,159 @@ +package adql.query.from; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import adql.db.DBColumn; +import adql.db.DBCommonColumn; +import adql.db.DBType; +import adql.db.DBType.DBDatatype; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; +import adql.db.SearchColumnList; +import adql.query.IdentifierField; +import adql.query.operand.ADQLColumn; + +public class TestSQLServer_InnerJoin { + + private ADQLTable tableA, tableB, tableC; + + @Before + public void setUp() throws Exception{ + /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */ + // Describe the available table: + DefaultDBTable metaTableA = new DefaultDBTable("A"); + metaTableA.setADQLSchemaName("public"); + DefaultDBTable metaTableB = new DefaultDBTable("B"); + metaTableB.setADQLSchemaName("public"); + DefaultDBTable metaTableC = new DefaultDBTable("C"); + metaTableC.setADQLSchemaName("public"); + + // Describe its columns: + metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA)); + metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB)); + metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB)); + metaTableC.addColumn(new DefaultDBColumn("Id", new DBType(DBDatatype.VARCHAR), metaTableC)); + metaTableC.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableC)); + metaTableC.addColumn(new DefaultDBColumn("txtc", new DBType(DBDatatype.VARCHAR), metaTableC)); + + // Build the ADQL tables: + tableA = new ADQLTable("A"); + tableA.setDBLink(metaTableA); + tableB = new ADQLTable("B"); + tableB.setDBLink(metaTableB); + tableC = new ADQLTable("C"); + tableC.setDBLink(metaTableC); + } + + @Test + public void testGetDBColumns(){ + // Test NATURAL JOIN 1: + try{ + ADQLJoin join = new SQLServer_InnerJoin(tableA, tableB); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(3, joinColumns.size()); + List<DBColumn> lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + lstFound = joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + lstFound = joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + + // Test NATURAL JOIN 2: + try{ + ADQLJoin join = new SQLServer_InnerJoin(tableA, tableC); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(3, joinColumns.size()); + + // check id (only the column of table A should be found): + List<DBColumn> lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txta (only the column of table A should be found): + lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txtc (only for table C) + lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("C", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + + // Test with a USING("id"): + try{ + List<ADQLColumn> usingList = new ArrayList<ADQLColumn>(1); + usingList.add(new ADQLColumn("id")); + ADQLJoin join = new SQLServer_InnerJoin(tableA, tableC, usingList); + SearchColumnList joinColumns = join.getDBColumns(); + assertEquals(4, joinColumns.size()); + + // check id (only the column of table A should be found): + List<DBColumn> lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass()); + assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size()); + + // check A.txta and C.txta: + lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true)); + assertEquals(2, lstFound.size()); + // A.txta + assertNotNull(lstFound.get(0).getTable()); + assertEquals("A", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + // C.txta + assertNotNull(lstFound.get(1).getTable()); + assertEquals("C", lstFound.get(1).getTable().getADQLName()); + assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size()); + + // check txtc (only for table C): + lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true)); + assertEquals(1, lstFound.size()); + assertNotNull(lstFound.get(0).getTable()); + assertEquals("C", lstFound.get(0).getTable().getADQLName()); + assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName()); + assertEquals(1, joinColumns.search(null, "public", "C", "txtc", IdentifierField.getFullCaseSensitive(true)).size()); + assertEquals(0, joinColumns.search(null, "public", "A", "txtc", IdentifierField.getFullCaseSensitive(true)).size()); + + }catch(Exception ex){ + ex.printStackTrace(); + fail("This test should have succeeded!"); + } + } + +} diff --git a/test/adql/translator/TestSQLServerTranslator.java b/test/adql/translator/TestSQLServerTranslator.java new file mode 100644 index 0000000000000000000000000000000000000000..d3a0290ed0707900324ee13a0f62261819f9c2dd --- /dev/null +++ b/test/adql/translator/TestSQLServerTranslator.java @@ -0,0 +1,85 @@ +package adql.translator; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import adql.db.DBChecker; +import adql.db.DBTable; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; +import adql.parser.ADQLParser; +import adql.parser.ParseException; +import adql.parser.SQLServer_ADQLQueryFactory; +import adql.query.ADQLQuery; + +public class TestSQLServerTranslator { + + private List<DBTable> tables = null; + + @Before + public void setUp() throws Exception { + tables = new ArrayList<DBTable>(2); + DefaultDBTable t = new DefaultDBTable("aTable"); + t.addColumn(new DefaultDBColumn("id", t)); + t.addColumn(new DefaultDBColumn("name", t)); + t.addColumn(new DefaultDBColumn("aColumn", t)); + tables.add(t); + t = new DefaultDBTable("anotherTable"); + t.addColumn(new DefaultDBColumn("id", t)); + t.addColumn(new DefaultDBColumn("name", t)); + t.addColumn(new DefaultDBColumn("anotherColumn", t)); + tables.add(t); + } + + @Test + public void testNaturalJoin() { + final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;"; + + try{ + ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery); + SQLServerTranslator translator = new SQLServerTranslator(); + + // Test the FROM part: + assertEquals("\"aTable\" AS A INNER JOIN \"anotherTable\" AS B ON \"aTable\".\"id\"=\"anotherTable\".\"id\" AND \"aTable\".\"name\"=\"anotherTable\".\"name\"", translator.translate(query.getFrom())); + + // Test the SELECT part (in order to ensure the usual common columns (due to NATURAL) are actually translated as columns of the first joined table): + assertEquals("SELECT A.\"id\" AS \"id\" , A.\"name\" AS \"name\" , A.\"aColumn\" AS \"aColumn\" , B.\"anotherColumn\" AS \"anotherColumn\"", translator.translate(query.getSelect())); + + }catch(ParseException pe){ + pe.printStackTrace(); + fail("The given ADQL query is completely correct. No error should have occurred while parsing it. (see the console for more details)"); + }catch(TranslationException te){ + te.printStackTrace(); + fail("No error was expected from this translation. (see the console for more details)"); + } + } + + @Test + public void testJoinWithUSING() { + final String adqlquery = "SELECT B.id, name, aColumn, anotherColumn FROM aTable A JOIN anotherTable B USING(name);"; + + try{ + ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery); + SQLServerTranslator translator = new SQLServerTranslator(); + + // Test the FROM part: + assertEquals("\"aTable\" AS A INNER JOIN \"anotherTable\" AS B ON \"aTable\".\"name\"=\"anotherTable\".\"name\"", translator.translate(query.getFrom())); + + // Test the SELECT part (in order to ensure the usual common columns (due to USING) are actually translated as columns of the first joined table): + assertEquals("SELECT B.\"id\" AS \"id\" , A.\"name\" AS \"name\" , A.\"aColumn\" AS \"aColumn\" , B.\"anotherColumn\" AS \"anotherColumn\"", translator.translate(query.getSelect())); + + }catch(ParseException pe){ + pe.printStackTrace(); + fail("The given ADQL query is completely correct. No error should have occurred while parsing it. (see the console for more details)"); + }catch(TranslationException te){ + te.printStackTrace(); + fail("No error was expected from this translation. (see the console for more details)"); + } + } + +}