Skip to content
Snippets Groups Projects
Commit cc49ee2c authored by gmantele's avatar gmantele
Browse files

Merge branch 'sqlserver'

Add an ADQLTranslator for MS-SQLServer.
(more improvements may later come but this translator already works)
parents 03a31bc7 82d56925
No related branches found
No related tags found
No related merge requests found
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&eacute;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
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&eacute;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);
}
}
}
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&eacute;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);
}
}
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.ClauseSelect;
import adql.query.IdentifierField;
import adql.query.from.ADQLJoin;
import adql.query.operand.ADQLColumn;
import adql.query.operand.ADQLOperand;
import adql.query.operand.function.DefaultUDF;
import adql.query.operand.function.MathFunction;
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;
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&eacute;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);
}
/* For SQL Server, translate(ADQLQuery) must be overridden for TOP/LIMIT handling.
* We must not add "LIMIT" at the end of the query, it must go in select.
* @see adql.translator.ADQLTranslator#translate(adql.query.ADQLQuery)
*/
@Override
public String translate(ADQLQuery query) throws TranslationException{
StringBuffer sql = new StringBuffer(translate(query.getSelect()));
sql.append("\nFROM ").append(translate(query.getFrom()));
if (!query.getWhere().isEmpty())
sql.append('\n').append(translate(query.getWhere()));
if (!query.getGroupBy().isEmpty())
sql.append('\n').append(translate(query.getGroupBy()));
if (!query.getHaving().isEmpty())
sql.append('\n').append(translate(query.getHaving()));
if (!query.getOrderBy().isEmpty())
sql.append('\n').append(translate(query.getOrderBy()));
return sql.toString();
}
/* For SQL Server, translate(ClauseSelect) must be overridden for TOP/LIMIT handling.
* We must not add "LIMIT" at the end of the query, it must go in select.
* @see adql.translator.ADQLTranslator#translate(adql.query.ClauseSelect)
*/
@Override
public String translate(ClauseSelect clause) throws TranslationException{
String sql = null;
for(int i = 0; i < clause.size(); i++){
if (i == 0){
sql = clause.getName() +
(clause.hasLimit() ? " TOP " + clause.getLimit() + " " : "") +
(clause.distinctColumns() ? " DISTINCT" : "");
}else
sql += " " + clause.getSeparator(i);
sql += " " + translate(clause.get(i));
}
return sql;
}
@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);
// item 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 String translate(MathFunction fct) throws TranslationException{
switch(fct.getType()){
case TRUNCATE:
// third argument to round nonzero means do a truncate
return "round(" + ((fct.getNbParameters() >= 2) ? (translate(fct.getParameter(0)) + ", " + translate(fct.getParameter(1))) : "" ) + ",1)";
case MOD:
return ((fct.getNbParameters() >= 2) ? (translate(fct.getParameter(0)) + "% " + translate(fct.getParameter(1))) : "");
default:
return getDefaultADQLFunction(fct);
}
}
@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!");
}
}
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!");
}
}
}
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)");
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment