diff --git a/identifiers_rules.md b/identifiers_rules.md new file mode 100644 index 0000000000000000000000000000000000000000..b3219070bc81da9c0c99f11358bc0bbc776dc37c --- /dev/null +++ b/identifiers_rules.md @@ -0,0 +1,218 @@ +_**Date:** 25th Sept. 2019_ +_**Author:** GrĂ©gory Mantelet_ + +# Identifiers' rules in ADQL-Lib + +## Definitions + +These rules apply to any kind of identifier (e.g. table, column). + +To simplify explanations, we will consider here that an identifier is composed of 3 pieces of information: + +- adqlName _- string_ +- adqlCaseSensitive _- boolean_ +- dbName _- string_ + +`adqlName` and `dbName` are never stored in their qualified (e.g. the table prefix inside a full column name) or their delimited (e.g. `"aTable"` is the delimited form of `aTable`) form ; surrounded double quotes (and escaped double quotes: `""`) and prefixes are used/checked at initialization but always discarded for storage. + +If `dbName` is not specified, it is the same as `adqlName`. + +`adqlCaseSensitive` is set to `true` only if `adqlName` was delimited at initialization. + +*Rules about how to build this 3-tuple depend on the origin of the identifier (e.g. `TAP_SCHEMA`, subquery, CTE) ; these rules are detailed below.* + +Let's now see how to write ADQL and SQL queries with a such 3-tuple... + +## Identifiers in ADQL + +In ADQL queries, an identifier MUST be delimited only in the following cases: + +* if a ADQL/SQL reserved keyword + + ```sql + -- declared table identifier: adqlName=`distance` + SELECT ... FROM distance -- INCORRECT because `distance` is a reserved keyword + SELECT ... FROM "distance" -- CORRECT + ``` + +* if not a regular ADQL identifier + + ```sql + -- declared table identifier: adqlName=`2do` + SELECT ... FROM 2do -- INCORRECT because `2do` is starting with a digit + SELECT ... FROM "2do" -- CORRECT + ``` + +* if ambiguous with another identifier of the same type + + ```sql + -- declared column identifiers: adqlName=`id` in `table1` and adqlName=`id` in `table2` + SELECT id FROM table1, table2 -- INCORRECT because the column `id` exists in both tables + SELECT table1.id FROM table1, table2 -- CORRECT + ``` + + + +In any other case, the identifier _MAY_ be delimited, but if not, you are free to write it in upper-/lower-/mixed-case. + +If the identifier is declared in a *CASE-SENSITIVE* way, it MUST be respected when delimited in the ADQL query. + +If the identifier is *CASE-INSENSITIVE*, its delimited ADQL version MUST be all in lower-case. + +Then, the following ADQL queries are perfectly allowed: + +```sql +-- declared table: adqlName=`aTable`, adqlCaseSensitive=`false` +SELECT ... FROM atable -- OK +SELECT ... FROM ATABLE -- OK +SELECT ... FROM "atable" -- OK (because lower-case if not declared CS) +SELECT ... FROM "aTable" -- INCORRECT + +-- declared table: adqlName=`aTable`, adqlCaseSensitive=`true` +SELECT ... FROM atable -- OK +SELECT ... FROM ATABLE -- OK +SELECT ... FROM "atable" -- INCORRECT +SELECT ... FROM "aTable" -- OK +``` + +## SQL translation + +_In this part, we will consider PostgreSQL as SQL target._ + +The `dbName` of an identifier is _always_ considered as _case-sensitive_. So, it will _always_ be written delimited in SQL queries. + +**Examples of SQL queries:** + +```sql +-- declared table: adqlName=`aTable`, dbName=- +-- ADQL: SELECT ... FROM atable +SELECT ... FROM "aTable" + +-- declare table: adqlName=`aTable`, dbName=`DBTable` +-- ADQL: SELECT ... FROM atable +SELECT ... FROM "DBTable" +``` + +## Automatic column aliases in SQL + +To ensure having the expected labels in SQL query results, aliases are automatically added (if none is specified) to all items of the `SELECT` clause. + +As `dbName`s, these default aliases are considered as case sensitive. + +They are built using the `adqlName` of the aliased identifiers. If this name is _not case-sensitive_, the alias will be written in lower-case. But if _case-sensitive_, it is written exactly the same. + +**Examples of SQL queries:** + +```sql +-- declared table: adqlName=`aTable`, dbName=`DBTable` +-- declared columns in `aTable`: +-- * adqlName=`ColCS`, adqlCaseSensitive=`false`, dbName=`dbCol1` +-- * adqlName=`ColCI`, adqlCaseSensitive=`true`, dbName=`dbCol2` +-- ADQL: SELECT colcs, colci FROM atable +SELECT "dbCol1" AS "colcs", "dbCol2" AS "ColCI" FROM "DBTable" +``` + + + +## Schemas/Tables/Columns declared in `TAP_SCHEMA` + +* `adqlName` = `TAP_SCHEMA.(schemas.schema_name|tables.table_name|columns.column_name)` +* `dbName` = `TAP_SCHEMA.(schemas|tables|columns).dbName` or if NULL `adqlName` + +**Examples with `TAP_SCHEMA.tables`:** + +| In TAP_SCHEMA.tables | In ADQL-Lib | +| ---------------------------------------------- | ------------------------------------------------------------ | +| table_name=`aTable`, dbName=_null_ | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=_null_ | +| table_name=`schema.aTable`, dbName=_null_ | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=_null_ | +| table_name=`"aTable"`, dbName=_null_ | adqlName=`aTable`, adqlCaseSensitive=`true`, dbName=_null_ | +| table_name=`schema."aTable"`, dbName=_null_ | adqlName=`aTable`, adqlCaseSensitive=`true`, dbName=_null_ | +| table_name=`aTable`, dbName=`DBTable` | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=`DBTable` | +| table_name=`aTable`, dbName=`"DBTable"` | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=`DBTable` | +| table_name=`aTable`, dbName=`schema.DBTable` | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=`schema.DBTable` | +| table_name=`aTable`, dbName=`schema."DBTable"` | adqlName=`aTable`, adqlCaseSensitive=`false`, dbName=`schema."DBTable"` | + +## Tables from subqueries (i.e. `FROM` and `WITH`) + +_Reminder: in ADQL, a subquery declared as table in the `FROM` clause and a CTE declared in the `WITH` clause MUST always be aliased/named._ + +* `adqlName` = _subquery's alias/CTE's name_ +* `dbName` = `adqlName` + +If the alias/name is *delimited* (i.e. case sensitive), `adqlCaseSensitive` will be set to `true` and surrounding double quotes are removed from `adqlName` . + +If the alias/name is *not delimited*, `adqlName` is set to the alias put into lower-case, and `adqlCaseSensitive` is `false`. + +**Examples:** + +```sql +-- +-- Subqueries in FROM: +-- +SELECT ... FROM (SELECT * FROM atable) AS t1 +SELECT ... FROM (SELECT * FROM atable) AS T1 +-- => adqlName=`t1`, adqlCaseSensitive=`false`, dbName=`t1` + +SELECT ... FROM (SELECT * FROM atable) AS "T1" +-- => adqlName=`T1`, adqlCaseSensitive=`true`, dbName=`T1` + +-- +-- CTEs in WITH: +-- +WITH t1 AS (SELECT * FROM atable) SELECT ... FROM t1 +WITH T1 AS (SELECT * FROM atable) SELECT ... FROM t1 +-- => adqlName=`t1`, adqlCaseSensitive=`false`, dbName=`t1` + +WITH "T1" AS (SELECT * FROM atable) SELECT ... FROM t1 +-- => adqlName=`T1`, adqlCaseSensitive=`true`, dbName=`T1` +``` + +## Columns of a (sub)query + +* If _NOT aliased_: + + * `adqlName` = _original's `adqlName`_ + * `adqlCaseSensitive` = _original's `adqlCaseSensitive`_ + * `dbName` = _original's `dbName`_ + + + +* If _aliased_: + + * `adqlName` = _alias in lower-case if not delimited, exact same alias otherwise_ + * `adqlCaseSensitive` = `true` _if alias is delimited_, `false` _otherwise_ + * `dbName` = `adqlName` + + + +**Examples:** + +```sql +-- declared column: adqlName=`aColumn`, adqlCaseSensitive=`false`, dbName=`DBCol` +SELECT acolumn FROM atable +SELECT AColumn FROM atable +-- => the declared column + +-- declared column: adqlName=`aColumn`, adqlCaseSensitive=`true`, dbName=`DBCol` +SELECT acolumn FROM atable +SELECT AColumn FROM atable +-- => the declared column + +-- declared column: adqlName=`aColumn`, adqlCaseSensitive=`true`, dbName=`DBCol` +SELECT acolumn AS myColumn FROM atable +-- => adqlName=`mycolumn`, adqlCaseSensitive=`false`, dbName=`mycolumn` +SELECT acolumn AS "myColumn" FROM atable +-- => adqlName=`myColumn`, adqlCaseSensitive=`true`, dbName=`myColumn` + +``` + +_**Note:** The new representation of an aliased column has different ADQL and DB names, but the other metadata (e.g. datatype, UCD, ...) of the original column are copied as such._ + +## Duplicated output columns + +_The term 'output columns' means here the columns written in the output format (e.g. VOTable). They are not the columns represented as a 3-tuple in this document._ + +**TODO** + + + diff --git a/src/adql/db/CheckContext.java b/src/adql/db/CheckContext.java new file mode 100644 index 0000000000000000000000000000000000000000..977773f17a928fa98d646166893eea480ea0a4d5 --- /dev/null +++ b/src/adql/db/CheckContext.java @@ -0,0 +1,65 @@ +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 2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) + */ + +/** + * State of the {@link DBChecker} at one recursion level inside an ADQL query. + * + * <p> + * An instance of this class aims to list columns and Common Table Expressions + * (i.e. CTE - temporary tables defined in the WITH clause) available inside + * a specific ADQL (sub-)query. + * </p> + * + * @author Grégory Mantelet (CDS) + * @version 2.0 (10/2019) + * @since 2.0 + */ +public class CheckContext { + + /** List of available CTEs at this level. */ + public final SearchTableApi cteTables; + + /** List of available columns (of all tables). */ + public final SearchColumnList availableColumns; + + /** + * Create a context with the given list of CTEs and columns. + * + * @param cteTables All available CTEs. + * <i>Replaced by an empty list, if NULL.</i> + * @param columns All available columns. + * <i>Replaced by an empty list, if NULL.</i> + */ + public CheckContext(final SearchTableApi cteTables, final SearchColumnList columns) { + this.cteTables = (cteTables == null ? new SearchTableList() : cteTables); + this.availableColumns = (columns == null ? new SearchColumnList() : columns); + } + + /** + * Create a deep copy of this context. + * + * @return Deep copy. + */ + public CheckContext getCopy() { + return new CheckContext(cteTables.getCopy(), new SearchColumnList(availableColumns)); + } + +} diff --git a/src/adql/db/DBChecker.java b/src/adql/db/DBChecker.java index b83e7fefc3f4f2a37481ef165d4c08e765bcd153..9bbb80fa368365ad63cadf1b8beccf18db4570f0 100644 --- a/src/adql/db/DBChecker.java +++ b/src/adql/db/DBChecker.java @@ -25,7 +25,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -50,6 +49,7 @@ import adql.query.ColumnReference; import adql.query.IdentifierField; import adql.query.SelectAllColumns; import adql.query.SelectItem; +import adql.query.WithItem; import adql.query.from.ADQLTable; import adql.query.from.FromContent; import adql.query.operand.ADQLColumn; @@ -71,14 +71,14 @@ import adql.search.SimpleReplaceHandler; import adql.search.SimpleSearchHandler; /** + * <h3>ADQL Query verification</h3> + * * This {@link QueryChecker} implementation is able to do the following * verifications on an ADQL query: * <ol> - * <li>Check the existence of all table and column references found in a - * query</li> - * <li>Resolve all unknown functions as supported User Defined Functions - * (UDFs)</li> - * <li>Check that types of columns and UDFs match with their context</li> + * <li>Check existence of all table and column references,</li> + * <li>Resolve User Defined Functions (UDFs),</li> + * <li>Check types of columns and UDFs.</li> * </ol> * * <p><i><b>IMPORTANT note:</b> @@ -90,21 +90,16 @@ import adql.search.SimpleSearchHandler; * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). * </i></p> * - * <h3>Check tables and columns</h3> + * <h3>DB content annotations</h3> + * * <p> * In addition to check the existence of tables and columns referenced in the - * query, this checked will also attach database metadata on these references - * ({@link ADQLTable} and {@link ADQLColumn} instances when they are resolved. + * query, database metadata ({@link DBTable} or {@link DBColumn}) will also be + * attached to ({@link ADQLTable} and {@link ADQLColumn} instances when they + * are resolved. * </p> * - * <p>These information are:</p> - * <ul> - * <li>the corresponding {@link DBTable} or {@link DBColumn} (see getter and - * setter for DBLink in {@link ADQLTable} and {@link ADQLColumn})</li> - * <li>the link between an {@link ADQLColumn} and its {@link ADQLTable}</li> - * </ul> - * - * <p><i><u>Note:</u> + * <p><i><b>Note:</b> * Knowing DB metadata of {@link ADQLTable} and {@link ADQLColumn} is * particularly useful for the translation of the ADQL query to SQL, because * the ADQL name of columns and tables can be replaced in SQL by their DB name, @@ -113,77 +108,42 @@ import adql.search.SimpleSearchHandler; * </i></p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (08/2019) + * @version 2.0 (09/2019) */ public class DBChecker implements QueryChecker { - /** List of all available tables ({@link DBTable}). */ + /** List of all available tables ({@link DBTable}). + * <p><i><b>IMPORTANT: List shared with all threads.</b> + * This list must list all the tables in common to any ADQL query. It + * must never contain any temporary table (e.g. uploads). + * </i></p> */ protected SearchTableApi lstTables; - /** List of all allowed geometrical functions (i.e. CONTAINS, REGION, POINT, - * COORD2, ...). - * - * <p> - * If this list is NULL, all geometrical functions are allowed. - * However, if not, all items of this list must be the only allowed - * geometrical functions. So, if the list is empty, no such function is - * allowed. - * </p> - * - * @since 1.3 - * @deprecated Since v2.0, supported geometrical functions must be declared - * in ADQLParser. */ - @Deprecated - protected String[] allowedGeo = null; - - /** <p>List of all allowed coordinate systems.</p> - * <p> - * Each item of this list must be of the form: "{frame} {refpos} {flavor}". - * Each of these 3 items can be either of value, a list of values expressed with the syntax "({value1}|{value2}|...)" - * or a '*' to mean all possible values. - * </p> - * <p><i>Note: since a default value (corresponding to the empty string - '') should always be possible for each part of a coordinate system, - * the checker will always add the default value (UNKNOWNFRAME, UNKNOWNREFPOS or SPHERICAL2) into the given list of possible values for each coord. sys. part.</i></p> - * <p> - * If this list is NULL, all coordinates systems are allowed. - * However, if not, all items of this list must be the only allowed coordinate systems. - * So, if the list is empty, none is allowed. - * </p> - * @since 1.3 - * @deprecated Since v2.0, supported coordinate systems must be declared - * in ADQLParser. */ - @Deprecated - protected String[] allowedCoordSys = null; - - /** <p>A regular expression built using the list of allowed coordinate systems. - * With this regex, it is possible to known whether a coordinate system expression is allowed or not.</p> - * <p>If NULL, all coordinate systems are allowed.</p> - * @since 1.3 - * @deprecated Since v2.0, supported coordinate systems must be declared - * in ADQLParser. */ - @Deprecated - protected String coordSysRegExp = null; - - /** <p>List of all allowed User Defined Functions (UDFs).</p> - * <p> + /** List of all allowed User Defined Functions (UDFs). + * <p><i><b>Note:</b> * If this list is NULL, any encountered UDF will be allowed. * However, if not, all items of this list must be the only allowed UDFs. * So, if the list is empty, no UDF is allowed. - * </p> + * </i></p> + * <p><i><b>IMPORTANT: List shared with all threads.</b></i></p> * @since 1.3 */ protected FunctionDef[] allowedUdfs = null; - /* ************ */ - /* CONSTRUCTORS */ - /* ************ */ + /* ********************************************************************** + * CONSTRUCTORS * + ********************************************************************** */ + /** - * <p>Builds a {@link DBChecker} with an empty list of tables.</p> + * Builds a {@link DBChecker} with an empty list of tables. * * <p>Verifications done by this object after creation:</p> * <ul> - * <li>Existence of tables and columns: <b>NO <i>(even unknown or fake tables and columns are allowed)</i></b></li> - * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> - * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> + * <li>Existence of tables and columns: + * <b>NO <i>(even unknown or fake tables and columns are allowed)</i></b></li> + * <li>Existence of User Defined Functions (UDFs): + * <b>NO <i>(any "unknown" function is allowed)</i></b></li> + * <li>Types consistency: + * <b>NO</b></li> * </ul> */ public DBChecker() { @@ -191,13 +151,16 @@ public class DBChecker implements QueryChecker { } /** - * <p>Builds a {@link DBChecker} with the given list of known tables.</p> + * Builds a {@link DBChecker} with the given list of known tables. * * <p>Verifications done by this object after creation:</p> * <ul> - * <li>Existence of tables and columns: <b>OK</b></li> - * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> - * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> + * <li>Existence of tables and columns: + * <b>OK</b></li> + * <li>Existence of User Defined Functions (UDFs): + * <b>NO <i>(any "unknown" function is allowed)</i></b></li> + * <li>Types consistency: + * <b>OK, except with unknown functions</b></li> * </ul> * * @param tables List of all available tables. @@ -207,20 +170,26 @@ public class DBChecker implements QueryChecker { } /** - * <p>Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.</p> + * Builds a {@link DBChecker} with the given list of known tables and with a + * restricted list of User Defined Functions (UDFs). * * <p>Verifications done by this object after creation:</p> * <ul> - * <li>Existence of tables and columns: <b>OK</b></li> - * <li>Existence of User Defined Functions (UDFs): <b>OK</b></li> - * <li>Support of coordinate systems: <b>NO <i>(all valid coordinate systems are allowed)</i></b></li> + * <li>Existence of tables and columns: + * <b>OK</b></li> + * <li>Existence of User Defined Functions (UDFs): + * <b>OK</b></li> + * <li>Types consistency: + * <b>OK</b></li> * </ul> * * @param tables List of all available tables. * @param allowedUdfs List of all allowed user defined functions. - * If NULL, no verification will be done (and so, all UDFs are allowed). + * If NULL, no verification will be done (and so, all + * UDFs are allowed). * If empty list, no "unknown" (or UDF) is allowed. - * <i>Note: match with items of this list are done case insensitively.</i> + * <i><b>Note:</b> match with items of this list are + * done case insensitively.</i> * * @since 1.3 */ @@ -250,144 +219,17 @@ public class DBChecker implements QueryChecker { } } + /* ********************************************************************** + * SETTERS * + ********************************************************************** */ /** - * <p>Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.</p> - * - * <p>Verifications done by this object after creation:</p> - * <ul> - * <li>Existence of tables and columns: <b>OK</b></li> - * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> - * <li>Support of geometrical functions: <b>OK</b></li> - * <li>Support of coordinate systems: <b>OK</b></li> - * </ul> - * - * @param tables List of all available tables. - * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). - * If NULL, no verification will be done (and so, all geometries are allowed). - * If empty list, no geometry function is allowed. - * <i>Note: match with items of this list are done case insensitively.</i> - * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: - * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. - * Each part of this pattern can be one the possible values (case insensitive), a list of possible values - * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. - * For instance: "ICRS (GEOCENTER|heliocenter) *". - * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). - * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). - * - * @since 1.3 - * @deprecated Since v2.0, the check of geometrical functions support is - * performed in ADQLParser. It must now be done with - * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} - * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). - */ - @Deprecated - public DBChecker(final Collection<? extends DBTable> tables, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException { - this(tables, null, allowedGeoFcts, allowedCoordSys); - } - - /** - * <p>Builds a {@link DBChecker}.</p> - * - * <p>Verifications done by this object after creation:</p> - * <ul> - * <li>Existence of tables and columns: <b>OK</b></li> - * <li>Existence of User Defined Functions (UDFs): <b>OK</b></li> - * <li>Support of coordinate systems: <b>OK</b></li> - * </ul> - * - * <p><i><b>IMPORTANT note:</b> - * Since v2.0, the check of supported geometrical functions is performed - * directly in ADQLParser through the notion of Optional Features. - * The declaration of supported geometrical functions must now be done - * with {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} - * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). - * </i></p> - * - * @param tables List of all available tables. - * @param allowedUdfs List of all allowed user defined functions. - * If NULL, no verification will be done (and so, all UDFs are allowed). - * If empty list, no "unknown" (or UDF) is allowed. - * <i>Note: match with items of this list are done case insensitively.</i> - * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). - * If NULL, no verification will be done (and so, all geometries are allowed). - * If empty list, no geometry function is allowed. - * <i>Note: match with items of this list are done case insensitively.</i> - * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: - * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. - * Each part of this pattern can be one the possible values (case insensitive), a list of possible values - * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. - * For instance: "ICRS (GEOCENTER|heliocenter) *". - * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). - * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). - * - * @since 2.0 - * @deprecated Since v2.0, the check of geometrical functions support is - * performed in ADQLParser. It must now be done with - * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} - * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). - */ - @Deprecated - public DBChecker(final Collection<? extends DBTable> tables, final Collection<? extends FunctionDef> allowedUdfs, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException { - // Set the list of available tables + Set the list of all known UDFs: - this(tables, allowedUdfs); - - // Set the list of allowed geometrical functions: - allowedGeo = specialSort(allowedGeoFcts); - - // Set the list of allowed coordinate systems: - this.allowedCoordSys = specialSort(allowedCoordSys); - coordSysRegExp = STCS.buildCoordSysRegExp(this.allowedCoordSys); - } - - /** - * Transform the given collection of string elements in a sorted array. - * Only non-NULL and non-empty strings are kept. - * - * @param items Items to copy and sort. - * - * @return A sorted array containing all - except NULL and empty strings - items of the given collection. - * - * @since 1.3 - * - * @deprecated Since v2.0, this tool function is no longer used. It was - * useful only to collect allowed geometries and coordinate - * systems....but these are now checked by - * {@link adql.parser.ADQLParser ADQLParser}. - */ - @Deprecated - protected final static String[] specialSort(final Collection<String> items) { - // Nothing to do if the array is NULL: - if (items == null) - return null; - - // Keep only valid items (not NULL and not empty string): - String[] tmp = new String[items.size()]; - int cnt = 0; - for(String item : items) { - if (item != null && item.trim().length() > 0) - tmp[cnt++] = item; - } - - // Make an adjusted array copy: - String[] copy = new String[cnt]; - System.arraycopy(tmp, 0, copy, 0, cnt); - - // Sort the values: - Arrays.sort(copy); - - return copy; - } - - /* ****** */ - /* SETTER */ - /* ****** */ - /** - * <p>Sets the list of all available tables.</p> + * Sets the list of all available tables. * - * <p><i><u>Note:</u> - * Only if the given collection is NOT an implementation of - * {@link SearchTableApi}, the collection will be copied inside a new - * {@link SearchTableList}, otherwise it is used as provided. + * <p><i><b>Note:</b> + * Only if the given collection is an implementation of + * {@link SearchTableApi}, it will be used directly as provided. + * Otherwise the given collection will be copied inside a new + * {@link SearchTableList}. * </i></p> * * @param tables List of {@link DBTable}s. @@ -401,16 +243,17 @@ public class DBChecker implements QueryChecker { lstTables = new SearchTableList(tables); } - /* ************* */ - /* CHECK METHODS */ - /* ************* */ + /* ********************************************************************** + * CHECK METHODS * + ********************************************************************** */ + /** - * Check all the columns, tables and UDFs references inside the given query. + * Check all the column, table and UDF references inside the given query. * * <p><i><b>Note:</b> * This query has already been parsed ; thus it is already syntactically * correct. Only the consistency with the published tables, columns and all - * the defined UDFs must be checked. + * the defined UDFs will be checked. * </i></p> * * @param query The query to check. @@ -439,12 +282,9 @@ public class DBChecker implements QueryChecker { * </ol> * * @param query The query to check. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. Each - * item of this stack is a list of columns available in - * each father-level query. <i><b>Note:</b> this - * parameter is NULL if this function is called with - * the root/father query as parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * * @throws UnresolvedIdentifiersException An {@link UnresolvedIdentifiersException} * if one or several of the above @@ -459,11 +299,19 @@ public class DBChecker implements QueryChecker { * @see #checkUDFs(ADQLQuery, UnresolvedIdentifiersException) * @see #checkTypes(ADQLQuery, UnresolvedIdentifiersException) */ - protected void check(final ADQLQuery query, final Stack<SearchColumnList> fathersList) throws UnresolvedIdentifiersException { + protected void check(final ADQLQuery query, Stack<CheckContext> contextList) throws UnresolvedIdentifiersException { UnresolvedIdentifiersException errors = new UnresolvedIdentifiersException(); + // Initialize the context: + if (contextList == null) + contextList = new Stack<CheckContext>(); + if (contextList.isEmpty()) + contextList.push(new CheckContext(null, null)); + else + contextList.push(contextList.peek().getCopy()); + // A. Check DB items (tables and columns): - SearchColumnList availableColumns = checkDBItems(query, fathersList, errors); + checkDBItems(query, contextList, errors); // B. Check UDFs: if (allowedUdfs != null) @@ -473,44 +321,50 @@ public class DBChecker implements QueryChecker { checkTypes(query, errors); // D. Check sub-queries: - checkSubQueries(query, fathersList, availableColumns, errors); + checkSubQueries(query, contextList, errors); // Throw all errors, if any: if (errors.getNbErrors() > 0) throw errors; + + // Remove the current context: + contextList.pop(); } - /* ************************************************ */ - /* CHECKING METHODS FOR DB ITEMS (TABLES & COLUMNS) */ - /* ************************************************ */ + /* ********************************************************************** + * CHECKING METHODS FOR DB ITEMS (TABLES & COLUMNS) * + ********************************************************************** */ /** - * <p>Check DB items (tables and columns) used in the given ADQL query.</p> + * Check DB items (tables and columns) used in the given ADQL query. * * <p>Operations done in this function:</p> * <ol> * <li>Resolve all found tables</li> - * <li>Get the whole list of all available columns <i>Note: this list is returned by this function.</i></li> + * <li>Get the whole list of all available columns. <i>(<b>note:</b> this list is + * returned by this function)</i></li> * <li>Resolve all found columns</li> * </ol> * - * @param query Query in which the existence of DB items must be checked. - * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. - * Each item of this stack is a list of columns available in each father-level query. - * <i>Note: this parameter is NULL if this function is called with the root/father query as parameter.</i> - * @param errors List of errors to complete in this function each time an unknown table or column is encountered. + * @param query Query in which the existence of DB items must be + * checked. + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. + * @param errors List of errors to complete in this function each + * time an unknown table or column is encountered. * * @return List of all columns available in the given query. * * @see #resolveTables(ADQLQuery, Stack, UnresolvedIdentifiersException) * @see FromContent#getDBColumns() - * @see #resolveColumns(ADQLQuery, Stack, Map, SearchColumnList, UnresolvedIdentifiersException) + * @see #resolveColumns(ADQLQuery, Stack, UnresolvedIdentifiersException) * * @since 1.3 */ - protected SearchColumnList checkDBItems(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors) { + protected SearchColumnList checkDBItems(final ADQLQuery query, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { // a. Resolve all tables: - Map<DBTable, ADQLTable> mapTables = resolveTables(query, fathersList, errors); + resolveTables(query, contextList, errors); // b. Get the list of all columns made available in the clause FROM: SearchColumnList availableColumns; @@ -520,9 +374,10 @@ public class DBChecker implements QueryChecker { errors.addException(pe); availableColumns = new SearchColumnList(); } + contextList.peek().availableColumns.addAll(availableColumns); // c. Resolve all columns: - resolveColumns(query, fathersList, mapTables, availableColumns, errors); + resolveColumns(query, contextList, errors); return availableColumns; } @@ -532,16 +387,24 @@ public class DBChecker implements QueryChecker { * the available tables, and if there is only one match, attach the matching * metadata to them. * - * <b>Management of sub-query tables</b> + * <h3>Management of Common Table Expressions (CTEs ; WITH expressions)</h3> + * <p> + * If the clause WITH is not empty, any declared CTE/sub-query will be + * checked. If correct, a {@link DBTable} will be generated using + * {@link #generateDBTable(WithItem)} representing this sub-query. This + * {@link DBTable} is immediately added to the current context so that + * being referenced in the main query. + * </p> + * + * <h3>Management of sub-query tables</h3> * <p> * If a table is not a DB table reference but a sub-query, this latter is - * first checked (using {@link #check(ADQLQuery, Stack)} ; but the father - * list must not contain tables of the given query, because on the same - * level) and then corresponding table metadata are generated (using + * first checked, using {@link #check(ADQLQuery, Stack)}. Then, its + * corresponding table metadata are generated (using * {@link #generateDBTable(ADQLQuery, String)}) and attached to it. * </p> * - * <b>Management of "{table}.*" in the SELECT clause</b> + * <h3>Management of "{table}.*" in the SELECT clause</h3> * <p> * For each of this SELECT item, this function tries to resolve the table * name. If only one match is found, the corresponding ADQL table object @@ -550,7 +413,7 @@ public class DBChecker implements QueryChecker { * the referenced table is a sub-query). * </p> * - * <b>Table alias</b> + * <h3>Table alias</h3> * <p> * When a simple table (i.e. not a sub-query) is aliased, the metadata of * this table will be wrapped inside a {@link DBTableAlias} in order to @@ -567,22 +430,52 @@ public class DBChecker implements QueryChecker { * * @param query Query in which the existence of tables must be * checked. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i>Note: this parameter is NULL if this function is - * called with the root/father query as parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * @param errors List of errors to complete in this function each * time an unknown table or column is encountered. - * - * @return An associative map of all the resolved tables. */ - protected Map<DBTable, ADQLTable> resolveTables(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors) { - HashMap<DBTable, ADQLTable> mapTables = new HashMap<DBTable, ADQLTable>(); + protected void resolveTables(final ADQLQuery query, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { + final CheckContext context = contextList.peek(); ISearchHandler sHandler; - // Check the existence of all tables: + // Resolve tables/queries declared in the WITH clause: + ADQLTable[] declaredCTEs = new ADQLTable[query.getWith().size()]; + int i = 0; + for(WithItem withItem : query.getWith()) { + + // Check this query (and set all the metadata on all DB items) + try { + check(withItem.getQuery(), contextList); + } catch(UnresolvedIdentifiersException uie) { + for(ParseException pe : uie) + errors.addException(pe); + } + + // Generate the corresponding DBTable: + withItem.setDBLink(generateDBTable(withItem)); + + // Build a corresponding virtual ADQLTable: + ADQLTable adqlTable = new ADQLTable(null, withItem.getLabel()); + adqlTable.setCaseSensitive(IdentifierField.TABLE, withItem.isLabelCaseSensitive()); + adqlTable.setDBLink(withItem.getDBLink()); + declaredCTEs[i++] = adqlTable; + + // Update the context: + context.cteTables.add(adqlTable.getDBLink()); + + // Check the number of columns: + if (withItem.getColumnLabels() != null) { + DBColumn[] columns = withItem.getResultingColumns(); + if (withItem.getColumnLabels().size() > columns.length) + errors.addException(new ParseException("The WITH query \"" + withItem.getLabel() + "\" specifies MORE columns (" + withItem.getColumnLabels().size() + ") than available (" + columns.length + ")!", withItem.getPosition())); + else if (withItem.getColumnLabels().size() < columns.length) + errors.addException(new ParseException("The WITH query \"" + withItem.getLabel() + "\" specifies LESS columns (" + withItem.getColumnLabels().size() + ") than available (" + columns.length + ")!", withItem.getPosition())); + } + } + + // Check the existence of all tables in the FROM clause: sHandler = new SearchTableHandler(); sHandler.search(query.getFrom()); for(ADQLObject result : sHandler) { @@ -593,19 +486,25 @@ public class DBChecker implements QueryChecker { DBTable dbTable = null; if (table.isSubQuery()) { // check the sub-query tables: - check(table.getSubQuery(), fathersList); + check(table.getSubQuery(), contextList); // generate its DBTable: - dbTable = generateDBTable(table.getSubQuery(), table.getAlias()); + dbTable = generateDBTable(table.getSubQuery(), (table.isCaseSensitive(IdentifierField.ALIAS) ? "\"" + table.getAlias() + "\"" : table.getAlias())); } else { - dbTable = resolveTable(table); + // search among DB tables: + if (dbTable == null) + dbTable = resolveTable(table, contextList); // wrap this table metadata if an alias should be used: - if (dbTable != null && table.hasAlias()) - dbTable = new DBTableAlias(dbTable, (table.isCaseSensitive(IdentifierField.ALIAS) ? table.getAlias() : table.getAlias().toLowerCase())); + if (dbTable != null && table.hasAlias()) { + dbTable = new DBTableAlias(dbTable, (table.isCaseSensitive(IdentifierField.ALIAS) ? "\"" + table.getAlias() + "\"" : table.getAlias().toLowerCase())); + } } // link with the matched DBTable: table.setDBLink(dbTable); - mapTables.put(dbTable, table); + if (table.isSubQuery() || table.hasAlias()) { + if (!context.cteTables.add(dbTable)) + errors.addException(new ParseException("Table name already used: \"" + dbTable.getADQLName() + "\". Please, choose a different alias for this table.")); + } } catch(ParseException pe) { errors.addException(pe); } @@ -624,39 +523,39 @@ public class DBChecker implements QueryChecker { ADQLTable table = wildcard.getAdqlTable(); DBTable dbTable = null; - // first, try to resolve the table by table alias: - if (table.getTableName() != null && table.getSchemaName() == null) { - List<ADQLTable> tables = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE)); - if (tables.size() == 1) - dbTable = tables.get(0).getDBLink(); - } - - // then try to resolve the table reference by table name: - if (dbTable == null) - dbTable = resolveTable(table); + // resolve the table reference: + dbTable = resolveTable(table, contextList); // set the corresponding tables among the list of resolved tables: - wildcard.setAdqlTable(mapTables.get(dbTable)); + //wildcard.setAdqlTable(mapTables.get(dbTable)); + wildcard.getAdqlTable().setDBLink(dbTable); } catch(ParseException pe) { errors.addException(pe); } } - - return mapTables; } /** - * Resolve the given table, that's to say search for the corresponding {@link DBTable}. + * Resolve the given table, that's to say search for the corresponding + * {@link DBTable}. * - * @param table The table to resolve. + * @param table The table to resolve. + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * - * @return The corresponding {@link DBTable} if found, <i>null</i> otherwise. + * @return The corresponding {@link DBTable} if found. * - * @throws ParseException An {@link UnresolvedTableException} if the given table can't be resolved. + * @throws ParseException An {@link UnresolvedTableException} if the given + * table can't be resolved. */ - protected DBTable resolveTable(final ADQLTable table) throws ParseException { + protected DBTable resolveTable(final ADQLTable table, final Stack<CheckContext> contextList) throws ParseException { + // search among fix tables: List<DBTable> tables = lstTables.search(table); + // complete the search with CTEs: + tables.addAll(contextList.peek().cteTables.search(table)); + // good if only one table has been found: if (tables.size() == 1) return tables.get(0); @@ -673,7 +572,7 @@ public class DBChecker implements QueryChecker { * to the given tables' metadata, and if there is only one match, attach the * matching metadata to them. * - * <h4>Management of selected columns' references</h4> + * <h3>Management of selected columns' references</h3> * <p> * A column reference is not only a direct reference to a table column * using a column name. It can also be a reference to an item of the SELECT @@ -689,20 +588,53 @@ public class DBChecker implements QueryChecker { * * @param query Query in which the existence of tables must be * checked. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i><b>Note:</b> this parameter is NULL if this - * function is called with the root/father query as - * parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * @param mapTables List of all resolved tables. * @param list List of column metadata to complete in this function * each time a column reference is resolved. * @param errors List of errors to complete in this function each * time an unknown table or column is encountered. + * + * @deprecated Since v2.0, the parameter 'mapTables' is no more used. + * You should used {@link #resolveColumns(ADQLQuery, Stack, UnresolvedIdentifiersException)} instead. + */ + @Deprecated + protected final void resolveColumns(final ADQLQuery query, final Stack<CheckContext> contextList, final Map<DBTable, ADQLTable> mapTables, final UnresolvedIdentifiersException errors) { + resolveColumns(query, contextList, errors); + } + + /** + * Search all column references inside the given query, resolve them thanks + * to the given tables' metadata, and if there is only one match, attach the + * matching metadata to them. + * + * <h3>Management of selected columns' references</h3> + * <p> + * A column reference is not only a direct reference to a table column + * using a column name. It can also be a reference to an item of the SELECT + * clause (which will then call a "selected column"). That kind of + * reference can be either an index (an unsigned integer starting from + * 1 to N, where N is the number selected columns), or the name/alias of + * the column. + * </p> + * <p> + * These references are also checked, in second steps, in this function. + * Column metadata are also attached to them, as common columns. + * </p> + * + * @param query Query in which the existence of columns must be + * checked. + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. + * @param errors List of errors to complete in this function each + * time an unknown table or column is encountered. + * + * @since 2.0 */ - protected void resolveColumns(final ADQLQuery query, final Stack<SearchColumnList> fathersList, final Map<DBTable, ADQLTable> mapTables, final SearchColumnList list, final UnresolvedIdentifiersException errors) { + protected void resolveColumns(final ADQLQuery query, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { ISearchHandler sHandler; // Check the existence of all columns: @@ -710,7 +642,7 @@ public class DBChecker implements QueryChecker { sHandler.search(query); for(ADQLObject result : sHandler) { try { - resolveColumn((ADQLColumn)result, list, mapTables, fathersList); + resolveColumn((ADQLColumn)result, contextList); } catch(ParseException pe) { errors.addException(pe); } @@ -718,10 +650,10 @@ public class DBChecker implements QueryChecker { // Check the GROUP BY items: ClauseSelect select = query.getSelect(); - checkGroupBy(query.getGroupBy(), select, list, mapTables, fathersList, errors); + checkGroupBy(query.getGroupBy(), select, contextList, errors); // Check the ORDER BY items: - checkOrderBy(query.getOrderBy(), select, list, mapTables, fathersList, errors); + checkOrderBy(query.getOrderBy(), select, contextList, errors); /* Check the correctness of all column references (= references to * selected columns): @@ -734,11 +666,9 @@ public class DBChecker implements QueryChecker { try { ColumnReference colRef = (ColumnReference)result; // resolve the column reference: - DBColumn dbColumn = checkColumnReference(colRef, select, list); + DBColumn dbColumn = checkColumnReference(colRef, select, contextList); // link with the matched DBColumn: colRef.setDBLink(dbColumn); - if (dbColumn != null) - colRef.setAdqlTable(mapTables.get(dbColumn.getTable())); } catch(ParseException pe) { errors.addException(pe); } @@ -746,26 +676,13 @@ public class DBChecker implements QueryChecker { } /** - * Resolve the given column (i.e. search for the corresponding - * {@link DBColumn}) and update the given {@link ADQLColumn} with these - * found DB metadata. - * - * <p> - * The fourth parameter is used only if this function is called inside a - * sub-query. In this case, the column is tried to be resolved with the - * first list (dbColumns). If no match is found, the resolution is tried - * with the father columns list (fathersList). - * </p> + * Resolve the given column, that's to say search for the corresponding + * {@link DBColumn}. * * @param column The column to resolve. - * @param dbColumns List of all available {@link DBColumn}s. - * @param mapTables List of all resolved tables. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i>Note: this parameter is NULL if this function is - * called with the root/father query as parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * * @return The corresponding {@link DBColumn} if found. * Otherwise an exception is thrown. @@ -774,72 +691,32 @@ public class DBChecker implements QueryChecker { * given column can't be resolved * or an {@link UnresolvedTableException} if its * table reference can't be resolved. - * - * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack) - * - * @since 2.0 */ - protected final DBColumn resolveColumn(final ADQLColumn column, final SearchColumnList dbColumns, final Map<DBTable, ADQLTable> mapTables, Stack<SearchColumnList> fathersList) throws ParseException { - // Resolve the column: - DBColumn dbColumn = resolveColumn(column, dbColumns, fathersList); - - // Link the given ADQLColumn with the matched DBColumn: - column.setDBLink(dbColumn); - if (mapTables != null) - column.setAdqlTable(mapTables.get(dbColumn.getTable())); - - return dbColumn; - } - - /** - * Resolve the given column, that's to say search for the corresponding - * {@link DBColumn}. - * - * <p> - * The third parameter is used only if this function is called inside a - * sub-query. In this case, the column is tried to be resolved with the - * first list (dbColumns). If no match is found, the resolution is tried - * with the father columns list (fathersList). - * </p> - * - * @param column The column to resolve. - * @param dbColumns List of all available {@link DBColumn}s. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i>Note: this parameter is NULL if this function is - * called with the root/father query as parameter.</i> - * - * @return The corresponding {@link DBColumn} if found. - * Otherwise an exception is thrown. - * - * @throws ParseException An {@link UnresolvedColumnException} if the - * given column can't be resolved - * or an {@link UnresolvedTableException} if its - * table reference can't be resolved. - */ - protected DBColumn resolveColumn(final ADQLColumn column, final SearchColumnList dbColumns, Stack<SearchColumnList> fathersList) throws ParseException { - List<DBColumn> foundColumns = dbColumns.search(column); + protected DBColumn resolveColumn(final ADQLColumn column, final Stack<CheckContext> contextList) throws ParseException { + List<DBColumn> foundColumns = contextList.peek().availableColumns.search(column); // good if only one column has been found: - if (foundColumns.size() == 1) + if (foundColumns.size() == 1) { + column.setDBLink(foundColumns.get(0)); return foundColumns.get(0); + } // but if more than one: ambiguous table reference ! else if (foundColumns.size() > 1) { if (column.getTableName() == null) throw new UnresolvedColumnException(column, (foundColumns.get(0).getTable() == null) ? "<NULL>" : (foundColumns.get(0).getTable().getADQLName() + "." + foundColumns.get(0).getADQLName()), (foundColumns.get(1).getTable() == null) ? "<NULL>" : (foundColumns.get(1).getTable().getADQLName() + "." + foundColumns.get(1).getADQLName())); else throw new UnresolvedTableException(column, (foundColumns.get(0).getTable() == null) ? "<NULL>" : foundColumns.get(0).getTable().getADQLName(), (foundColumns.get(1).getTable() == null) ? "<NULL>" : foundColumns.get(1).getTable().getADQLName()); - }// otherwise (no match): unknown column ! + }// otherwise (i.e. no direct match)... else { - if (fathersList == null || fathersList.isEmpty()) - throw new UnresolvedColumnException(column); - else { - Stack<SearchColumnList> subStack = new Stack<SearchColumnList>(); - subStack.addAll(fathersList.subList(0, fathersList.size() - 1)); - return resolveColumn(column, fathersList.peek(), subStack); + // ...try searching among columns of the parent queries: + if (contextList.size() > 1) { + Stack<CheckContext> subStack = new Stack<CheckContext>(); + subStack.addAll(contextList.subList(0, contextList.size() - 1)); + return resolveColumn(column, subStack); } + // ...else, unknown column! + else + throw new UnresolvedColumnException(column); } } @@ -849,20 +726,15 @@ public class DBChecker implements QueryChecker { * * @param groupBy The GROUP BY to check. * @param select The SELECT clause (and all its selected items). - * @param list List of all available {@link DBColumn}s. - * @param mapTables List of all resolved tables. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i>Note: this parameter is NULL if this function is - * called with the root/father query as parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * @param errors List of errors to complete in this function each * time an unknown table or column is encountered. * * @since 2.0 */ - protected void checkGroupBy(final ClauseADQL<ADQLOperand> groupBy, final ClauseSelect select, final SearchColumnList list, final Map<DBTable, ADQLTable> mapTables, Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors) { + protected void checkGroupBy(final ClauseADQL<ADQLOperand> groupBy, final ClauseSelect select, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { for(ADQLOperand obj : groupBy) { try { if (obj instanceof ADQLColumn) { @@ -870,15 +742,15 @@ public class DBChecker implements QueryChecker { /* resolve the column either as a selected column reference * or as a normal column: */ if (adqlColumn.getTableName() == null) - resolveColumnNameReference(adqlColumn, select, list, mapTables); + resolveColumnNameReference(adqlColumn, select, contextList); else - resolveColumn(adqlColumn, list, fathersList); + resolveColumn(adqlColumn, contextList); } else { ISearchHandler sHandler = new SearchColumnHandler(); sHandler.search(obj); for(ADQLObject result : sHandler) { try { - resolveColumn((ADQLColumn)result, list, mapTables, fathersList); + resolveColumn((ADQLColumn)result, contextList); } catch(ParseException pe) { errors.addException(pe); } @@ -896,20 +768,15 @@ public class DBChecker implements QueryChecker { * * @param orderBy The ORDER BY to check. * @param select The SELECT clause (and all its selected items). - * @param list List of all available {@link DBColumn}s. - * @param mapTables List of all resolved tables. - * @param fathersList List of all columns available in the father queries - * and that should be accessed in sub-queries. - * Each item of this stack is a list of columns - * available in each father-level query. - * <i>Note: this parameter is NULL if this function is - * called with the root/father query as parameter.</i> + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * @param errors List of errors to complete in this function each * time an unknown table or column is encountered. * * @since 2.0 */ - protected void checkOrderBy(final ClauseADQL<ADQLOrder> orderBy, final ClauseSelect select, final SearchColumnList list, final Map<DBTable, ADQLTable> mapTables, Stack<SearchColumnList> fathersList, final UnresolvedIdentifiersException errors) { + protected void checkOrderBy(final ClauseADQL<ADQLOrder> orderBy, final ClauseSelect select, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { for(ADQLObject obj : orderBy) { try { ADQLOrder order = (ADQLOrder)obj; @@ -920,15 +787,15 @@ public class DBChecker implements QueryChecker { /* resolve the column either as a selected column reference * or as a normal column: */ if (adqlColumn.getTableName() == null) - resolveColumnNameReference(adqlColumn, select, list, mapTables); + resolveColumnNameReference(adqlColumn, select, contextList); else - resolveColumn(adqlColumn, list, fathersList); + resolveColumn(adqlColumn, contextList); } else { ISearchHandler sHandler = new SearchColumnHandler(); sHandler.search(expr); for(ADQLObject result : sHandler) { try { - resolveColumn((ADQLColumn)result, list, mapTables, fathersList); + resolveColumn((ADQLColumn)result, contextList); } catch(ParseException pe) { errors.addException(pe); } @@ -947,41 +814,13 @@ public class DBChecker implements QueryChecker { * * @param col The column to check. * @param select The SELECT clause of the ADQL query. - * @param dbColumns The list of all available columns. - * - * @return The corresponding {@link DBColumn} if this column corresponds to - * an existing column, - * <i>NULL</i> otherwise. - * - * @throws ParseException An {@link UnresolvedColumnException} if the - * given column can't be resolved - * or an {@link UnresolvedTableException} if its - * table reference can't be resolved. - * - * @see ClauseSelect#searchByAlias(String) - * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack) - * - * @since 1.4 - * - * @deprecated Since 2.0, this function has been renamed into - * {@link #resolveColumnNameReference(ADQLColumn, ClauseSelect, SearchColumnList, Map)}. - */ - @Deprecated - protected final DBColumn checkGroupByItem(final ADQLColumn col, final ClauseSelect select, final SearchColumnList dbColumns) throws ParseException { - return resolveColumnNameReference(col, select, dbColumns, null); - } - - /** - * Check whether the given column corresponds to a selected item's alias or - * to an existing column. - * - * @param col The column to check. - * @param select The SELECT clause of the ADQL query. - * @param dbColumns The list of all available columns. + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * * @return The corresponding {@link DBColumn} if this column corresponds to * an existing column, - * <i>NULL</i> otherwise. + * NULL otherwise. * * @throws ParseException An {@link UnresolvedColumnException} if the * given column can't be resolved @@ -989,11 +828,11 @@ public class DBChecker implements QueryChecker { * table reference can't be resolved. * * @see ClauseSelect#searchByAlias(String) - * @see #resolveColumn(ADQLColumn, SearchColumnList, Map, Stack) + * @see #resolveColumn(ADQLColumn, Stack) * * @since 2.0 */ - protected DBColumn resolveColumnNameReference(final ADQLColumn col, final ClauseSelect select, final SearchColumnList dbColumns, final Map<DBTable, ADQLTable> mapTables) throws ParseException { + protected DBColumn resolveColumnNameReference(final ADQLColumn col, final ClauseSelect select, final Stack<CheckContext> contextList) throws ParseException { /* If the column name is not qualified, it may be a SELECT-item's alias. * So, try resolving the name as an alias. * If it fails, perform the normal column resolution.*/ @@ -1004,7 +843,7 @@ public class DBChecker implements QueryChecker { else if (founds.size() > 1) throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias()); } - return resolveColumn(col, dbColumns, mapTables, null); + return resolveColumn(col, contextList); } /** @@ -1013,7 +852,9 @@ public class DBChecker implements QueryChecker { * * @param colRef The column reference which must be checked. * @param select The SELECT clause of the ADQL query. - * @param dbColumns The list of all available columns. + * @param contextList Each item of this stack represents a recursion level + * inside the main ADQL query. A such item contains the + * list of columns and tables available at this level. * * @return The corresponding {@link DBColumn} if this reference is * actually referencing a selected column, @@ -1024,7 +865,7 @@ public class DBChecker implements QueryChecker { * or an {@link UnresolvedTableException} if its * table reference can't be resolved. */ - protected DBColumn checkColumnReference(final ColumnReference colRef, final ClauseSelect select, final SearchColumnList dbColumns) throws ParseException { + protected DBColumn checkColumnReference(final ColumnReference colRef, final ClauseSelect select, final Stack<CheckContext> contextList) throws ParseException { int index = colRef.getColumnIndex(); if (index > 0 && index <= select.size()) { SelectItem item = select.get(index - 1); @@ -1037,46 +878,88 @@ public class DBChecker implements QueryChecker { } /** - * Generate a {@link DBTable} corresponding to the given sub-query with the given table name. - * This {@link DBTable} will contain all {@link DBColumn} returned by {@link ADQLQuery#getResultingColumns()}. + * Generate a {@link DBTable} corresponding to the given sub-query with the + * given table name. This {@link DBTable} will contain all {@link DBColumn} + * returned by {@link ADQLQuery#getResultingColumns()}. * * @param subQuery Sub-query in which the specified table must be searched. * @param tableName Name of the table to search. + * <i>If between double quotes, the table name will be + * considered as case sensitive.</i> * - * @return The corresponding {@link DBTable} if the table has been found in the given sub-query, <i>null</i> otherwise. + * @return The corresponding {@link DBTable} if the table has been found in + * the given sub-query, + * or NULL otherwise. * - * @throws ParseException Can be used to explain why the table has not been found. <i>Note: not used by default.</i> + * @throws ParseException Can be used to explain why the table has not + * been found. <i>Not used by default.</i> */ public static DBTable generateDBTable(final ADQLQuery subQuery, final String tableName) throws ParseException { - DefaultDBTable dbTable = new DefaultDBTable(tableName); + // Create default DB meta: + DefaultDBTable dbTable = new DefaultDBTable((DefaultDBTable.isDelimited(tableName) ? tableName : tableName.toLowerCase())); + // Fetch all available columns: DBColumn[] columns = subQuery.getResultingColumns(); + + // Add all available columns: + for(DBColumn dbCol : columns) + dbTable.addColumn(dbCol.copy(dbCol.getDBName(), DBIdentifier.denormalize(dbCol.getADQLName(), dbCol.isCaseSensitive()), dbTable)); + + return dbTable; + } + + /** + * Generate a {@link DBTable} corresponding to the given + * Common Table Expression (i.e. CTE = item of a WITH clause). + * + * <p> + * This {@link DBTable} will contain all {@link DBColumn}s returned by + * {@link WithItem#getResultingColumns()}. + * </p> + * + * @param withItem CTE declaration. + * + * @return The corresponding {@link DBTable}, + * or NULL otherwise. + * + * @since 2.0 + */ + public static DBTable generateDBTable(final WithItem withItem) { + // Create default DB meta: + DefaultDBTable dbTable = new DefaultDBTable((withItem.isLabelCaseSensitive() ? withItem.getLabel() : withItem.getLabel().toLowerCase())); + dbTable.setCaseSensitive(withItem.isLabelCaseSensitive()); + + // Fetch all available columns: + DBColumn[] columns = withItem.getResultingColumns(); + + // Add all available columns: for(DBColumn dbCol : columns) - dbTable.addColumn(dbCol.copy(dbCol.getADQLName(), dbCol.getADQLName(), dbTable)); + dbTable.addColumn(dbCol.copy(dbCol.getDBName(), DBIdentifier.denormalize(dbCol.getADQLName(), dbCol.isCaseSensitive()), dbTable)); return dbTable; } - /* ************************* */ - /* CHECKING METHODS FOR UDFs */ - /* ************************* */ + /* ********************************************************************** + * CHECKING METHODS FOR UDFs * + ********************************************************************** */ /** - * <p>Search all UDFs (User Defined Functions) inside the given query, and then - * check their signature against the list of allowed UDFs.</p> + * Search all UDFs (User Defined Functions) inside the given query, and then + * check their signature against the list of allowed UDFs. * - * <p><i>Note: - * When more than one allowed function match, the function is considered as correct - * and no error is added. - * However, in case of multiple matches, the return type of matching functions could - * be different and in this case, there would be an error while checking later - * the types. In such case, throwing an error could make sense, but the user would - * then need to cast some parameters to help the parser identifying the right function. + * <p><i><b>Note:</b> + * When more than one allowed function match, the function is considered as + * correct and no error is added. However, in case of multiple matches, the + * return type of matching functions could be different and in this case, + * there would be an error while checking later the types. In such case, + * throwing an error could make sense, but the user would then need to cast + * some parameters to help the parser identifying the right function. * But the type-casting ability is not yet possible in ADQL. * </i></p> * * @param query Query in which UDFs must be checked. - * @param errors List of errors to complete in this function each time a UDF does not match to any of the allowed UDFs. + * @param errors List of errors to complete in this function each time a + * UDF does not match to any of the allowed UDFs. * * @since 1.3 */ @@ -1145,19 +1028,23 @@ public class DBChecker implements QueryChecker { } /** - * <p>Tell whether the type of all parameters of the given ADQL function - * is resolved.</p> + * Tell whether the type of all parameters of the given ADQL function + * is resolved. * * <p>A parameter type may not be resolved for 2 main reasons:</p> * <ul> - * <li>the parameter is a <b>column</b>, but this column has not been successfully resolved. Thus its type is still unknown.</li> - * <li>the parameter is a <b>UDF</b>, but this UDF has not been already resolved. Thus, as for the column, its return type is still unknown. - * But it could be known later if the UDF is resolved later ; a second try should be done afterwards.</li> + * <li>the parameter is a <b>column</b>, but this column has not been + * successfully resolved. Thus its type is still unknown.</li> + * <li>the parameter is a <b>UDF</b>, but this UDF has not been already + * resolved. Thus, as for the column, its return type is still unknown. + * But it could be known later if the UDF is resolved later ; a second + * try should be done afterwards.</li> * </ul> * * @param fct ADQL function whose the parameters' type should be checked. * - * @return <i>true</i> if the type of all parameters is known, <i>false</i> otherwise. + * @return <code>true</code> if the type of all parameters is known, + * <code>false</code> otherwise. * * @since 1.3 */ @@ -1169,583 +1056,843 @@ public class DBChecker implements QueryChecker { return true; } - /* ************************************************************************************************* */ - /* METHODS CHECKING THE GEOMETRIES (geometrical functions, coordinate systems and STC-S expressions) */ - /* ************************************************************************************************* */ + /* ********************************************************************** + * METHODS CHECKING TYPES UNKNOWN WHILE CHECKING SYNTAX * + ********************************************************************** */ /** - * <p>Check all geometries.</p> + * Search all operands whose the type is not yet known and try to resolve it + * now and to check whether it matches the type expected by the syntactic + * parser. * - * <p>Operations done in this function:</p> - * <ol> - * <li>Check that all explicit (string constant) coordinate system definitions are supported</i></li> - * <li>Check all STC-S expressions (only in {@link RegionFunction} for the moment) and - * Apply the 2 previous checks on them</li> - * </ol> + * <p> + * Only two operands may have an unresolved type: columns and user defined + * functions. Indeed, their type can be resolved only if the list of + * available columns and UDFs is known, and if columns and UDFs used in the + * query are resolved successfully. + * </p> * - * <p><i><b>IMPORTANT note:</b> - * Since v2.0, the check of supported geometrical functions is performed - * directly in ADQLParser through the notion of Optional Features. - * The declaration of supported geometrical functions must now be done - * with {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} - * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). + * <p> + * When an operand type is still unknown, they will own the three kinds of + * type and so this function won't raise an error: it is thus automatically + * on the expected type. This behavior is perfectly correct because if the + * type is not resolved that means the item/operand has not been resolved + * in the previous steps and so that an error about this item has already + * been raised. + * </p> + * + * <p><i><b>Important note:</b> + * This function does not check the types exactly, but just roughly by + * considering only three categories: string, numeric and geometry. * </i></p> * - * @param query Query in which geometries must be checked. - * @param errors List of errors to complete in this function each time a geometry item is not supported. + * @param query Query in which unknown types must be resolved and + * checked. + * @param errors List of errors to complete in this function each time a + * types does not match to the expected one. * - * @see #resolveCoordinateSystems(ADQLQuery, UnresolvedIdentifiersException) - * @see #resolveSTCSExpressions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) + * @see UnknownType * * @since 1.3 - * - * @deprecated Since 2.0, validation of the geometric functions is - * performed automatically by - * {@link adql.parser.ADQLParser ADQLParser}. Geometric - * functions are optional features and should be declared as - * such in the {@link adql.parser.ADQLParser ADQLParser} if - * they are supported (see - * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). */ - @Deprecated - protected void checkGeometries(final ADQLQuery query, final UnresolvedIdentifiersException errors) { - BinarySearch<String, String> binSearch = new BinarySearch<String, String>() { - @Override - protected int compare(String searchItem, String arrayItem) { - return searchItem.compareToIgnoreCase(arrayItem); - } - }; - - // a. Check whether the coordinate systems are allowed: - if (allowedCoordSys != null) - resolveCoordinateSystems(query, errors); + protected void checkTypes(final ADQLQuery query, final UnresolvedIdentifiersException errors) { + // Search all unknown types: + ISearchHandler sHandler = new SearchUnknownTypeHandler(); + sHandler.search(query); - // b. Check all STC-S expressions (in RegionFunctions only) + the used coordinate systems (if StringConstant only): - if (allowedGeo == null || (allowedGeo.length > 0 && binSearch.search("REGION", allowedGeo) >= 0)) - resolveSTCSExpressions(query, binSearch, errors); + // Check whether their type matches the expected one: + UnknownType unknown; + for(ADQLObject result : sHandler) { + unknown = (UnknownType)result; + switch(unknown.getExpectedType()) { + case 'G': + case 'g': + if (!unknown.isGeometry()) + errors.addException(new ParseException("Type mismatch! A geometry was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); + break; + case 'N': + case 'n': + if (!unknown.isNumeric()) + errors.addException(new ParseException("Type mismatch! A numeric value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); + break; + case 'S': + case 's': + if (!unknown.isString()) + errors.addException(new ParseException("Type mismatch! A string value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); + break; + } + } } + /* ********************************************************************** + * METHODS CHECKING THE SUB-QUERIES * + ********************************************************************** */ + /** - * Search for all geometrical functions and check whether they are allowed. + * Search for all sub-queries found in the given query but not in the clause + * FROM. These sub-queries are then checked using + * {@link #check(ADQLQuery, Stack)}. * - * @param query Query in which geometrical functions must be checked. - * @param errors List of errors to complete in this function each time a geometrical function is not supported. * - * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) + * @param query Query in which sub-queries must be checked. + * @param contextList Each item of this stack represents a recursion + * level inside the main ADQL query. A such item + * contains the list of columns and tables + * available at this level. + * @param availableColumns List of all columns resolved in the given query. + * @param errors List of errors to complete in this function each + * time a semantic error is encountered. * * @since 1.3 - * - * @deprecated Since 2.0, validation of the geometric functions is - * performed automatically by - * {@link adql.parser.ADQLParser ADQLParser}. Geometric - * functions are optional features and should be declared as - * such in the {@link adql.parser.ADQLParser ADQLParser} if - * they are supported (see - * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). */ - @Deprecated - protected final void resolveGeometryFunctions(final ADQLQuery query, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { - ISearchHandler sHandler = new SearchGeometryHandler(); + protected void checkSubQueries(final ADQLQuery query, final Stack<CheckContext> contextList, final UnresolvedIdentifiersException errors) { + // Check sub-queries outside the clause FROM: + ISearchHandler sHandler = new SearchSubQueryHandler(); sHandler.search(query); + if (sHandler.getNbMatch() > 0) { - String fctName; - for(ADQLObject result : sHandler) { - fctName = result.getName(); - checkGeometryFunction(fctName, (ADQLFunction)result, binSearch, errors); + // Check each found sub-query: + for(ADQLObject result : sHandler) { + try { + check((ADQLQuery)result, contextList); + } catch(UnresolvedIdentifiersException uie) { + Iterator<ParseException> itPe = uie.getErrors(); + while(itPe.hasNext()) + errors.addException(itPe.next()); + } + } } } + /* ********************************************************************** + * SEARCH HANDLERS * + ********************************************************************** */ + /** - * <p>Check whether the specified geometrical function is allowed by this implementation.</p> - * - * <p><i>Note: - * If the list of allowed geometrical functions is empty, this function will always add an errors to the given list. - * Indeed, it means that no geometrical function is allowed and so that the specified function is automatically not supported. - * </i></p> + * Lets searching all {@link ADQLColumn} in the given object, + * EXCEPT in the GROUP BY and ORDER BY clauses. * - * @param fctName Name of the geometrical function to test. - * @param fct The function instance being or containing the geometrical function to check. <i>Note: this function can be the function to test or a function embedding the function under test (i.e. RegionFunction). - * @param binSearch The object to use in order to search a function name inside the list of allowed functions. - * It is able to perform a binary search inside a sorted array of String objects. The interest of - * this object is its compare function which must be overridden and tells how to compare the item - * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). - * @param errors List of errors to complete in this function each time a geometrical function is not supported. + * <p> + * {@link ADQLColumn}s of the GROUP BY and ORDER BY may be aliases and so, + * they can not be checked exactly as a normal column. + * </p> * - * @since 1.3 + * <p> + * {@link ADQLColumn} of a {@link ColumnReference} may be an alias, they + * can not be checked exactly as a normal column. + * </p> * - * @deprecated Since 2.0, validation of the geometric functions is - * performed automatically by - * {@link adql.parser.ADQLParser ADQLParser}. Geometric - * functions are optional features and should be declared as - * such in the {@link adql.parser.ADQLParser ADQLParser} if - * they are supported (see - * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). + * @author Grégory Mantelet (ARI;CDS) + * @version 2.0 (08/2019) + * @since 1.4 */ - @Deprecated - protected final void checkGeometryFunction(final String fctName, final ADQLFunction fct, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { - int match = -1; - if (allowedGeo.length != 0) - match = binSearch.search(fctName, allowedGeo); - if (match < 0) - errors.addException(new UnresolvedFunctionException("The geometrical function \"" + fctName + "\" is not available in this implementation!", fct)); + private static class SearchColumnOutsideGroupByHandler extends SearchColumnHandler { + @Override + protected boolean goInto(final ADQLObject obj) { + if (obj instanceof ClauseADQL<?> && ((ClauseADQL<?>)obj).getName() != null) { + ClauseADQL<?> clause = (ClauseADQL<?>)obj; + return !(clause.getName().equalsIgnoreCase("GROUP BY") || clause.getName().equalsIgnoreCase("ORDER BY")); + } else + return super.goInto(obj); + } } /** - * <p>Search all explicit coordinate system declarations, check their syntax and whether they are allowed by this implementation.</p> - * - * <p><i>Note: - * "explicit" means here that all {@link StringConstant} instances. Only coordinate systems expressed as string can - * be parsed and so checked. So if a coordinate system is specified by a column, no check can be done at this stage... - * it will be possible to perform such test only at the execution. - * </i></p> - * - * @param query Query in which coordinate systems must be checked. - * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. - * - * @see #checkCoordinateSystem(StringConstant, UnresolvedIdentifiersException) - * - * @since 1.3 + * Lets searching all tables. * - * @deprecated Since 2.0, the validation of coordinate systems is performed - * automatically by {@link adql.parser.ADQLParser ADQLParser}. + * @author Grégory Mantelet (CDS) + * @version 1.0 (07/2011) */ - @Deprecated - protected void resolveCoordinateSystems(final ADQLQuery query, final UnresolvedIdentifiersException errors) { - ISearchHandler sHandler = new SearchCoordSysHandler(); - sHandler.search(query); - for(ADQLObject result : sHandler) - checkCoordinateSystem((StringConstant)result, errors); + private static class SearchTableHandler extends SimpleSearchHandler { + @Override + public boolean match(final ADQLObject obj) { + return obj instanceof ADQLTable; + } } /** - * Parse and then check the coordinate system contained in the given {@link StringConstant} instance. - * - * @param adqlCoordSys The {@link StringConstant} object containing the coordinate system to check. - * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. - * - * @see STCS#parseCoordSys(String) - * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) + * Lets searching all wildcards. * - * @since 1.3 - * - * @deprecated Since 2.0, the validation of coordinate systems is performed - * automatically by {@link adql.parser.ADQLParser ADQLParser}. + * @author Grégory Mantelet (CDS) + * @version 1.0 (09/2011) */ - @Deprecated - protected void checkCoordinateSystem(final StringConstant adqlCoordSys, final UnresolvedIdentifiersException errors) { - String coordSysStr = adqlCoordSys.getValue(); - try { - checkCoordinateSystem(STCS.parseCoordSys(coordSysStr), adqlCoordSys, errors); - } catch(ParseException pe) { - errors.addException(new ParseException(pe.getMessage(), adqlCoordSys.getPosition())); + private static class SearchWildCardHandler extends SimpleSearchHandler { + @Override + public boolean match(final ADQLObject obj) { + return (obj instanceof SelectAllColumns) && (((SelectAllColumns)obj).getAdqlTable() != null); } } /** - * Check whether the given coordinate system is allowed by this implementation. - * - * @param coordSys Coordinate system to test. - * @param operand The operand representing or containing the coordinate system under test. - * @param errors List of errors to complete in this function each time a coordinate system is not supported. - * - * @since 1.3 + * Lets searching column references. * - * @deprecated Since 2.0, the validation of coordinate systems is performed - * automatically by {@link adql.parser.ADQLParser ADQLParser}. + * @author Grégory Mantelet (CDS) + * @version 1.0 (11/2011) */ - @Deprecated - protected void checkCoordinateSystem(final CoordSys coordSys, final ADQLOperand operand, final UnresolvedIdentifiersException errors) { - if (coordSysRegExp != null && coordSys != null && !coordSys.toFullSTCS().matches(coordSysRegExp)) { - StringBuffer buf = new StringBuffer(); - if (allowedCoordSys != null) { - for(String cs : allowedCoordSys) { - if (buf.length() > 0) - buf.append(", "); - buf.append(cs); - } - } - if (buf.length() == 0) - buf.append("No coordinate system is allowed!"); - else - buf.insert(0, "Allowed coordinate systems are: "); - errors.addException(new ParseException("Coordinate system \"" + ((operand instanceof StringConstant) ? ((StringConstant)operand).getValue() : coordSys.toString()) + "\" (= \"" + coordSys.toFullSTCS() + "\") not allowed in this implementation. " + buf.toString(), operand.getPosition())); + private static class SearchColReferenceHandler extends SimpleSearchHandler { + @Override + public boolean match(final ADQLObject obj) { + return (obj instanceof ColumnReference); } } /** - * <p>Search all STC-S expressions inside the given query, parse them (and so check their syntax) and then determine - * whether the declared coordinate system and the expressed region are allowed in this implementation.</p> + * Lets searching subqueries in every clause except the WITH and FROM ones + * (hence the modification of the {@link #goInto(ADQLObject)}.</p> * - * <p><i>Note: - * In the current ADQL language definition, STC-S expressions can be found only as only parameter of the REGION function. + * <p><i><b>Note:</b> + * The function {@link #addMatch(ADQLObject, ADQLIterator)} has been + * modified in order to not have the root search object (here: the main + * query) in the list of results. * </i></p> * - * @param query Query in which STC-S expressions must be checked. - * @param binSearch The object to use in order to search a region name inside the list of allowed functions/regions. - * It is able to perform a binary search inside a sorted array of String objects. The interest of - * this object is its compare function which must be overridden and tells how to compare the item - * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). - * @param errors List of errors to complete in this function each time the STC-S syntax is wrong or each time the declared coordinate system or region is not supported. - * - * @see STCS#parseRegion(String) - * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) - * - * @since 1.3 - * - * @deprecated Since 2.0, the validation of STCs expressions is performed - * automatically by {@link adql.parser.ADQLParser ADQLParser}. + * @author Grégory Mantelet (ARI;CDS) + * @version 2.0 (08/2019) + * @since 1.2 */ - @Deprecated - protected void resolveSTCSExpressions(final ADQLQuery query, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { - // Search REGION functions: - ISearchHandler sHandler = new SearchRegionHandler(); - sHandler.search(query); - - // Parse and check their STC-S expression: - String stcs; - Region region; - for(ADQLObject result : sHandler) { - try { - // get the STC-S expression: - stcs = ((StringConstant)((RegionFunction)result).getParameter(0)).getValue(); + private static class SearchSubQueryHandler extends SimpleSearchHandler { + @Override + protected void addMatch(ADQLObject matchObj, ADQLIterator it) { + if (it != null) + super.addMatch(matchObj, it); + } - // parse the STC-S expression (and so check the syntax): - region = STCS.parseRegion(stcs); + @Override + protected boolean goInto(ADQLObject obj) { + return super.goInto(obj) && !(obj instanceof FromContent) && !(obj instanceof ClauseADQL && "WITH".equals(obj.getName())); + } - // check whether the regions (this one + the possible inner ones) and the coordinate systems are allowed: - checkRegion(region, (RegionFunction)result, binSearch, errors); - } catch(ParseException pe) { - errors.addException(new ParseException(pe.getMessage(), result.getPosition())); - } + @Override + protected boolean match(ADQLObject obj) { + return (obj instanceof ADQLQuery); } } /** - * <p>Check the given region.</p> - * - * <p>The following points are checked in this function:</p> - * <ul> - * <li>whether the coordinate system is allowed</li> - * <li>whether the type of region is allowed</li> - * <li>whether the inner regions are correct (here this function is called recursively on each inner region).</li> - * </ul> + * Let searching user defined functions. * - * @param r The region to check. - * @param fct The REGION function containing the region to check. - * @param errors List of errors to complete in this function if the given region or its inner regions are not supported. + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) + * @since 1.3 + */ + private static class SearchUDFHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj) { + return (obj instanceof UserDefinedFunction); + } + } + + /** + * Let replacing every {@link DefaultUDF}s whose a {@link FunctionDef} is + * set by their corresponding {@link UserDefinedFunction} class. * - * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) - * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) - * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) + * <p><i><b>Important note:</b> + * If the replacer can not be created using the class returned by + * {@link FunctionDef#getUDFClass()}, no replacement is performed. + * </i></p> * + * @author Grégory Mantelet (ARI) + * @version 1.3 (02/2015) * @since 1.3 - * - * @deprecated Since 2.0, the validation of REGIONs is performed - * automatically by {@link adql.parser.ADQLParser ADQLParser}. */ - @Deprecated - protected void checkRegion(final Region r, final RegionFunction fct, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { - if (r == null) - return; + private static class ReplaceDefaultUDFHandler extends SimpleReplaceHandler { + private final UnresolvedIdentifiersException errors; - // Check the coordinate system (if any): - if (r.coordSys != null) - checkCoordinateSystem(r.coordSys, fct, errors); + public ReplaceDefaultUDFHandler(final UnresolvedIdentifiersException errorsContainer) { + errors = errorsContainer; + } - // Check that the region type is allowed: - if (allowedGeo != null) { - if (allowedGeo.length == 0) - errors.addException(new UnresolvedFunctionException("The region type \"" + r.type + "\" is not available in this implementation!", fct)); - else - checkGeometryFunction((r.type == RegionType.POSITION) ? "POINT" : r.type.toString(), fct, binSearch, errors); + @Override + protected boolean match(ADQLObject obj) { + return (obj.getClass().getName().equals(DefaultUDF.class.getName())) && (((DefaultUDF)obj).getDefinition() != null) && (((DefaultUDF)obj).getDefinition().getUDFClass() != null); + /* Note: detection of DefaultUDF is done on the exact class name rather than using "instanceof" in order to have only direct instances of DefaultUDF, + * and not extensions of it. Indeed, DefaultUDFs are generally created automatically by the ADQLQueryFactory ; so, extensions of it can only be custom + * UserDefinedFunctions. */ } - // Check all the inner regions: - if (r.regions != null) { - for(Region innerR : r.regions) - checkRegion(innerR, fct, binSearch, errors); + @Override + protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException { + try { + // get the associated UDF class: + Class<? extends UserDefinedFunction> udfClass = ((DefaultUDF)objToReplace).getDefinition().getUDFClass(); + // get the constructor with a single parameter of type ADQLOperand[]: + Constructor<? extends UserDefinedFunction> constructor = udfClass.getConstructor(ADQLOperand[].class); + // create a new instance of this UDF class with the operands stored in the object to replace: + return constructor.newInstance((Object)(((DefaultUDF)objToReplace).getParameters())); /* note: without this class, each item of the given array will be considered as a single parameter. */ + } catch(Exception ex) { + // IF NO INSTANCE CAN BE CREATED... + // ...keep the error for further report: + errors.addException(new UnresolvedFunctionException("Impossible to represent the function \"" + ((DefaultUDF)objToReplace).getName() + "\": the following error occured while creating this representation: \"" + ((ex instanceof InvocationTargetException) ? "[" + ex.getCause().getClass().getSimpleName() + "] " + ex.getCause().getMessage() : ex.getMessage()) + "\"", (DefaultUDF)objToReplace)); + // ...keep the same object (i.e. no replacement): + return objToReplace; + } } } - /* **************************************************** */ - /* METHODS CHECKING TYPES UNKNOWN WHILE CHECKING SYNTAX */ - /* **************************************************** */ - /** - * <p>Search all operands whose the type is not yet known and try to resolve it now - * and to check whether it matches the type expected by the syntactic parser.</p> - * - * <p> - * Only two operands may have an unresolved type: columns and user defined functions. - * Indeed, their type can be resolved only if the list of available columns and UDFs is known, - * and if columns and UDFs used in the query are resolved successfully. - * </p> - * - * <p> - * When an operand type is still unknown, they will own the three kinds of type and - * so this function won't raise an error: it is thus automatically on the expected type. - * This behavior is perfectly correct because if the type is not resolved - * that means the item/operand has not been resolved in the previous steps and so that - * an error about this item has already been raised. - * </p> + * Let searching all ADQL objects whose the type was not known while + * checking the syntax of the ADQL query. These objects are + * {@link ADQLColumn}s and {@link UserDefinedFunction}s. * * <p><i><b>Important note:</b> - * This function does not check the types exactly, but just roughly by considering only three categories: - * string, numeric and geometry. + * Only {@link UnknownType} instances having an expected type equals to 'S' + * (or 's' ; for string) or 'N' (or 'n' ; for numeric) are kept by this + * handler. Others are ignored. * </i></p> * - * @param query Query in which unknown types must be resolved and checked. - * @param errors List of errors to complete in this function each time a types does not match to the expected one. - * - * @see UnknownType - * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) * @since 1.3 */ - protected void checkTypes(final ADQLQuery query, final UnresolvedIdentifiersException errors) { - // Search all unknown types: - ISearchHandler sHandler = new SearchUnknownTypeHandler(); - sHandler.search(query); - - // Check whether their type matches the expected one: - UnknownType unknown; - for(ADQLObject result : sHandler) { - unknown = (UnknownType)result; - switch(unknown.getExpectedType()) { - case 'G': - case 'g': - if (!unknown.isGeometry()) - errors.addException(new ParseException("Type mismatch! A geometry was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); - break; - case 'N': - case 'n': - if (!unknown.isNumeric()) - errors.addException(new ParseException("Type mismatch! A numeric value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); - break; - case 'S': - case 's': - if (!unknown.isString()) - errors.addException(new ParseException("Type mismatch! A string value was expected instead of \"" + unknown.toADQL() + "\".", result.getPosition())); - break; - } + private static class SearchUnknownTypeHandler extends SimpleSearchHandler { + @Override + protected boolean match(ADQLObject obj) { + if (obj instanceof UnknownType) { + char expected = ((UnknownType)obj).getExpectedType(); + return (expected == 'G' || expected == 'g' || expected == 'S' || expected == 's' || expected == 'N' || expected == 'n'); + } else + return false; } } - /* ******************************** */ - /* METHODS CHECKING THE SUB-QUERIES */ - /* ******************************** */ - /** - * <p>Search all sub-queries found in the given query but not in the clause FROM. - * These sub-queries are then checked using {@link #check(ADQLQuery, Stack)}.</p> + * Implement the binary search algorithm over a sorted array. * - * <b>Fathers stack</b> * <p> - * Each time a sub-query must be checked with {@link #check(ADQLQuery, Stack)}, - * the list of all columns available in each of its father queries must be provided. - * This function is composing itself this stack by adding the given list of available - * columns (= all columns resolved in the given query) at the end of the given stack. - * If this stack is given empty, then a new stack is created. + * The only difference with the standard implementation of Java is + * that this object lets perform research with a different type + * of object than the types of array items. * </p> + * * <p> - * This modification of the given stack is just the execution time of this function. - * Before returning, this function removes the last item of the stack. + * For that reason, the "compare" function must always be implemented. * </p> * + * @author Grégory Mantelet (ARI) + * @version 1.3 (10/2014) * - * @param query Query in which sub-queries must be checked. - * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries. - * Each item of this stack is a list of columns available in each father-level query. - * <i>Note: this parameter is NULL if this function is called with the root/father query as parameter.</i> - * @param availableColumns List of all columns resolved in the given query. - * @param errors List of errors to complete in this function each time a semantic error is encountered. + * @param <T> Type of items stored in the array. + * @param <S> Type of the item to search. * * @since 1.3 */ - protected void checkSubQueries(final ADQLQuery query, Stack<SearchColumnList> fathersList, final SearchColumnList availableColumns, final UnresolvedIdentifiersException errors) { - // Check sub-queries outside the clause FROM: - ISearchHandler sHandler = new SearchSubQueryHandler(); - sHandler.search(query); - if (sHandler.getNbMatch() > 0) { - - // Push the list of columns into the father columns stack: - if (fathersList == null) - fathersList = new Stack<SearchColumnList>(); - fathersList.push(availableColumns); - - // Check each found sub-query: - for(ADQLObject result : sHandler) { - try { - check((ADQLQuery)result, fathersList); - } catch(UnresolvedIdentifiersException uie) { - Iterator<ParseException> itPe = uie.getErrors(); - while(itPe.hasNext()) - errors.addException(itPe.next()); - } - } + protected static abstract class BinarySearch<T, S> { + private int s, e, m, comp; - // Pop the list of columns from the father columns stack: - fathersList.pop(); + /** + * Search the given item in the given array. + * + * <p> + * In case the given object matches to several items of the array, + * this function will return the smallest index, pointing thus to the + * first of all matches. + * </p> + * + * @param searchItem Object for which a corresponding array item must + * be searched. + * @param array Array in which the given object must be searched. + * + * @return The array index of the first item of all matches. + */ + public int search(final S searchItem, final T[] array) { + s = 0; + e = array.length - 1; + while(s < e) { + // middle of the sorted array: + m = s + ((e - s) / 2); + // compare the fct with the middle item of the array: + comp = compare(searchItem, array[m]); + // if the fct is after, trigger the inspection of the right part of the array: + if (comp > 0) + s = m + 1; + // otherwise, the left part: + else + e = m; + } + if (s != e || compare(searchItem, array[s]) != 0) + return -1; + else + return s; } + + /** + * Compare the search item and the array item. + * + * @param searchItem Item whose a corresponding value must be found in the array. + * @param arrayItem An item of the array. + * + * @return Negative value if searchItem is less than arrayItem, 0 if they are equals, or a positive value if searchItem is greater. + */ + protected abstract int compare(final S searchItem, final T arrayItem); } - /* *************** */ - /* SEARCH HANDLERS */ - /* *************** */ + /* ********************************************************************** + * DEPRECATED STUFF ABOUT GEOMETRIES * + ********************************************************************** */ - /** - * Lets searching all {@link ADQLColumn} in the given object, - * EXCEPT in the GROUP BY and ORDER BY clauses. + /** List of all allowed geometrical functions (i.e. CONTAINS, REGION, POINT, + * COORD2, ...). * * <p> - * {@link ADQLColumn}s of the GROUP BY and ORDER BY may be aliases and so, - * they can not be checked exactly as a normal column. + * If this list is NULL, all geometrical functions are allowed. + * However, if not, all items of this list must be the only allowed + * geometrical functions. So, if the list is empty, no such function is + * allowed. * </p> * + * @since 1.3 + * @deprecated Since v2.0, supported geometrical functions must be declared + * in ADQLParser. */ + @Deprecated + protected String[] allowedGeo = null; + + /** <p>List of all allowed coordinate systems.</p> * <p> - * {@link ADQLColumn} of a {@link ColumnReference} may be an alias, they - * can not be checked exactly as a normal column. + * Each item of this list must be of the form: "{frame} {refpos} {flavor}". + * Each of these 3 items can be either of value, a list of values expressed with the syntax "({value1}|{value2}|...)" + * or a '*' to mean all possible values. + * </p> + * <p><i>Note: since a default value (corresponding to the empty string - '') should always be possible for each part of a coordinate system, + * the checker will always add the default value (UNKNOWNFRAME, UNKNOWNREFPOS or SPHERICAL2) into the given list of possible values for each coord. sys. part.</i></p> + * <p> + * If this list is NULL, all coordinates systems are allowed. + * However, if not, all items of this list must be the only allowed coordinate systems. + * So, if the list is empty, none is allowed. * </p> + * @since 1.3 + * @deprecated Since v2.0, supported coordinate systems must be declared + * in ADQLParser. */ + @Deprecated + protected String[] allowedCoordSys = null; + + /** <p>A regular expression built using the list of allowed coordinate systems. + * With this regex, it is possible to known whether a coordinate system expression is allowed or not.</p> + * <p>If NULL, all coordinate systems are allowed.</p> + * @since 1.3 + * @deprecated Since v2.0, supported coordinate systems must be declared + * in ADQLParser. */ + @Deprecated + protected String coordSysRegExp = null; + + /** + * <p>Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.</p> * - * @author Grégory Mantelet (ARI;CDS) - * @version 2.0 (08/2019) - * @since 1.4 + * <p>Verifications done by this object after creation:</p> + * <ul> + * <li>Existence of tables and columns: <b>OK</b></li> + * <li>Existence of User Defined Functions (UDFs): <b>NO <i>(any "unknown" function is allowed)</i></b></li> + * <li>Support of geometrical functions: <b>OK</b></li> + * <li>Support of coordinate systems: <b>OK</b></li> + * </ul> + * + * @param tables List of all available tables. + * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). + * If NULL, no verification will be done (and so, all geometries are allowed). + * If empty list, no geometry function is allowed. + * <i>Note: match with items of this list are done case insensitively.</i> + * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: + * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. + * Each part of this pattern can be one the possible values (case insensitive), a list of possible values + * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. + * For instance: "ICRS (GEOCENTER|heliocenter) *". + * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). + * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). + * + * @since 1.3 + * @deprecated Since v2.0, the check of geometrical functions support is + * performed in ADQLParser. It must now be done with + * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} + * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). */ - private static class SearchColumnOutsideGroupByHandler extends SearchColumnHandler { - @Override - protected boolean goInto(final ADQLObject obj) { - if (obj instanceof ClauseADQL<?> && ((ClauseADQL<?>)obj).getName() != null) { - ClauseADQL<?> clause = (ClauseADQL<?>)obj; - return !(clause.getName().equalsIgnoreCase("GROUP BY") || clause.getName().equalsIgnoreCase("ORDER BY")); - } else - return super.goInto(obj); + @Deprecated + public DBChecker(final Collection<? extends DBTable> tables, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException { + this(tables, null, allowedGeoFcts, allowedCoordSys); + } + + /** + * <p>Builds a {@link DBChecker}.</p> + * + * <p>Verifications done by this object after creation:</p> + * <ul> + * <li>Existence of tables and columns: <b>OK</b></li> + * <li>Existence of User Defined Functions (UDFs): <b>OK</b></li> + * <li>Support of coordinate systems: <b>OK</b></li> + * </ul> + * + * <p><i><b>IMPORTANT note:</b> + * Since v2.0, the check of supported geometrical functions is performed + * directly in ADQLParser through the notion of Optional Features. + * The declaration of supported geometrical functions must now be done + * with {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} + * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). + * </i></p> + * + * @param tables List of all available tables. + * @param allowedUdfs List of all allowed user defined functions. + * If NULL, no verification will be done (and so, all UDFs are allowed). + * If empty list, no "unknown" (or UDF) is allowed. + * <i>Note: match with items of this list are done case insensitively.</i> + * @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1). + * If NULL, no verification will be done (and so, all geometries are allowed). + * If empty list, no geometry function is allowed. + * <i>Note: match with items of this list are done case insensitively.</i> + * @param allowedCoordSys List of all allowed coordinate system patterns. The syntax of a such pattern is the following: + * "{frame} {refpos} {flavor}" ; on the contrary to a coordinate system expression, here no part is optional. + * Each part of this pattern can be one the possible values (case insensitive), a list of possible values + * expressed with the syntax "({value1}|{value2}|...)", or a '*' for any valid value. + * For instance: "ICRS (GEOCENTER|heliocenter) *". + * If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed). + * If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: ''). + * + * @since 2.0 + * @deprecated Since v2.0, the check of geometrical functions support is + * performed in ADQLParser. It must now be done with + * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} + * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). + */ + @Deprecated + public DBChecker(final Collection<? extends DBTable> tables, final Collection<? extends FunctionDef> allowedUdfs, final Collection<String> allowedGeoFcts, final Collection<String> allowedCoordSys) throws ParseException { + // Set the list of available tables + Set the list of all known UDFs: + this(tables, allowedUdfs); + + // Set the list of allowed geometrical functions: + allowedGeo = specialSort(allowedGeoFcts); + + // Set the list of allowed coordinate systems: + this.allowedCoordSys = specialSort(allowedCoordSys); + coordSysRegExp = STCS.buildCoordSysRegExp(this.allowedCoordSys); + } + + /** + * Transform the given collection of string elements in a sorted array. + * Only non-NULL and non-empty strings are kept. + * + * @param items Items to copy and sort. + * + * @return A sorted array containing all - except NULL and empty strings - items of the given collection. + * + * @since 1.3 + * + * @deprecated Since v2.0, this tool function is no longer used. It was + * useful only to collect allowed geometries and coordinate + * systems....but these are now checked by + * {@link adql.parser.ADQLParser ADQLParser}. + */ + @Deprecated + protected final static String[] specialSort(final Collection<String> items) { + // Nothing to do if the array is NULL: + if (items == null) + return null; + + // Keep only valid items (not NULL and not empty string): + String[] tmp = new String[items.size()]; + int cnt = 0; + for(String item : items) { + if (item != null && item.trim().length() > 0) + tmp[cnt++] = item; } + + // Make an adjusted array copy: + String[] copy = new String[cnt]; + System.arraycopy(tmp, 0, copy, 0, cnt); + + // Sort the values: + Arrays.sort(copy); + + return copy; } /** - * Lets searching all tables. + * <p>Check all geometries.</p> * - * @author Grégory Mantelet (CDS) - * @version 1.0 (07/2011) + * <p>Operations done in this function:</p> + * <ol> + * <li>Check that all explicit (string constant) coordinate system definitions are supported</i></li> + * <li>Check all STC-S expressions (only in {@link RegionFunction} for the moment) and + * Apply the 2 previous checks on them</li> + * </ol> + * + * <p><i><b>IMPORTANT note:</b> + * Since v2.0, the check of supported geometrical functions is performed + * directly in ADQLParser through the notion of Optional Features. + * The declaration of supported geometrical functions must now be done + * with {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()} + * (see also {@link adql.parser.feature.FeatureSet FeatureSet}). + * </i></p> + * + * @param query Query in which geometries must be checked. + * @param errors List of errors to complete in this function each time a geometry item is not supported. + * + * @see #resolveCoordinateSystems(ADQLQuery, UnresolvedIdentifiersException) + * @see #resolveSTCSExpressions(ADQLQuery, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + * + * @deprecated Since 2.0, validation of the geometric functions is + * performed automatically by + * {@link adql.parser.ADQLParser ADQLParser}. Geometric + * functions are optional features and should be declared as + * such in the {@link adql.parser.ADQLParser ADQLParser} if + * they are supported (see + * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). */ - private static class SearchTableHandler extends SimpleSearchHandler { - @Override - public boolean match(final ADQLObject obj) { - return obj instanceof ADQLTable; + @Deprecated + protected void checkGeometries(final ADQLQuery query, final UnresolvedIdentifiersException errors) { + BinarySearch<String, String> binSearch = new BinarySearch<String, String>() { + @Override + protected int compare(String searchItem, String arrayItem) { + return searchItem.compareToIgnoreCase(arrayItem); + } + }; + + // a. Check whether the coordinate systems are allowed: + if (allowedCoordSys != null) + resolveCoordinateSystems(query, errors); + + // b. Check all STC-S expressions (in RegionFunctions only) + the used coordinate systems (if StringConstant only): + if (allowedGeo == null || (allowedGeo.length > 0 && binSearch.search("REGION", allowedGeo) >= 0)) + resolveSTCSExpressions(query, binSearch, errors); + } + + /** + * Search for all geometrical functions and check whether they are allowed. + * + * @param query Query in which geometrical functions must be checked. + * @param errors List of errors to complete in this function each time a geometrical function is not supported. + * + * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + * + * @deprecated Since 2.0, validation of the geometric functions is + * performed automatically by + * {@link adql.parser.ADQLParser ADQLParser}. Geometric + * functions are optional features and should be declared as + * such in the {@link adql.parser.ADQLParser ADQLParser} if + * they are supported (see + * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). + */ + @Deprecated + protected final void resolveGeometryFunctions(final ADQLQuery query, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { + ISearchHandler sHandler = new SearchGeometryHandler(); + sHandler.search(query); + + String fctName; + for(ADQLObject result : sHandler) { + fctName = result.getName(); + checkGeometryFunction(fctName, (ADQLFunction)result, binSearch, errors); } } /** - * Lets searching all wildcards. + * <p>Check whether the specified geometrical function is allowed by this implementation.</p> * - * @author Grégory Mantelet (CDS) - * @version 1.0 (09/2011) + * <p><i>Note: + * If the list of allowed geometrical functions is empty, this function will always add an errors to the given list. + * Indeed, it means that no geometrical function is allowed and so that the specified function is automatically not supported. + * </i></p> + * + * @param fctName Name of the geometrical function to test. + * @param fct The function instance being or containing the geometrical function to check. <i>Note: this function can be the function to test or a function embedding the function under test (i.e. RegionFunction). + * @param binSearch The object to use in order to search a function name inside the list of allowed functions. + * It is able to perform a binary search inside a sorted array of String objects. The interest of + * this object is its compare function which must be overridden and tells how to compare the item + * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). + * @param errors List of errors to complete in this function each time a geometrical function is not supported. + * + * @since 1.3 + * + * @deprecated Since 2.0, validation of the geometric functions is + * performed automatically by + * {@link adql.parser.ADQLParser ADQLParser}. Geometric + * functions are optional features and should be declared as + * such in the {@link adql.parser.ADQLParser ADQLParser} if + * they are supported (see + * {@link adql.parser.ADQLParser#getSupportedFeatures() ADQLParser.getSupportedFeatures()}). */ - private static class SearchWildCardHandler extends SimpleSearchHandler { - @Override - public boolean match(final ADQLObject obj) { - return (obj instanceof SelectAllColumns) && (((SelectAllColumns)obj).getAdqlTable() != null); + @Deprecated + protected final void checkGeometryFunction(final String fctName, final ADQLFunction fct, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { + int match = -1; + if (allowedGeo.length != 0) + match = binSearch.search(fctName, allowedGeo); + if (match < 0) + errors.addException(new UnresolvedFunctionException("The geometrical function \"" + fctName + "\" is not available in this implementation!", fct)); + } + + /** + * <p>Search all explicit coordinate system declarations, check their syntax and whether they are allowed by this implementation.</p> + * + * <p><i>Note: + * "explicit" means here that all {@link StringConstant} instances. Only coordinate systems expressed as string can + * be parsed and so checked. So if a coordinate system is specified by a column, no check can be done at this stage... + * it will be possible to perform such test only at the execution. + * </i></p> + * + * @param query Query in which coordinate systems must be checked. + * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. + * + * @see #checkCoordinateSystem(StringConstant, UnresolvedIdentifiersException) + * + * @since 1.3 + * + * @deprecated Since 2.0, the validation of coordinate systems is performed + * automatically by {@link adql.parser.ADQLParser ADQLParser}. + */ + @Deprecated + protected void resolveCoordinateSystems(final ADQLQuery query, final UnresolvedIdentifiersException errors) { + ISearchHandler sHandler = new SearchCoordSysHandler(); + sHandler.search(query); + for(ADQLObject result : sHandler) + checkCoordinateSystem((StringConstant)result, errors); + } + + /** + * Parse and then check the coordinate system contained in the given {@link StringConstant} instance. + * + * @param adqlCoordSys The {@link StringConstant} object containing the coordinate system to check. + * @param errors List of errors to complete in this function each time a coordinate system has a wrong syntax or is not supported. + * + * @see STCS#parseCoordSys(String) + * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) + * + * @since 1.3 + * + * @deprecated Since 2.0, the validation of coordinate systems is performed + * automatically by {@link adql.parser.ADQLParser ADQLParser}. + */ + @Deprecated + protected void checkCoordinateSystem(final StringConstant adqlCoordSys, final UnresolvedIdentifiersException errors) { + String coordSysStr = adqlCoordSys.getValue(); + try { + checkCoordinateSystem(STCS.parseCoordSys(coordSysStr), adqlCoordSys, errors); + } catch(ParseException pe) { + errors.addException(new ParseException(pe.getMessage(), adqlCoordSys.getPosition())); } } /** - * Lets searching column references. + * Check whether the given coordinate system is allowed by this implementation. * - * @author Grégory Mantelet (CDS) - * @version 1.0 (11/2011) + * @param coordSys Coordinate system to test. + * @param operand The operand representing or containing the coordinate system under test. + * @param errors List of errors to complete in this function each time a coordinate system is not supported. + * + * @since 1.3 + * + * @deprecated Since 2.0, the validation of coordinate systems is performed + * automatically by {@link adql.parser.ADQLParser ADQLParser}. */ - private static class SearchColReferenceHandler extends SimpleSearchHandler { - @Override - public boolean match(final ADQLObject obj) { - return (obj instanceof ColumnReference); + @Deprecated + protected void checkCoordinateSystem(final CoordSys coordSys, final ADQLOperand operand, final UnresolvedIdentifiersException errors) { + if (coordSysRegExp != null && coordSys != null && !coordSys.toFullSTCS().matches(coordSysRegExp)) { + StringBuffer buf = new StringBuffer(); + if (allowedCoordSys != null) { + for(String cs : allowedCoordSys) { + if (buf.length() > 0) + buf.append(", "); + buf.append(cs); + } + } + if (buf.length() == 0) + buf.append("No coordinate system is allowed!"); + else + buf.insert(0, "Allowed coordinate systems are: "); + errors.addException(new ParseException("Coordinate system \"" + ((operand instanceof StringConstant) ? ((StringConstant)operand).getValue() : coordSys.toString()) + "\" (= \"" + coordSys.toFullSTCS() + "\") not allowed in this implementation. " + buf.toString(), operand.getPosition())); } } /** - * <p>Lets searching subqueries in every clause except the FROM one (hence the modification of the {@link #goInto(ADQLObject)}.</p> + * <p>Search all STC-S expressions inside the given query, parse them (and so check their syntax) and then determine + * whether the declared coordinate system and the expressed region are allowed in this implementation.</p> * - * <p><i> - * <u>Note:</u> The function {@link #addMatch(ADQLObject, ADQLIterator)} has been modified in order to - * not have the root search object (here: the main query) in the list of results. + * <p><i>Note: + * In the current ADQL language definition, STC-S expressions can be found only as only parameter of the REGION function. * </i></p> * - * @author Grégory Mantelet (ARI) - * @version 1.2 (12/2013) - * @since 1.2 + * @param query Query in which STC-S expressions must be checked. + * @param binSearch The object to use in order to search a region name inside the list of allowed functions/regions. + * It is able to perform a binary search inside a sorted array of String objects. The interest of + * this object is its compare function which must be overridden and tells how to compare the item + * to search and the items of the array (basically, a non-case-sensitive comparison between 2 strings). + * @param errors List of errors to complete in this function each time the STC-S syntax is wrong or each time the declared coordinate system or region is not supported. + * + * @see STCS#parseRegion(String) + * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) + * + * @since 1.3 + * + * @deprecated Since 2.0, the validation of STCs expressions is performed + * automatically by {@link adql.parser.ADQLParser ADQLParser}. */ - private static class SearchSubQueryHandler extends SimpleSearchHandler { - @Override - protected void addMatch(ADQLObject matchObj, ADQLIterator it) { - if (it != null) - super.addMatch(matchObj, it); - } + @Deprecated + protected void resolveSTCSExpressions(final ADQLQuery query, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { + // Search REGION functions: + ISearchHandler sHandler = new SearchRegionHandler(); + sHandler.search(query); - @Override - protected boolean goInto(ADQLObject obj) { - return super.goInto(obj) && !(obj instanceof FromContent); - } + // Parse and check their STC-S expression: + String stcs; + Region region; + for(ADQLObject result : sHandler) { + try { + // get the STC-S expression: + stcs = ((StringConstant)((RegionFunction)result).getParameter(0)).getValue(); - @Override - protected boolean match(ADQLObject obj) { - return (obj instanceof ADQLQuery); - } - } + // parse the STC-S expression (and so check the syntax): + region = STCS.parseRegion(stcs); - /** - * Let searching user defined functions. - * - * @author Grégory Mantelet (ARI) - * @version 1.3 (10/2014) - * @since 1.3 - */ - private static class SearchUDFHandler extends SimpleSearchHandler { - @Override - protected boolean match(ADQLObject obj) { - return (obj instanceof UserDefinedFunction); + // check whether the regions (this one + the possible inner ones) and the coordinate systems are allowed: + checkRegion(region, (RegionFunction)result, binSearch, errors); + } catch(ParseException pe) { + errors.addException(new ParseException(pe.getMessage(), result.getPosition())); + } } } /** - * <p>Let replacing every {@link DefaultUDF}s whose a {@link FunctionDef} is set by their corresponding {@link UserDefinedFunction} class.</p> + * <p>Check the given region.</p> * - * <p><i><b>Important note:</b> - * If the replacer can not be created using the class returned by {@link FunctionDef#getUDFClass()}, no replacement is performed. - * </i></p> + * <p>The following points are checked in this function:</p> + * <ul> + * <li>whether the coordinate system is allowed</li> + * <li>whether the type of region is allowed</li> + * <li>whether the inner regions are correct (here this function is called recursively on each inner region).</li> + * </ul> + * + * @param r The region to check. + * @param fct The REGION function containing the region to check. + * @param errors List of errors to complete in this function if the given region or its inner regions are not supported. + * + * @see #checkCoordinateSystem(adql.db.STCS.CoordSys, ADQLOperand, UnresolvedIdentifiersException) + * @see #checkGeometryFunction(String, ADQLFunction, BinarySearch, UnresolvedIdentifiersException) + * @see #checkRegion(adql.db.STCS.Region, RegionFunction, BinarySearch, UnresolvedIdentifiersException) * - * @author Grégory Mantelet (ARI) - * @version 1.3 (02/2015) * @since 1.3 + * + * @deprecated Since 2.0, the validation of REGIONs is performed + * automatically by {@link adql.parser.ADQLParser ADQLParser}. */ - private static class ReplaceDefaultUDFHandler extends SimpleReplaceHandler { - private final UnresolvedIdentifiersException errors; + @Deprecated + protected void checkRegion(final Region r, final RegionFunction fct, final BinarySearch<String, String> binSearch, final UnresolvedIdentifiersException errors) { + if (r == null) + return; - public ReplaceDefaultUDFHandler(final UnresolvedIdentifiersException errorsContainer) { - errors = errorsContainer; - } + // Check the coordinate system (if any): + if (r.coordSys != null) + checkCoordinateSystem(r.coordSys, fct, errors); - @Override - protected boolean match(ADQLObject obj) { - return (obj.getClass().getName().equals(DefaultUDF.class.getName())) && (((DefaultUDF)obj).getDefinition() != null) && (((DefaultUDF)obj).getDefinition().getUDFClass() != null); - /* Note: detection of DefaultUDF is done on the exact class name rather than using "instanceof" in order to have only direct instances of DefaultUDF, - * and not extensions of it. Indeed, DefaultUDFs are generally created automatically by the ADQLQueryFactory ; so, extensions of it can only be custom - * UserDefinedFunctions. */ + // Check that the region type is allowed: + if (allowedGeo != null) { + if (allowedGeo.length == 0) + errors.addException(new UnresolvedFunctionException("The region type \"" + r.type + "\" is not available in this implementation!", fct)); + else + checkGeometryFunction((r.type == RegionType.POSITION) ? "POINT" : r.type.toString(), fct, binSearch, errors); } - @Override - protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException { - try { - // get the associated UDF class: - Class<? extends UserDefinedFunction> udfClass = ((DefaultUDF)objToReplace).getDefinition().getUDFClass(); - // get the constructor with a single parameter of type ADQLOperand[]: - Constructor<? extends UserDefinedFunction> constructor = udfClass.getConstructor(ADQLOperand[].class); - // create a new instance of this UDF class with the operands stored in the object to replace: - return constructor.newInstance((Object)(((DefaultUDF)objToReplace).getParameters())); /* note: without this class, each item of the given array will be considered as a single parameter. */ - } catch(Exception ex) { - // IF NO INSTANCE CAN BE CREATED... - // ...keep the error for further report: - errors.addException(new UnresolvedFunctionException("Impossible to represent the function \"" + ((DefaultUDF)objToReplace).getName() + "\": the following error occured while creating this representation: \"" + ((ex instanceof InvocationTargetException) ? "[" + ex.getCause().getClass().getSimpleName() + "] " + ex.getCause().getMessage() : ex.getMessage()) + "\"", (DefaultUDF)objToReplace)); - // ...keep the same object (i.e. no replacement): - return objToReplace; - } + // Check all the inner regions: + if (r.regions != null) { + for(Region innerR : r.regions) + checkRegion(innerR, fct, binSearch, errors); } } @@ -1755,7 +1902,10 @@ public class DBChecker implements QueryChecker { * @author Grégory Mantelet (ARI) * @version 1.3 (10/2014) * @since 1.3 + * + * @deprecated Since 2.0. */ + @Deprecated private static class SearchGeometryHandler extends SimpleSearchHandler { @Override protected boolean match(ADQLObject obj) { @@ -1763,30 +1913,6 @@ public class DBChecker implements QueryChecker { } } - /** - * <p>Let searching all ADQL objects whose the type was not known while checking the syntax of the ADQL query. - * These objects are {@link ADQLColumn}s and {@link UserDefinedFunction}s.</p> - * - * <p><i><b>Important note:</b> - * Only {@link UnknownType} instances having an expected type equals to 'S' (or 's' ; for string) or 'N' (or 'n' ; for numeric) - * are kept by this handler. Others are ignored. - * </i></p> - * - * @author Grégory Mantelet (ARI) - * @version 1.3 (10/2014) - * @since 1.3 - */ - private static class SearchUnknownTypeHandler extends SimpleSearchHandler { - @Override - protected boolean match(ADQLObject obj) { - if (obj instanceof UnknownType) { - char expected = ((UnknownType)obj).getExpectedType(); - return (expected == 'G' || expected == 'g' || expected == 'S' || expected == 's' || expected == 'N' || expected == 'n'); - } else - return false; - } - } - /** * Let searching all explicit declaration of coordinate systems. * So, only {@link StringConstant} objects will be returned. @@ -1834,75 +1960,4 @@ public class DBChecker implements QueryChecker { } } - - /** - * <p>Implement the binary search algorithm over a sorted array.</p> - * - * <p> - * The only difference with the standard implementation of Java is - * that this object lets perform research with a different type - * of object than the types of array items. - * </p> - * - * <p> - * For that reason, the "compare" function must always be implemented. - * </p> - * - * @author Grégory Mantelet (ARI) - * @version 1.3 (10/2014) - * - * @param <T> Type of items stored in the array. - * @param <S> Type of the item to search. - * - * @since 1.3 - */ - protected static abstract class BinarySearch<T, S> { - private int s, e, m, comp; - - /** - * <p>Search the given item in the given array.</p> - * - * <p> - * In case the given object matches to several items of the array, - * this function will return the smallest index, pointing thus to the first - * of all matches. - * </p> - * - * @param searchItem Object for which a corresponding array item must be searched. - * @param array Array in which the given object must be searched. - * - * @return The array index of the first item of all matches. - */ - public int search(final S searchItem, final T[] array) { - s = 0; - e = array.length - 1; - while(s < e) { - // middle of the sorted array: - m = s + ((e - s) / 2); - // compare the fct with the middle item of the array: - comp = compare(searchItem, array[m]); - // if the fct is after, trigger the inspection of the right part of the array: - if (comp > 0) - s = m + 1; - // otherwise, the left part: - else - e = m; - } - if (s != e || compare(searchItem, array[s]) != 0) - return -1; - else - return s; - } - - /** - * Compare the search item and the array item. - * - * @param searchItem Item whose a corresponding value must be found in the array. - * @param arrayItem An item of the array. - * - * @return Negative value if searchItem is less than arrayItem, 0 if they are equals, or a positive value if searchItem is greater. - */ - protected abstract int compare(final S searchItem, final T arrayItem); - } - } diff --git a/src/adql/db/DBColumn.java b/src/adql/db/DBColumn.java index 07988bfd0830e5c65002c5d233f10f30fceca177..ec4bc3687ae1c83e55bb772660603a66566b5b24 100644 --- a/src/adql/db/DBColumn.java +++ b/src/adql/db/DBColumn.java @@ -35,7 +35,20 @@ package adql.db; public interface DBColumn { /** - * Gets the name of this column (without any prefix and double-quotes). + * Gets the name of this column. + * + * <i> + * <p><b>Notes:</b> + * The returned ADQL name is: + * </p> + * <ul> + * <li>non-empty/NULL</li> + * <li>non-delimited (i.e. not between double quotes),</li> + * <li>non-prefixed (i.e. no table/schema/catalog name)</li> + * <li>and in the same case as provided at initialization (even if not case + * sensitive).</li> + * </ul> + * </i> * * @return Its ADQL name. */ @@ -54,17 +67,31 @@ public interface DBColumn { public boolean isCaseSensitive(); /** - * Gets the name of this column in the "database". + * Gets the name of this column in the "database" (e.g. as it should be used + * in SQL queries). + * + * <i> + * <p><b>Notes</b> + * The returned DB name is: + * </p> + * <ul> + * <li>non-empty/NULL</li> + * <li>non-delimited (i.e. not between double quotes),</li> + * <li>non-prefixed (i.e. no table/schema/catalog name)</li> + * <li>and in the EXACT case as it MUST be used.</li> + * </ul> * * @return Its DB name. */ public String getDBName(); /** - * <p>Get the type of this column (as closed as possible from the "database" type).</p> + * Get the type of this column (as closed as possible from the "database" + * type). * - * <p><i>Note: - * The returned type should be as closed as possible from a type listed by the IVOA in the TAP protocol description into the section UPLOAD. + * <p><i><b>Note:</b> + * The returned type should be as closed as possible from a type listed by + * the IVOA in the TAP protocol description into the section UPLOAD. * </i></p> * * @return Its type. @@ -76,7 +103,8 @@ public interface DBColumn { /** * Gets the table which contains this {@link DBColumn}. * - * @return Its table or <i>null</i> if no table is specified. + * @return Its table + * or NULL if no table is specified. */ public DBTable getTable(); diff --git a/src/adql/db/DBIdentifier.java b/src/adql/db/DBIdentifier.java new file mode 100644 index 0000000000000000000000000000000000000000..a85f8c01e8a2ccfc86a7005fd9474cbb1033c877 --- /dev/null +++ b/src/adql/db/DBIdentifier.java @@ -0,0 +1,344 @@ +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 2019- UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) + */ + +/** + * Generic implementation of any kind of ADQL/DB identifier. + * + * <p> + * It already implements functions getting and setting the ADQL and DB names + * of the interfaces {@link DBTable} and {@link DBColumn}. Thus, it guarantees + * that all DB... identifiers will behave the same way when manipulating their + * ADQL and DB names. + * </p> + * + * @author Grégory Mantelet (CDS) + * @version 2.0 (09/2019) + * @since 2.0 + * + * @see DBTable + * @see DBColumn + */ +public abstract class DBIdentifier { + + /** Regular expression of a delimited identifier (i.e. an identifier between + * double quotes ; an inner double quote is escaped by doubling it). */ + private final static String REGEXP_DELIMITED = "\"(\"\"|[^\"])*\""; + + /** Name (not delimited, not prefixed) to use in ADQL queries. + * <p><i><b>Important:</b> It must never be NULL.</i></p> */ + protected String adqlName = null; + + /** A flag indicating if the ADQL name is case sensitive or not (i.e. if it + * must be delimited or not in an ADQL query). */ + protected boolean adqlCaseSensitive = false; + + /** Name (not delimited, not prefixed) of this identifier in the "database". + * This name must be used, for example, while translating an ADQL query into + * SQL. + * <p><i><b>Note:</b> It may be NULL. In such case, {@link #getDBName()} + * must return {@link #adqlName}.</i></p> */ + protected String dbName = null; + + /** + * Create an identifier with the given ADQL name. + * + * <p> + * In this constructor, the DB name is not set. Thus, {@link #getDBName()} + * will return the same as {@link #getADQLName()}. + * </p> + * + * <p><i><b>Note:</b> + * If the given name is delimited, the surrounding double quotes will be + * removed and {@link #isCaseSensitive()} will return <code>true</code>. + * </i></p> + * + * @param adqlName The ADQL and DB name of this identifier. + * <i>It may be delimited and/or qualified.</i> + * + * @throws NullPointerException If the given name is NULL or empty. + * + * @see #setADQLName(String) + */ + protected DBIdentifier(final String adqlName) throws NullPointerException { + setADQLName(adqlName); + } + + /** + * Create an identifier with the given ADQL and DB names. + * + * <p> + * In this constructor, the DB name is not set. Thus, {@link #getDBName()} + * will return the same as {@link #getADQLName()}. + * </p> + * + * <p><i><b>Note:</b> + * If the given name is delimited, the surrounding double quotes will be + * removed and {@link #isCaseSensitive()} will return <code>true</code>. + * </i></p> + * + * @param adqlName The ADQL and DB name of this identifier. + * <i>It may be delimited and/or qualified.</i> + * + * @throws NullPointerException If the given name is NULL or empty. + * + * @see #setADQLName(String) + * @see #setDBName(String) + */ + protected DBIdentifier(final String adqlName, final String dbName) throws NullPointerException { + setADQLName(adqlName); + setDBName(dbName); + } + + /** + * Get the ADQL version of this identifier. + * + * <p> + * This name is neither delimited, nor prefixed. + * To determine whether it should be delimited in an ADQL query, use + * {@link #isCaseSensitive()}. + * </p> + * + * <p><i><b>Note:</b> + * The returned string is never empty or NULL. + * </i></p> + * + * @return The name to use in ADQL queries. + */ + public String getADQLName() { + return adqlName; + } + + /** + * Set the ADQL version of this identifier. + * + * <p> + * If the given name is delimited, the surrounding double quotes will be + * removed and case sensitivity will be set to <code>true</code> + * (i.e. {@link #isCaseSensitive()} will return <code>true</code>). + * </p> + * + * <p><i><b>Note:</b> + * The given name must not be prefixed. + * </i></p> + * + * <p><i><b>WARNING:</b> + * If the given name is NULL or empty (even after removal of surrounding + * double quotes, if delimited), this function will immediately throw an + * exception. + * </i></p> + * + * @param newName New ADQL version of this identifier. + * + * @throws NullPointerException If the given name is NULL or empty. + * + * @see #isDelimited(String) + * @see #normalize(String) + */ + public void setADQLName(final String newName) throws NullPointerException { + boolean adqlCS = isDelimited(newName); + String normName = normalize(newName); + + if (normName == null) + throw new NullPointerException("Missing ADQL name!"); + + this.adqlName = normName; + this.adqlCaseSensitive = adqlCS; + } + + /** + * Tell whether the ADQL version of this identifier is case sensitive or + * not. + * + * <p> + * If case sensitive, the ADQL name must be written between double quotes + * (and all inner double quotes should be doubled). + * </p> + * + * @return <code>true</code> if case sensitive, + * <code>false</code> otherwise. + */ + public boolean isCaseSensitive() { + return adqlCaseSensitive; + } + + /** + * Set the case sensitivity of the ADQL version of this identifier. + * + * <p> + * Setting the case sensitivity to <code>true</code> will force the + * delimited form of the ADQL name (i.e. it will be written between + * double quotes). + * </p> + * + * @param caseSensitive <code>true</code> to declare the ADQL name as case + * sensitive, + * <code>false</code> otherwise. + */ + public void setCaseSensitive(final boolean caseSensitive) { + this.adqlCaseSensitive = caseSensitive; + } + + /** + * Get the database version of this identifier. + * + * <p>This name is neither delimited, nor prefixed.</p> + * + * <p>In an SQL query, this name should be considered as case sensitive.</p> + * + * <p><i><b>Note:</b> + * The returned string is never empty or NULL. + * </i></p> + * + * @return The real name of this identifier in the "database". + */ + public String getDBName() { + return (dbName == null) ? getADQLName() : dbName; + } + + /** + * Set the database version of this identifier. + * + * <p> + * If the given name is delimited, the surrounding double quotes will be + * removed. + * </p> + * + * <p><i><b>Note 1:</b> + * The given name should not be prefixed. + * </i></p> + * + * <p><i><b>Note 2:</b> + * If the given name is NULL or empty (even after removal of surrounding + * double quotes if delimited), {@link #getDBName()} will return the same + * as {@link #getADQLName()}. + * </i></p> + * + * @param newName The real name of this identifier in the "database". + * + * @see #normalize(String) + */ + public void setDBName(final String newName) { + dbName = normalize(newName); + } + + /** + * Tell whether the given identifier is delimited (i.e. within the same pair + * of double quotes - <code>"</code>). + * + * <i> + * <p>The following identifiers ARE delimited:</p> + * <ul> + * <li><code>"a"</code></li> + * <li><code>""</code> (empty string ; but won't be considered as a + * valid ADQL name)</li> + * <li><code>" "</code> (string with spaces ; but won't be considered as a + * valid ADQL name)</li> + * <li><code>"foo.bar"</code></li> + * <li><code>"foo"".""bar"</code> (with escaped double quotes)</li> + * <li><code>""""</code> (idem)</li> + * </ul> + * </i> + * + * <i> + * <p>The following identifiers are NOT considered as delimited:</p> + * <ul> + * <li><code>"foo</code> (missing ending double quote)</li> + * <li><code>foo"</code> (missing leading double quote)</li> + * <li><code>"foo"."bar"</code> (not the same pair of double quotes)</li> + * </ul> + * </i> + * + * @param ident Identifier that may be delimited. + * + * @return <code>true</code> if the given identifier is delimited, + * <code>false</code> otherwise. + */ + public static boolean isDelimited(final String ident) { + return ident != null && ident.trim().matches(REGEXP_DELIMITED); + } + + /** + * Normalize the given identifier. + * + * <p>This function performs the following operations:</p> + * <ol> + * <li>Remove leading and trailing space characters.</li> + * <li>If the resulting string is empty, return NULL.</li> + * <li>If {@link #isDelimited(String) delimited}, remove the leading and + * trailing double quotes.</li> + * <li>If the resulting string without leading and trailing spaces is + * empty, return NULL.</li> + * <li>Return the resulting string.</li> + * </ol> + * + * @param ident The identifier to normalize. + * + * @return The normalized string, + * or NULL if NULL or empty. + * + * @see #denormalize(String, boolean) + */ + public static String normalize(final String ident) { + // Return NULL if empty: + if (ident == null || ident.trim().length() == 0) + return null; + + // Remove leading and trailing space characters: + String normIdent = ident.trim(); + + // If delimited, remove the leading and trailing ": + if (isDelimited(normIdent)) { + normIdent = normIdent.substring(1, normIdent.length() - 1).replaceAll("\"\"", "\""); + return (normIdent.trim().length() == 0) ? null : normIdent; + } else + return normIdent; + } + + /** + * De-normalize the given string. + * + * <p> + * This function does something only if the given string is declared as + * case sensitive. In such case, it will surround it by double quotes. + * All inner double quotes will be escaped by doubling them. + * </p> + * + * <p><i><b>Note:</b> + * If the given string is NULL, it will be returned as such (i.e. NULL). + * </i></p> + * + * @param ident The identifier to de-normalize. + * @param caseSensitive <code>true</code> if the given identifier is + * considered as case sensitive, + * <code>false</code> otherwise. + * + * @return The de-normalized identifier. + * + * @see #normalize(String) + */ + public static String denormalize(final String ident, final boolean caseSensitive) { + if (caseSensitive && ident != null) + return "\"" + ident.replaceAll("\"", "\"\"") + "\""; + else + return ident; + } + +} diff --git a/src/adql/db/DBTable.java b/src/adql/db/DBTable.java index 8ff8dea37d38b85578fefeca7c40bcea2827cc50..cefb4a3c8a4467b02b2776a0c0e8175d4d03c359 100644 --- a/src/adql/db/DBTable.java +++ b/src/adql/db/DBTable.java @@ -35,7 +35,20 @@ package adql.db; public interface DBTable extends Iterable<DBColumn> { /** - * Gets the name of this table (without any prefix and double-quotes). + * Gets the name of this table in ADQL queries. + * + * <i> + * <p><b>Notes:</b> + * The returned ADQL name is: + * </p> + * <ul> + * <li>non-empty/NULL</li> + * <li>non-delimited (i.e. not between double quotes),</li> + * <li>non-prefixed (i.e. no schema/catalog name)</li> + * <li>and in the same case as provided at initialization (even if not case + * sensitive).</li> + * </ul> + * </i> * * @return Its ADQL name. */ @@ -54,7 +67,19 @@ public interface DBTable extends Iterable<DBColumn> { public boolean isCaseSensitive(); /** - * Gets the name of this table in the "database". + * Gets the name of this table in the "database" (e.g. as it should be used + * in SQL queries). + * + * <i> + * <p><b>Notes</b> + * The returned DB name is: + * </p> + * <ul> + * <li>non-empty/NULL</li> + * <li>non-delimited (i.e. not between double quotes),</li> + * <li>non-prefixed (i.e. no schema/catalog name)</li> + * <li>and in the EXACT case as it MUST be used.</li> + * </ul> * * @return Its DB name. */ @@ -63,6 +88,10 @@ public interface DBTable extends Iterable<DBColumn> { /** * Gets the ADQL name of the schema which contains this table. * + * <p><i><b>Warning!</b> + * Same rules as {@link #getADQLName()}. + * </i></p> + * * @return ADQL name of its schema. */ public String getADQLSchemaName(); @@ -70,6 +99,10 @@ public interface DBTable extends Iterable<DBColumn> { /** * Gets the DB name of the schema which contains this table. * + * <p><i><b>Warning!</b> + * Same rules as {@link #getDBName()}. + * </i></p> + * * @return DB name of its schema. */ public String getDBSchemaName(); @@ -77,6 +110,10 @@ public interface DBTable extends Iterable<DBColumn> { /** * Gets the ADQL name of the catalog which contains this table. * + * <p><i><b>Warning!</b> + * Same rules as {@link #getADQLName()}. + * </i></p> + * * @return ADQL name of its catalog. */ public String getADQLCatalogName(); @@ -84,6 +121,10 @@ public interface DBTable extends Iterable<DBColumn> { /** * Gets the DB name of the catalog which contains this table. * + * <p><i><b>Warning!</b> + * Same rules as {@link #getDBName()}. + * </i></p> + * * @return DB name of its catalog. */ public String getDBCatalogName(); @@ -91,36 +132,54 @@ public interface DBTable extends Iterable<DBColumn> { /** * Gets the definition of the specified column if it exists in this table. * - * @param colName Name of the column <i>(may be the ADQL or DB name depending of the second parameter)</i>. - * @param adqlName <i>true</i> means the given name is the ADQL name of the column and that the research must be done on the ADQL name of columns, - * <i>false</i> means the same thing but with the DB name. + * @param colName Name of the column <i>(may be the ADQL or DB name + * depending of the second parameter)</i>. + * @param adqlName <code>true</code> means the given name is the ADQL name + * of the column and that the research must be done on the + * ADQL name of columns, + * <code>false</code> means the same thing but with the DB + * name. * - * @return The corresponding column, or <i>null</i> if the specified column had not been found. + * @return The corresponding column, + * or NULL if the specified column had not been found. */ public DBColumn getColumn(String colName, boolean adqlName); /** - * <p>Makes a copy of this instance of {@link DBTable}, with the possibility to change the DB and ADQL names.</p> + * Makes a copy of this instance of {@link DBTable}, with the possibility + * to change the DB and ADQL names. * - * <p><b>IMPORTANT:</b> - * <b>The given DB and ADQL name may be NULL.</b> If NULL, the copy will contain exactly the same full name (DB and/or ADQL).<br/> - * <b>And they may be qualified</b> (that's to say: prefixed by the schema name or by the catalog and schema name). It means that it is possible to - * change the catalog, schema and table name in the copy.<br/> - * For instance: - * </p> + * <p><b>IMPORTANT:</b></p> + * <ul> + * <li><b>The given DB and ADQL name may be NULL.</b> If NULL, the copy + * will contain exactly the same full name (DB and/or ADQL).</li> + * <li><b>they may be qualified</b> (that's to say: prefixed by the + * schema name or by the catalog and schema name). It means that it is + * possible to change the catalog, schema and table name in the copy.</li> + * <li><b>they may be delimited</b> (that's to say: written between double + * quotes to force case sensitivity).</li> + * </ul> + * <i> + * <p><b>For instance:</b></p> * <ul> - * <li><i>.copy(null, "foo") =></i> a copy with the same full DB name, but with no ADQL catalog and schema name and with an ADQL table name equals to "foo"</li> - * <li><i>.copy("schema.table", ) =></i> a copy with the same full ADQL name, but with no DB catalog name, with a DB schema name equals to "schema" and with a DB table name equals to "table"</li> + * <li><code>.copy(null, "foo")</code> => a copy with the same full DB + * name, but with no ADQL catalog and schema name and with an ADQL + * table name equals to "foo"</li> + * <li><code>.copy("schema.table", null)</code> => a copy with the same + * full ADQL name, but with no DB catalog name, with a DB schema name + * equals to "schema" and with a DB table name equals to "table"</li> * </ul> + * </i> * * @param dbName Its new DB name. - * It may be qualified. - * It may also be NULL ; if so, the full DB name won't be different in the copy. - * @param adqlName Its new ADQL name. - * It may be qualified. - * It may also be NULL ; if so, the full DB name won't be different in the copy. + * It may be qualified and/or delimited. + * It may also be NULL ; if so, the full DB name won't be + * different in the copy. + * @param adqlName Its new ADQL name. It may be qualified and/or delimited. + * It may also be NULL ; if so, the full DB name won't be + * different in the copy. * - * @return A modified copy of this {@link DBTable}. + * @return A modified copy of this {@link DBTable}. */ public DBTable copy(final String dbName, final String adqlName); } diff --git a/src/adql/db/DBTableAlias.java b/src/adql/db/DBTableAlias.java index aef451e0a74a3ffc999cd656b1f24d913f34cf75..5e2cfd3446b1f16dc65e99180450465b4e6211a8 100644 --- a/src/adql/db/DBTableAlias.java +++ b/src/adql/db/DBTableAlias.java @@ -16,9 +16,14 @@ 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 2017 - Astronomisches Rechen Institut (ARI) + * Copyright 2017-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), + * Astronomisches Rechen Institut (ARI) */ +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + /** * This {@link DBTable} wraps another {@link DBTable} with a different ADQL and * DB name. @@ -41,11 +46,13 @@ package adql.db; * {@link #getOriginTable()}. * </i></p> * - * @author Grégory Mantelet (ARI) - * @version 1.4 (11/2017) + * @author Grégory Mantelet (CDS;ARI) + * @version 2.0 (09/2019) * @since 1.4 */ -public class DBTableAlias extends DefaultDBTable { +public final class DBTableAlias extends DBIdentifier implements DBTable { + + protected final Map<String, DBColumn> columns = new LinkedHashMap<String, DBColumn>(); /** Wrapped table. */ protected final DBTable originTable; @@ -56,13 +63,13 @@ public class DBTableAlias extends DefaultDBTable { * @param originTable The table to wrap/alias. * @param tableAlias The alias name. */ - public DBTableAlias(final DBTable originTable, final String tableAlias){ - super(null, null, tableAlias); + public DBTableAlias(final DBTable originTable, final String tableAlias) { + super(tableAlias); this.originTable = originTable; for(DBColumn col : originTable) - addColumn(col.copy(col.getDBName(), col.getADQLName(), this)); + columns.put(col.getADQLName(), col.copy(col.getDBName(), denormalize(col.getADQLName(), col.isCaseSensitive()), this)); } /** @@ -70,8 +77,51 @@ public class DBTableAlias extends DefaultDBTable { * * @return The aliased table. */ - public DBTable getOriginTable(){ + public DBTable getOriginTable() { return originTable; } + @Override + public Iterator<DBColumn> iterator() { + return columns.values().iterator(); + } + + @Override + public String getADQLSchemaName() { + return null; + } + + @Override + public String getDBSchemaName() { + return null; + } + + @Override + public String getADQLCatalogName() { + return null; + } + + @Override + public String getDBCatalogName() { + return null; + } + + @Override + public DBColumn getColumn(String colName, boolean byAdqlName) { + if (byAdqlName) + return columns.get(colName); + else { + for(DBColumn col : columns.values()) { + if (col.getDBName().equals(colName)) + return col; + } + return null; + } + } + + @Override + public DBTable copy(final String dbName, final String adqlName) { + return new DBTableAlias(originTable, adqlName); + } + } diff --git a/src/adql/db/DefaultDBColumn.java b/src/adql/db/DefaultDBColumn.java index f262b6f5d74a148acd00dcb602d96ebf262e8624..78c2f359027affd53b17cb978979733037e95782 100644 --- a/src/adql/db/DefaultDBColumn.java +++ b/src/adql/db/DefaultDBColumn.java @@ -16,156 +16,116 @@ 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,2015 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), + * Copyright 2012-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ /** * Default implementation of {@link DBColumn}. * + * <p><i><b>WARNING: constructors signature and behavior changed since v2.0!</b> + * Before v2.0, the constructors expected to have the DB names before the ADQL + * names and thus, they forced to give a DB column name ; the ADQL column name + * being optional (if not provided it was set to the DB name). + * But since v2.0, this logic is inverted: the ADQL name is mandatory (a + * {@link NullPointerException} will be thrown if NULL or empty) while the DB + * name is optional ({@link #getDBName()} will return the same as + * {@link #getADQLName()} if no DB name is specified at initialization). + * Consequently, the ADQL names are expected as first parameters. + * </i></p> + * * @author Grégory Mantelet (CDS;ARI) - * @version 1.4 (08/2015) + * @version 2.0 (09/2019) */ -public class DefaultDBColumn implements DBColumn { +public class DefaultDBColumn extends DBIdentifier implements DBColumn { - /** Name of the column in the "database". */ - protected String dbName; /** Type of the column in the "database". * <i>Note: This should be one of the types listed by the IVOA in the TAP description.</i> * @since 1.3 */ protected DBType type; + /** Table in which this column exists. */ protected DBTable table; - /** Name that this column must have in ADQL queries. */ - protected String adqlName = null; - - /** Indicate whether the ADQL column name should be considered as case - * sensitive. - * @since 2.0 */ - protected boolean columnCaseSensitive = false; /** - * Builds a default {@link DBColumn} with the given DB name and DB table. + * Builds a default {@link DBColumn} with the given ADQL name and table. + * + * <p>With this constructor: DB name = ADQL name.</p> * - * @param dbName Database column name (it will be also used for the ADQL name). - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> - * @param table DB table which contains this column. + * @param adqlName The ADQL name of this column (i.e. name to use in ADQL). + * @param table Table which contains this column. * - * @see #DefaultDBColumn(String, String, DBType, DBTable) + * @throws NullPointerException If the given ADQL name is NULL or empty. + * + * @since 2.0 */ - public DefaultDBColumn(final String dbName, final DBTable table) { - this(dbName, dbName, null, table); + public DefaultDBColumn(final String adqlName, final DBTable table) throws NullPointerException { + this(adqlName, null, null, table); } /** - * Builds a default {@link DBColumn} with the given DB name and DB table. + * Builds a default {@link DBColumn} with the given ADQL name and table. * - * @param dbName Database column name (it will be also used for the ADQL name). - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> + * @param adqlName The ADQL name of this column (i.e. name to use in ADQL). * @param type Type of the column. - * <i>Note: there is no default value. Consequently if this parameter is NULL, - * the type should be considered as unknown. It means that any comparison with - * any type will always return 'true'.</i> - * @param table DB table which contains this column. + * <i><b>Note:</b> there is no default value. Consequently + * if this parameter is NULL, the type should be considered + * as unknown. It means that any comparison with any type + * will always return <code>true</code>.</i> + * @param table Table which contains this column. * - * @see #DefaultDBColumn(String, String, DBType, DBTable) + * @throws NullPointerException If the given ADQL name is NULL or empty. * - * @since 1.3 + * @since 2.0 */ - public DefaultDBColumn(final String dbName, final DBType type, final DBTable table) { - this(dbName, dbName, type, table); + public DefaultDBColumn(final String adqlName, final DBType type, final DBTable table) throws NullPointerException { + this(adqlName, null, type, table); } /** - * Builds a default {@link DBColumn} with the given DB name, DB table and ADQL name. + * Builds a default {@link DBColumn} with the given ADQL and DB names and + * table. * - * @param dbName Database column name. - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> - * @param adqlName Column name used in ADQL queries. - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> - * @param table DB table which contains this column. + * @param adqlName The ADQL name of this column (i.e. name to use in ADQL). + * @param dbName Database name. + * <i>If NULL, {@link #getDBName()} will return the same as + * {@link #getADQLName()}.</i> + * @param table Table which contains this column. * - * @see #DefaultDBColumn(String, String, DBType, DBTable) + * @throws NullPointerException If the given ADQL name is NULL or empty. + * + * @since 2.0 */ - public DefaultDBColumn(final String dbName, final String adqlName, final DBTable table) { - this(dbName, adqlName, null, table); + public DefaultDBColumn(final String adqlName, final String dbName, final DBTable table) throws NullPointerException { + this(adqlName, dbName, null, table); } /** - * Builds a default {@link DBColumn} with the given DB name, DB table and ADQL name. - * - * @param dbName Database column name. - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> - * <b>REQUIRED parameter: it must be not NULL.</b> - * @param adqlName Column name used in ADQL queries. - * <b>Only the column name is expected. Contrary to {@link DefaultDBTable}, - * if a whole column reference is given, no split will be done.</b> - * <em>If NULL, it will be set to dbName.</em> + * Builds a default {@link DBColumn} with the given ADQL and DB names, type + * and table + * + * @param adqlName The ADQL name of this column (i.e. name to use in ADQL). + * @param dbName Database name. + * <i>If NULL, {@link #getDBName()} will return the same as + * {@link #getADQLName()}.</i> * @param type Type of the column. - * <i>Note: there is no default value. Consequently if this parameter is NULL, - * the type should be considered as unknown. It means that any comparison with - * any type will always return 'true'.</i> - * @param table DB table which contains this column. + * <i><b>Note:</b> there is no default value. Consequently + * if this parameter is NULL, the type should be considered + * as unknown. It means that any comparison with any type + * will always return <code>true</code>.</i> + * @param table Table which contains this column. * - * @since 1.3 + * @throws NullPointerException If the given ADQL name is NULL or empty. + * + * @since 2.0 */ - public DefaultDBColumn(final String dbName, final String adqlName, final DBType type, final DBTable table) { - - if (dbName == null || dbName.length() == 0) - throw new NullPointerException("Missing DB name!"); - - this.dbName = dbName; - setADQLName(adqlName); + public DefaultDBColumn(final String adqlName, final String dbName, final DBType type, final DBTable table) throws NullPointerException { + super(adqlName, dbName); this.type = type; this.table = table; } - @Override - public final String getADQLName() { - return adqlName; - } - - public final void setADQLName(String name) { - if (name != null) { - - // Remove leading and trailing space characters: - name = name.trim(); - - // Detect automatically case sensitivity: - boolean caseSensitive = DefaultDBTable.isDelimited(name); - if (caseSensitive) - name = name.substring(1, name.length() - 1).replaceAll("\"\"", "\""); - - // ONLY if the final name is NOT empty: - if (name.trim().length() > 0) { - adqlName = name; - columnCaseSensitive = caseSensitive; - } - } - } - - @Override - public boolean isCaseSensitive() { - return columnCaseSensitive; - } - - /** - * Change the case sensitivity of the ADQL column name. - * - * @param sensitive <code>true</code> to consider the current ADQL name as - * case sensitive, - * <code>false</code> otherwise. - */ - public void setCaseSensitive(final boolean sensitive) { - columnCaseSensitive = sensitive; - } - @Override public final DBType getDatatype() { return type; @@ -192,11 +152,6 @@ public class DefaultDBColumn implements DBColumn { this.type = type; } - @Override - public final String getDBName() { - return dbName; - } - @Override public final DBTable getTable() { return table; @@ -208,7 +163,7 @@ public class DefaultDBColumn implements DBColumn { @Override public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable) { - return new DefaultDBColumn(dbName, adqlName, type, dbTable); + return new DefaultDBColumn(adqlName, dbName, type, dbTable); } } diff --git a/src/adql/db/DefaultDBTable.java b/src/adql/db/DefaultDBTable.java index fb89c046c08d46a2eb5a314a158fcb607195e92a..735e824ee8ea02ba787b31b25c4013656f1da082 100644 --- a/src/adql/db/DefaultDBTable.java +++ b/src/adql/db/DefaultDBTable.java @@ -28,34 +28,41 @@ import java.util.Map; /** * Default implementation of {@link DBTable}. * + * <p><i><b>WARNING: constructors signature and behavior changed since v2.0!</b> + * Before v2.0, the constructors expected to have the DB names before the ADQL + * names and thus, they forced to give a DB table name ; the ADQL table name + * being optional (if not provided it was set to the DB name). + * But since v2.0, this logic is inverted: the ADQL name is mandatory (a + * {@link NullPointerException} will be thrown if NULL or empty) while the DB + * name is optional ({@link #getDBName()} will return the same as + * {@link #getADQLName()} if no DB name is specified at initialization). + * Consequently, the ADQL names are expected as first parameters. + * </i></p> + * * @author Grégory Mantelet (CDS;ARI) * @version 2.0 (09/2019) */ -public class DefaultDBTable implements DBTable { +public class DefaultDBTable extends DBIdentifier implements DBTable { protected String dbCatalogName = null; protected String dbSchemaName = null; - protected String dbName; protected String adqlCatalogName = null; protected String adqlSchemaName = null; - protected String adqlName = null; - - protected boolean tableCaseSensitive = false; protected Map<String, DBColumn> columns = new LinkedHashMap<String, DBColumn>(); /** - * Builds a default {@link DBTable} with the given DB name. + * Builds a default {@link DBTable} with the given <b>ADQL name</b>. * - * <p>With this constructor: ADQL name = DB name.</p> + * <p>With this constructor: DB name = ADQL name.</p> * * <p><i><b>Note:</b> * The ADQL/DB schema and catalog names are set to NULL. * </i></p> * * <p><i><b>WARNING:</b> - * The ADQL table name MUST NOT be qualified (i.e. prefixed by a schema + * The ADQL table name MUST be NON-qualified (i.e. not prefixed by a schema * and/or a catalog)! For instance, <code>t1</code> is ok, but not * <code>schema1.t1</code> or <code>cat1.schema1.t2</code> which won't be * split but instead, considered as the whole ADQL name. @@ -66,16 +73,18 @@ public class DefaultDBTable implements DBTable { * In such case, the surrounded name would be considered as case-sensitive. * </i></p> * - * @param dbName Database name (it will be also used as ADQL table name). + * @param adqlName The ADQL name of this table (i.e. name to use in ADQL). * - * @see #DefaultDBTable(String, String) + * @throws NullPointerException If the given ADQL name is NULL or empty. + * + * @since 2.0 */ - public DefaultDBTable(final String dbName) { - this(dbName, null); + public DefaultDBTable(final String adqlName) throws NullPointerException { + super(adqlName); } /** - * Builds a default {@link DBTable} with the given DB and ADQL names. + * Builds a default {@link DBTable} with the given ADQL and DB names. * * <p><i><b>Note:</b> * The ADQL/DB schema and catalog names are set to NULL. @@ -93,19 +102,22 @@ public class DefaultDBTable implements DBTable { * In such case, the surrounded name would be considered as case-sensitive. * </i></p> * - * @param dbName Database name. * @param adqlName Name used in ADQL queries. - * <i>If NULL, dbName will be used instead.</i> + * @param dbName Database name. + * <i>If NULL, {@link #getDBName()} will return the same as + * {@link #getADQLName()}.</i> + * + * @throws NullPointerException If the given ADQL name is NULL or empty. + * + * @since 2.0 */ - public DefaultDBTable(final String dbName, final String adqlName) { - if (dbName == null || dbName.trim().length() == 0) - throw new NullPointerException("Missing DB name!"); - this.dbName = dbName; - setADQLName(adqlName); + public DefaultDBTable(final String adqlName, final String dbName) throws NullPointerException { + super(adqlName, dbName); } /** - * Builds default {@link DBTable} with a DB catalog, schema and table names. + * Builds default {@link DBTable} with a ADQL catalog, schema and table + * names. * * <p><i><b>WARNING:</b> * The ADQL table name MUST NOT be qualified (i.e. prefixed by a schema @@ -119,22 +131,24 @@ public class DefaultDBTable implements DBTable { * In such case, the surrounded name would be considered as case-sensitive. * </i></p> * - * @param dbCatName Database catalog name (it will be also used as ADQL - * catalog name). - * @param dbSchemaName Database schema name (it will be also used as ADQL - * schema name). - * @param dbName Database table name (it will be also used as ADQL - * table name). - * <em>MUST NOT be NULL!</em> + * @param adqlCatName ADQL catalog name (it will be also used as DB + * catalog name). + * @param adqlSchemaName ADQL schema name (it will be also used as DB + * schema name). + * @param adqlName ADQL table name (it will be also used as DB + * table name). + * <i>MUST NOT be NULL!</i> + * + * @throws NullPointerException If the given ADQL name is NULL or empty. * - * @see #DefaultDBTable(String, String, String, String, String, String) + * @since 2.0 */ - public DefaultDBTable(final String dbCatName, final String dbSchemaName, final String dbName) { - this(dbCatName, null, dbSchemaName, null, dbName, null); + public DefaultDBTable(final String adqlCatName, final String adqlSchemaName, final String adqlName) throws NullPointerException { + this(adqlCatName, null, adqlSchemaName, null, adqlName, null); } /** - * Builds default {@link DBTable} with the DB and ADQL names for the + * Builds default {@link DBTable} with the ADQL and DB names for the * catalog, schema and table. * * <p><i><b>WARNING:</b> @@ -149,128 +163,45 @@ public class DefaultDBTable implements DBTable { * In such case, the surrounded name would be considered as case-sensitive. * </i></p> * - * @param dbCatName Database catalog name. * @param adqlCatName Catalog name used in ADQL queries. - * <em>If NULL, it will be set to dbCatName.</em> - * @param dbSchemaName Database schema name. + * @param dbCatName Database catalog name. + * <i>If NULL, it will be set to adqlCatName.</i> * @param adqlSchemaName Schema name used in ADQL queries. - * <em>If NULL, it will be set to dbSchemName.</em> - * @param dbName Database table name. - * <em>MUST NOT be NULL!</em> + * @param dbSchemaName Database schema name. + * <i>If NULL, it will be set to adqlSchemaName.</i> * @param adqlName Table name used in ADQL queries. - * <em>If NULL, it will be set to dbName.</em> + * <i>MUST NOT be NULL!</i> + * @param dbName Database table name. + * <i>If NULL, it will be set to adqlName.</i> + * + * @throws NullPointerException If the given ADQL name is NULL or empty. */ - public DefaultDBTable(final String dbCatName, final String adqlCatName, final String dbSchemaName, final String adqlSchemaName, final String dbName, final String adqlName) { - if (dbName == null || dbName.trim().length() == 0) - throw new NullPointerException("Missing DB name!"); - - this.dbName = dbName; - setADQLName(adqlName); - - this.dbSchemaName = dbSchemaName; - this.adqlSchemaName = (adqlSchemaName == null) ? dbSchemaName : adqlSchemaName; + public DefaultDBTable(final String adqlCatName, final String dbCatName, final String adqlSchemaName, final String dbSchemaName, final String adqlName, final String dbName) throws NullPointerException { + super(adqlName, dbName); - this.dbCatalogName = dbCatName; - this.adqlCatalogName = (adqlCatName == null) ? dbCatName : adqlCatName; - } + setADQLSchemaName(adqlSchemaName); + setDBSchemaName(dbSchemaName); - @Override - public final String getDBName() { - return dbName; + setADQLCatalogName(adqlCatName); + setDBCatalogName(dbCatName); } @Override public final String getDBSchemaName() { - return dbSchemaName; + return (dbSchemaName == null) ? adqlSchemaName : dbSchemaName; } - @Override - public final String getDBCatalogName() { - return dbCatalogName; + public final void setDBSchemaName(final String name) { + dbSchemaName = normalize(name); } @Override - public final String getADQLName() { - return adqlName; - } - - /** - * Change the ADQL name of this table. - * - * <p> - * The case sensitivity is automatically set. The table name will be - * considered as case sensitive if the given name is surrounded by double - * quotes (<code>"</code>). In such case, the table name is stored and then - * returned WITHOUT these double quotes. - * </p> - * - * <p><i><b>WARNING:</b> - * If the name without the double quotes (and then trimmed) is an empty - * string, the ADQL name will be set to the {@link #getDBName()} as such. - * Then the case sensitivity will be set to <code>false</code>. - * </i></p> - * - * @param name New ADQL name of this table. - */ - public void setADQLName(final String name) { - // Set the new table name (only if not NULL, otherwise use the DB name): - adqlName = (name != null) ? name : dbName; - - // Detect automatically case sensitivity: - if ((tableCaseSensitive = isDelimited(adqlName))) - adqlName = adqlName.substring(1, adqlName.length() - 1).replaceAll("\"\"", "\""); - - // If the final name is empty, no case sensitivity and use the DB name: - if (adqlName.trim().length() == 0) { - adqlName = dbName; - tableCaseSensitive = false; - } - } - - /** - * Tell whether the given identifier is delimited (i.e. within the same pair - * of double quotes - <code>"</code>). - * - * <i> - * <p>The following identifiers ARE delimited:</p> - * <ul> - * <li><code>"a"</code></li> - * <li><code>" "</code> (string with spaces ; but won't be considered as a - * valid ADQL name)</li> - * <li><code>"foo.bar"</code></li> - * <li><code>"foo"".""bar"</code> (with escaped double quotes)</li> - * <li><code>""""</code> (idem)</li> - * </ul> - * </i> - * - * <i> - * <p>The following identifiers are NOT considered as delimited:</p> - * <ul> - * <li><code>""</code> (empty string)</li> - * <li><code>"foo</code> (missing ending double quote)</li> - * <li><code>foo"</code> (missing leading double quote)</li> - * <li><code>"foo"."bar"</code> (not the same pair of double quotes)</li> - * </ul> - * </i> - * - * @param name Identifier that may be delimited. - * - * @return <code>true</code> if the given identifier is delimited, - * <code>false</code> otherwise. - * - * @since 2.0 - */ - public static final boolean isDelimited(final String name) { - return name != null && name.matches("\"(\"\"|[^\"])*\""); - } - - @Override - public boolean isCaseSensitive() { - return tableCaseSensitive; + public final String getDBCatalogName() { + return (dbCatalogName == null) ? adqlCatalogName : dbCatalogName; } - public void setCaseSensitive(final boolean sensitive) { - tableCaseSensitive = sensitive; + public final void setDBCatalogName(final String name) { + dbCatalogName = normalize(name); } @Override @@ -279,7 +210,7 @@ public class DefaultDBTable implements DBTable { } public void setADQLSchemaName(final String name) { - adqlSchemaName = (name != null) ? name : dbSchemaName; + adqlSchemaName = normalize(name); } @Override @@ -288,7 +219,7 @@ public class DefaultDBTable implements DBTable { } public void setADQLCatalogName(final String name) { - adqlName = (name != null) ? null : dbName; + adqlCatalogName = normalize(dbName); } /** @@ -423,8 +354,8 @@ public class DefaultDBTable implements DBTable { @Override public DBTable copy(String dbName, String adqlName) { - DefaultDBTable copy = new DefaultDBTable(dbCatalogName, adqlCatalogName, dbSchemaName, adqlSchemaName, dbName, adqlName); - copy.tableCaseSensitive = tableCaseSensitive; + DefaultDBTable copy = new DefaultDBTable(adqlCatalogName, dbCatalogName, adqlSchemaName, dbSchemaName, adqlName, dbName); + copy.setCaseSensitive(this.isCaseSensitive()); for(DBColumn col : this) { if (col instanceof DBCommonColumn) copy.addColumn(new DBCommonColumn((DBCommonColumn)col, col.getDBName(), col.getADQLName())); diff --git a/src/adql/db/SearchColumnList.java b/src/adql/db/SearchColumnList.java index 22c836099a21595ff36726ddd8ccb6b28097e408..c6169f88de29025f6bf199de823cba993fd329aa 100644 --- a/src/adql/db/SearchColumnList.java +++ b/src/adql/db/SearchColumnList.java @@ -2,21 +2,21 @@ 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 2012-2017 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), + * + * Copyright 2012-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ @@ -34,20 +34,24 @@ import adql.query.operand.ADQLColumn; import cds.utils.TextualSearchList; /** - * <p>A list of {@link DBColumn} elements ordered by their ADQL name in an ascending manner.</p> - * + * A list of {@link DBColumn} elements ordered by their ADQL name in an + * ascending manner. + * * <p> - * In addition to an ADQL name, {@link DBColumn} elements can be searched by specifying their table, schema and catalog. - * These last information will be used only if the ADQL column name is ambiguous, otherwise all matching elements are returned. + * In addition to an ADQL name, {@link DBColumn} elements can be searched by + * specifying their table, schema and catalog. These last information will be + * used only if the ADQL column name is ambiguous, otherwise all matching + * elements are returned. * </p> - * - * <p><i> - * <u>Note:</u> - * 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. + * + * <p><i><b>Note:</b> + * Table aliases can be listed here with their corresponding table name. + * Consequently, a table alias can be given as table name in the search + * parameters. * </i></p> - * + * * @author Grégory Mantelet (CDS;ARI) - * @version 1.4 (09/2017) + * @version 2.0 (09/2019) */ public class SearchColumnList extends TextualSearchList<DBColumn> { private static final long serialVersionUID = 1L; @@ -56,10 +60,10 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { private boolean distinct = false; /** Case-sensitive dictionary of table aliases. (tableAlias <-> TableName) */ - private final Map<String,String> tableAliases = new HashMap<String,String>(); + private final Map<String, String> tableAliases = new HashMap<String, String>(); /** Case-insensitive dictionary of table aliases. (tablealias <-> List<TableName>) */ - private final Map<String,List<String>> mapAliases = new HashMap<String,List<String>>(); + private final Map<String, List<String>> mapAliases = new HashMap<String, List<String>>(); /* ************ */ /* CONSTRUCTORS */ @@ -67,25 +71,25 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /** * Void constructor. */ - public SearchColumnList(){ + public SearchColumnList() { super(new DBColumnKeyExtractor()); } /** * Constructor by copy: all the elements of the given collection of {@link DBColumn} are copied ordered into this list. - * + * * @param collection Collection of {@link DBColumn} to copy. */ - public SearchColumnList(final Collection<DBColumn> collection){ + public SearchColumnList(final Collection<DBColumn> collection) { super(collection, new DBColumnKeyExtractor()); } /** * Constructor with the initial capacity. - * + * * @param initialCapacity Initial capacity of this list. */ - public SearchColumnList(final int initialCapacity){ + public SearchColumnList(final int initialCapacity) { super(initialCapacity, new DBColumnKeyExtractor()); } @@ -94,19 +98,19 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /* ******* */ /** * Tells whether multiple occurrences are allowed. - * + * * @return <i>true</i> means that multiple occurrences are allowed, <i>false</i> otherwise. */ - public final boolean isDistinct(){ + public final boolean isDistinct() { return distinct; } /** * Lets indicating that multiple occurrences are allowed. - * + * * @param distinct <i>true</i> means that multiple occurrences are allowed, <i>false</i> otherwise. */ - public final void setDistinct(final boolean distinct){ + public final void setDistinct(final boolean distinct) { this.distinct = distinct; } @@ -115,16 +119,16 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /* ********************** */ /** * Adds the given association between a table name and its alias in a query. - * + * * @param tableAlias Table alias. * @param tableName Table name. */ - public final void putTableAlias(final String tableAlias, final String tableName){ - if (tableAlias != null && tableName != null){ + public final void putTableAlias(final String tableAlias, final String tableName) { + if (tableAlias != null && tableName != null) { tableAliases.put(tableAlias, tableName); List<String> aliases = mapAliases.get(tableAlias.toLowerCase()); - if (aliases == null){ + if (aliases == null) { aliases = new ArrayList<String>(); mapAliases.put(tableAlias.toLowerCase(), aliases); } @@ -134,14 +138,14 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /** * Removes the given alias from this list. - * + * * @param tableAlias The table alias which must be removed. */ - public final void removeTableAlias(final String tableAlias){ + public final void removeTableAlias(final String tableAlias) { tableAliases.remove(tableAlias); List<String> aliases = mapAliases.get(tableAlias.toLowerCase()); - if (aliases != null){ + if (aliases != null) { aliases.remove(tableAlias); if (aliases.isEmpty()) mapAliases.remove(tableAlias.toLowerCase()); @@ -151,12 +155,12 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /** * Removes all table name/alias associations. */ - public final void removeAllTableAliases(){ + public final void removeAllTableAliases() { tableAliases.clear(); mapAliases.clear(); } - public final int getNbTableAliases(){ + public final int getNbTableAliases() { return tableAliases.size(); } @@ -165,71 +169,71 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /* ************** */ /** * Searches all {@link DBColumn} elements which has the given name (case insensitive). - * + * * @param columnName ADQL name of {@link DBColumn} to search for. - * + * * @return The corresponding {@link DBColumn} elements. - * + * * @see TextualSearchList#get(String) */ - public List<DBColumn> search(final String columnName){ + public List<DBColumn> search(final String columnName) { return get(columnName); } /** * Searches all {@link DBColumn} elements which have the given catalog, schema, table and column name (case insensitive). - * + * * @param catalog Catalog name. * @param schema Schema name. * @param table Table name. * @param column Column name. - * + * * @return The list of all matching {@link DBColumn} elements. - * + * * @see #search(String, String, String, String, byte) */ - public final List<DBColumn> search(final String catalog, final String schema, final String table, final String column){ + public final List<DBColumn> search(final String catalog, final String schema, final String table, final String column) { return search(catalog, schema, table, column, (byte)0); } /** * Searches all {@link DBColumn} elements corresponding to the given {@link ADQLColumn} (case insensitive). - * + * * @param column An {@link ADQLColumn}. - * + * * @return The list of all corresponding {@link DBColumn} elements. - * + * * @see #search(String, String, String, String, byte) */ - public List<DBColumn> search(final ADQLColumn column){ + public List<DBColumn> search(final ADQLColumn column) { return search(column.getCatalogName(), column.getSchemaName(), column.getTableName(), column.getColumnName(), column.getCaseSensitive()); } /** * Searches all {@link DBColumn} elements which have the given catalog, schema, table and column name, with the specified case sensitivity. - * + * * @param catalog Catalog name. * @param schema Schema name. * @param table Table name. * @param column Column name. * @param caseSensitivity Case sensitivity for each column parts (one bit by part ; 0=sensitive,1=insensitive ; see {@link IdentifierField} for more details). - * + * * @return The list of all matching {@link DBColumn} elements. - * + * * @see IdentifierField */ - public List<DBColumn> search(final String catalog, final String schema, final String table, final String column, final byte caseSensitivity){ + public List<DBColumn> search(final String catalog, final String schema, final String table, final String column, final byte caseSensitivity) { List<DBColumn> tmpResult = get(column, IdentifierField.COLUMN.isCaseSensitive(caseSensitivity)); /* WITH TABLE PREFIX */ - if (table != null){ + if (table != null) { /* 1. Figure out the table alias */ String tableName = null; List<String> aliasMatches = null; // Case sensitive => tableName is set , aliasMatches = null - if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)){ + if (IdentifierField.TABLE.isCaseSensitive(caseSensitivity)) { tableName = tableAliases.get(table); if (tableName == null) tableName = table; @@ -237,7 +241,7 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { // 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{ + else { aliasMatches = mapAliases.get(table.toLowerCase()); if (aliasMatches == null || aliasMatches.isEmpty()) tableName = table; @@ -246,7 +250,7 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /* 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){ + for(DBColumn match : tmpResult) { // Get the list of all tables covered by this column: // - only 1 if it is a normal column @@ -259,23 +263,23 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { // Test the matching with every covered tables: DBTable matchTable; - while(itMatchTables.hasNext()){ + 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 (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{ + } else { if (!matchTable.getADQLName().equalsIgnoreCase(tableName)) continue; } - }else{ // case INsensitive with at least one alias found + } else { // case INsensitive with at least one alias found boolean foundAlias = false; String temp; - for(int a = 0; !foundAlias && a < aliasMatches.size(); a++){ + for(int a = 0; !foundAlias && a < aliasMatches.size(); a++) { temp = tableAliases.get(aliasMatches.get(a)); if (temp != null) foundAlias = matchTable.getADQLName().equalsIgnoreCase(temp); @@ -285,27 +289,27 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { } // test the schema name: - if (schema != null){ + if (schema != null) { // No schema name (<=> no schema), then this table can not be a good match: if (matchTable.getADQLSchemaName() == null) continue; - if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ + if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)) { if (!matchTable.getADQLSchemaName().equals(schema)) continue; - }else{ + } else { if (!matchTable.getADQLSchemaName().equalsIgnoreCase(schema)) continue; } // test the catalog name: - if (catalog != null){ + if (catalog != null) { // No catalog name (<=> no catalog), then this table can not be a good match: if (matchTable.getADQLCatalogName() == null) continue; - if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ + if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)) { if (!matchTable.getADQLCatalogName().equals(catalog)) continue; - }else{ + } else { if (!matchTable.getADQLCatalogName().equalsIgnoreCase(catalog)) continue; } @@ -321,15 +325,15 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { } /* NO TABLE PREFIX */ - else{ + else { // Special case: the columns merged by a NATURAL JOIN or a USING may have no table reference: - if (tmpResult.size() > 1){ + 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 (ADQLJoin.isCommonColumn(tmpResult.get(i))){ + for(int i = 0; i < tmpResult.size(); 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 @@ -354,7 +358,7 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /* INHERITED METHODS */ /* ***************** */ @Override - public boolean add(final DBColumn item){ + public boolean add(final DBColumn item) { if (distinct && contains(item)) return false; else @@ -362,13 +366,13 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { } @Override - public boolean addAll(final Collection<? extends DBColumn> c){ + public boolean addAll(final Collection<? extends DBColumn> c) { boolean changed = super.addAll(c); - if (changed){ - if (c instanceof SearchColumnList){ + if (changed) { + if (c instanceof SearchColumnList) { SearchColumnList list = (SearchColumnList)c; - for(Map.Entry<String,String> entry : list.tableAliases.entrySet()) + for(Map.Entry<String, String> entry : list.tableAliases.entrySet()) putTableAlias(entry.getKey(), entry.getValue()); } } @@ -377,11 +381,11 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { } @Override - public boolean removeAll(final Collection<?> c){ + public boolean removeAll(final Collection<?> c) { boolean changed = super.removeAll(c); - if (changed){ - if (c instanceof SearchColumnList){ + if (changed) { + if (c instanceof SearchColumnList) { SearchColumnList list = (SearchColumnList)c; for(String key : list.tableAliases.keySet()) removeTableAlias(key); @@ -393,50 +397,53 @@ public class SearchColumnList extends TextualSearchList<DBColumn> { /** * Lets extracting the key to associate with a given {@link DBColumn} instance. - * + * * @author Grégory Mantelet (CDS) - * @version 09/2011 + * @version 2.0 (09/2019) */ private static class DBColumnKeyExtractor implements KeyExtractor<DBColumn> { @Override - public String getKey(DBColumn obj){ - return obj.getADQLName(); + public String getKey(DBColumn obj) { + if (obj.isCaseSensitive()) + return obj.getADQLName(); + else + return obj.getADQLName().toLowerCase(); } } /** * Iterator that iterates over only one item, given in the constructor. - * + * * @param <E> Type of the item that this Iterator must return. - * + * * @author Grégory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de * @version 1.2 (11/2013) * @since 1.2 */ - private static class SingleIterator< E > implements Iterator<E> { + private static class SingleIterator<E> implements Iterator<E> { private final E item; private boolean done = false; - public SingleIterator(final E singleItem){ + public SingleIterator(final E singleItem) { item = singleItem; } @Override - public boolean hasNext(){ + public boolean hasNext() { return !done; } @Override - public E next(){ - if (!done){ + public E next() { + if (!done) { done = true; return item; - }else + } else throw new NoSuchElementException(); } @Override - public void remove(){ + public void remove() { throw new UnsupportedOperationException(); } } diff --git a/src/adql/db/SearchTableApi.java b/src/adql/db/SearchTableApi.java index c7d047abd30d3e298edcecf946e2d3a4c8a22374..f92a76a4a99879931d458a3be050f12a1a30c9b0 100644 --- a/src/adql/db/SearchTableApi.java +++ b/src/adql/db/SearchTableApi.java @@ -2,21 +2,22 @@ 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 2017 - Astronomisches Rechen Institut (ARI) + * + * Copyright 2017-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) + * Astronomisches Rechen Institut (ARI) */ import java.util.List; @@ -26,22 +27,36 @@ import adql.query.from.ADQLTable; /** * Simple interface about a class which allows to search for a specified * {@link ADQLTable}. - * - * @author Grégory Mantelet (ARI) - * @version 1.4 (09/2017) + * + * @author Grégory Mantelet (ARI;CDS) + * @version 2.0 (09/2019) * @since 1.4 - * + * * @see SearchTableList */ public interface SearchTableApi { /** * Searches all {@link DBTable} elements corresponding to the given {@link ADQLTable} (case insensitive). - * + * * @param table An {@link ADQLTable}. - * + * * @return The list of all corresponding {@link DBTable} elements. */ public List<DBTable> search(final ADQLTable table); + /** + * Adds the given object at the end of this list. + * + * @param obj Object to add (different from NULL). + * + * @throws NullPointerException If the given object or its extracted key is <code>null</code>. + * @throws IllegalArgumentException If the extracted key is already used by another object in this list. + * + * @see java.util.ArrayList#add(java.lang.Object) + */ + public boolean add(final DBTable item); + + public SearchTableApi getCopy(); + } \ No newline at end of file diff --git a/src/adql/db/SearchTableList.java b/src/adql/db/SearchTableList.java index 8522d24bf851d0766ce6e932c8537cd3777219f2..80cc7afcfcf7a94325f42f42c33ad5fb6c7a4364 100644 --- a/src/adql/db/SearchTableList.java +++ b/src/adql/db/SearchTableList.java @@ -2,21 +2,21 @@ 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 2012-2017 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) + * + * Copyright 2012-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) * Astronomisches Rechen Institut (ARI) */ @@ -29,15 +29,18 @@ import adql.query.from.ADQLTable; import cds.utils.TextualSearchList; /** - * <p>A list of {@link DBTable} elements ordered by their ADQL name in an ascending manner.</p> - * + * A list of {@link DBTable} elements ordered by their ADQL name in an ascending + * manner. + * * <p> - * In addition to an ADQL name, {@link DBTable} elements can be searched by specifying their schema and catalog. - * These last information will be used only if the ADQL table name is ambiguous, otherwise all matching elements are returned. + * In addition to an ADQL name, {@link DBTable} elements can be searched by + * specifying their schema and catalog. These last information will be used + * only if the ADQL table name is ambiguous, otherwise all matching elements + * are returned. * </p> - * + * * @author Grégory Mantelet (CDS;ARI) - * @version 1.4 (09/2017) + * @version 2.0 (09/2019) */ public class SearchTableList extends TextualSearchList<DBTable> implements SearchTableApi { private static final long serialVersionUID = 1L; @@ -51,25 +54,25 @@ public class SearchTableList extends TextualSearchList<DBTable> implements Searc /** * Void constructor. */ - public SearchTableList(){ + public SearchTableList() { super(new DBTableKeyExtractor()); } /** * Constructor by copy: all the elements of the given collection of {@link DBTable} are copied ordered into this list. - * + * * @param collection Collection of {@link DBTable} to copy. */ - public SearchTableList(final Collection<? extends DBTable> collection){ + public SearchTableList(final Collection<? extends DBTable> collection) { super(collection, new DBTableKeyExtractor()); } /** * Constructor with the initial capacity. - * + * * @param initialCapacity Initial capacity of this list. */ - public SearchTableList(final int initialCapacity){ + public SearchTableList(final int initialCapacity) { super(initialCapacity, new DBTableKeyExtractor()); } @@ -78,105 +81,101 @@ public class SearchTableList extends TextualSearchList<DBTable> implements Searc /* ******* */ /** * Tells whether multiple occurrences are allowed. - * + * * @return <i>true</i> means that multiple occurrences are allowed, <i>false</i> otherwise. */ - public final boolean isDistinct(){ + public final boolean isDistinct() { return distinct; } /** * Lets indicating that multiple occurrences are allowed. - * + * * @param distinct <i>true</i> means that multiple occurrences are allowed, <i>false</i> otherwise. */ - public final void setDistinct(final boolean distinct){ + public final void setDistinct(final boolean distinct) { this.distinct = distinct; } + @Override + public SearchTableApi getCopy() { + return new SearchTableList(this); + } + /* ************** */ /* SEARCH METHODS */ /* ************** */ /** * Searches all {@link DBTable} elements which has the given name (case insensitive). - * + * * @param tableName ADQL name of {@link DBTable} to search for. - * + * * @return The corresponding {@link DBTable} elements. - * + * * @see TextualSearchList#get(String) */ - public List<DBTable> search(final String tableName){ + public List<DBTable> search(final String tableName) { return get(tableName); } /** * Searches all {@link DBTable} elements which have the given catalog, schema, and table name (case insensitive). - * + * * @param catalog Catalog name. * @param schema Schema name. * @param table Table name. - * + * * @return The list of all matching {@link DBTable} elements. - * + * * @see #search(String, String, String, byte) */ - public final List<DBTable> search(final String catalog, final String schema, final String table){ + public final List<DBTable> search(final String catalog, final String schema, final String table) { return search(catalog, schema, table, (byte)0); } - /** - * Searches all {@link DBTable} elements corresponding to the given {@link ADQLTable} (case insensitive). - * - * @param table An {@link ADQLTable}. - * - * @return The list of all corresponding {@link DBTable} elements. - * - * @see #search(String, String, String, byte) - */ @Override - public List<DBTable> search(final ADQLTable table){ + public List<DBTable> search(final ADQLTable table) { return search(table.getCatalogName(), table.getSchemaName(), table.getTableName(), table.getCaseSensitive()); } /** * Searches all {@link DBTable} elements which have the given catalog, schema, and table name, with the specified case sensitivity. - * + * * @param catalog Catalog name. * @param schema Schema name. * @param table Table name. * @param caseSensitivity Case sensitivity for each table parts (one bit by part ; 0=sensitive,1=insensitive ; see {@link IdentifierField} for more details). - * + * * @return The list of all matching {@link DBTable} elements. - * + * * @see IdentifierField */ - public List<DBTable> search(final String catalog, final String schema, final String table, final byte caseSensitivity){ + public List<DBTable> search(final String catalog, final String schema, final String table, final byte caseSensitivity) { List<DBTable> tmpResult = get(table, IdentifierField.TABLE.isCaseSensitive(caseSensitivity)); - if (schema != null){ + if (schema != null) { List<DBTable> result = new ArrayList<DBTable>(); - for(DBTable match : tmpResult){ + for(DBTable match : tmpResult) { // No schema name (<=> no schema), then this table can not be a good match: if (match.getADQLSchemaName() == null) continue; - if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)){ + if (IdentifierField.SCHEMA.isCaseSensitive(caseSensitivity)) { if (!match.getADQLSchemaName().equals(schema)) continue; - }else{ + } else { if (!match.getADQLSchemaName().equalsIgnoreCase(schema)) continue; } - if (catalog != null){ + if (catalog != null) { // No catalog name (<=> no catalog), then this table can not be a good match: if (match.getADQLCatalogName() == null) continue; - if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)){ + if (IdentifierField.CATALOG.isCaseSensitive(caseSensitivity)) { if (!match.getADQLCatalogName().equals(catalog)) continue; - }else{ + } else { if (!match.getADQLCatalogName().equalsIgnoreCase(catalog)) continue; } @@ -186,7 +185,7 @@ public class SearchTableList extends TextualSearchList<DBTable> implements Searc } return result; - }else + } else return tmpResult; } @@ -194,7 +193,7 @@ public class SearchTableList extends TextualSearchList<DBTable> implements Searc /* INHERITED METHODS */ /* ***************** */ @Override - public boolean add(final DBTable item){ + public boolean add(final DBTable item) { if (distinct && contains(item)) return false; else @@ -203,14 +202,17 @@ public class SearchTableList extends TextualSearchList<DBTable> implements Searc /** * Lets extracting a key to associate with a given {@link DBTable} instance. - * + * * @author Grégory Mantelet (CDS) - * @version 09/2011 + * @version 2.0 (09/2019) */ private static class DBTableKeyExtractor implements KeyExtractor<DBTable> { @Override - public String getKey(DBTable obj){ - return obj.getADQLName(); + public String getKey(DBTable obj) { + if (obj.isCaseSensitive()) + return obj.getADQLName(); + else + return obj.getADQLName().toLowerCase(); } } diff --git a/src/adql/parser/ADQLQueryFactory.java b/src/adql/parser/ADQLQueryFactory.java index e6558e66cdfc57526aa691244c53de881ee09c95..4305fdee0f9c178e52b9644962b0e9f838b48730 100644 --- a/src/adql/parser/ADQLQueryFactory.java +++ b/src/adql/parser/ADQLQueryFactory.java @@ -33,6 +33,7 @@ import adql.query.ColumnReference; import adql.query.IdentifierField; import adql.query.SelectItem; import adql.query.TextPosition; +import adql.query.WithItem; import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -207,6 +208,13 @@ public class ADQLQueryFactory { } } + /** @since 2.0 */ + public WithItem createWithItem(final IdentifierItem queryLabel, final ADQLQuery query, final Collection<ADQLColumn> colLabels) throws Exception { + WithItem item = new WithItem(queryLabel.identifier, query, colLabels); + item.setLabelCaseSensitive(queryLabel.caseSensitivity); + return item; + } + public SelectItem createSelectItem(ADQLOperand operand, String alias) throws Exception { return new SelectItem(operand, alias); } diff --git a/src/adql/parser/feature/FeatureSet.java b/src/adql/parser/feature/FeatureSet.java index e01d6e319175a86d8fd43c71909788884d35d674..da3073f0393036e003ee849a954a827b732453b5 100644 --- a/src/adql/parser/feature/FeatureSet.java +++ b/src/adql/parser/feature/FeatureSet.java @@ -29,6 +29,7 @@ import java.util.Set; import adql.db.FunctionDef; import adql.query.ClauseOffset; +import adql.query.WithItem; import adql.query.constraint.ComparisonOperator; import adql.query.operand.BitNotOperand; import adql.query.operand.OperationType; @@ -583,8 +584,6 @@ public class FeatureSet implements Iterable<LanguageFeature> { public static final LanguageFeature EXCEPT = new LanguageFeature(FeatureType.ADQL_SETS, "EXCEPT"); // TODO EXCEPT public static final LanguageFeature INTERSECT = new LanguageFeature(FeatureType.ADQL_SETS, "INTERSECT"); // TODO INTERSECT - public static final LanguageFeature WITH = new LanguageFeature(FeatureType.ADQL_COMMON_TABLE, "WITH"); // TODO WITH - public static final LanguageFeature CAST = new LanguageFeature(FeatureType.ADQL_TYPE, "CAST"); // TODO CAST*/ /** All standard features available. @@ -597,7 +596,7 @@ public class FeatureSet implements Iterable<LanguageFeature> { * <p><i><b>Important note:</b> * All of them must be optional and must have a type. * </i></p> */ - static LanguageFeature[] availableFeatures = new LanguageFeature[]{ InUnitFunction.FEATURE, BitNotOperand.FEATURE, OperationType.BIT_AND.getFeatureDescription(), OperationType.BIT_OR.getFeatureDescription(), OperationType.BIT_XOR.getFeatureDescription(), ClauseOffset.FEATURE, ComparisonOperator.ILIKE.getFeatureDescription(), LowerFunction.FEATURE, AreaFunction.FEATURE, BoxFunction.FEATURE, CentroidFunction.FEATURE, CircleFunction.FEATURE, ContainsFunction.FEATURE, ExtractCoord.FEATURE_COORD1, ExtractCoord.FEATURE_COORD2, ExtractCoordSys.FEATURE, DistanceFunction.FEATURE, IntersectsFunction.FEATURE, PointFunction.FEATURE, PolygonFunction.FEATURE, RegionFunction.FEATURE }; + static LanguageFeature[] availableFeatures = new LanguageFeature[]{ WithItem.FEATURE, InUnitFunction.FEATURE, BitNotOperand.FEATURE, OperationType.BIT_AND.getFeatureDescription(), OperationType.BIT_OR.getFeatureDescription(), OperationType.BIT_XOR.getFeatureDescription(), ClauseOffset.FEATURE, ComparisonOperator.ILIKE.getFeatureDescription(), LowerFunction.FEATURE, AreaFunction.FEATURE, BoxFunction.FEATURE, CentroidFunction.FEATURE, CircleFunction.FEATURE, ContainsFunction.FEATURE, ExtractCoord.FEATURE_COORD1, ExtractCoord.FEATURE_COORD2, ExtractCoordSys.FEATURE, DistanceFunction.FEATURE, IntersectsFunction.FEATURE, PointFunction.FEATURE, PolygonFunction.FEATURE, RegionFunction.FEATURE }; /** * List all available language features. diff --git a/src/adql/parser/grammar/adqlGrammar200.jj b/src/adql/parser/grammar/adqlGrammar200.jj index d1e6b4a78806e6e59c7355ceeb42749b5340c335..360c11cdf5c36b28a849910f8ae7c728dbf8a695 100644 --- a/src/adql/parser/grammar/adqlGrammar200.jj +++ b/src/adql/parser/grammar/adqlGrammar200.jj @@ -29,7 +29,7 @@ * If the syntax is not conform to the ADQL definition a TokenMgrError or a * ParseException is thrown. * -* Author: Grégory Mantelet (CDS) +* Author: Grégory Mantelet (CDS) * Version: 2.0 (10/2020) */ @@ -102,11 +102,11 @@ import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; * </i></p> * * @see ADQLParser -* -* @author Grégory Mantelet (CDS) + * + * @author Grégory Mantelet (CDS) * @version 2.0 (10/2020) -* @since 2.0 -*/ + * @since 2.0 + */ public class ADQLGrammar200 extends ADQLGrammarBase { /* ********************************************************************** @@ -482,7 +482,7 @@ void Select(): {ClauseSelect select = query.getSelect(); SelectItem item=null; T } } -SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true); IdentifierItem id = null, label = null; ADQLOperand op = null; SelectItem item; Token starToken;} { +SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true); IdentifierItem firstID = null, id = null, label = null; ADQLOperand op = null; SelectItem item; Token starToken;} { ( ( starToken=<ASTERISK> { @@ -493,7 +493,7 @@ SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true ) |LOOKAHEAD(7) ( - id=Identifier() <DOT> { identifiers.append(id); } + id=Identifier() <DOT> { identifiers.append(id); firstID = id; } ( id=Identifier() <DOT> { identifiers.append(id); } ( @@ -503,9 +503,11 @@ SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true starToken=<ASTERISK> { try{ - item = new SelectAllColumns( queryFactory.createTable(identifiers, null) ); - TextPosition firstPos = identifiers.get(0).position; - item.setPosition(new TextPosition(firstPos.beginLine, firstPos.beginColumn, starToken.endLine, (starToken.endColumn < 0) ? -1 : (starToken.endColumn + 1))); + ADQLTable table = queryFactory.createTable(identifiers, null); + table.setPosition(new TextPosition(firstID.position, id.position)); + + item = new SelectAllColumns(table); + item.setPosition(new TextPosition(firstID.position, new TextPosition(starToken))); return item; }catch(Exception ex) { throw generateParseException(ex); diff --git a/src/adql/parser/grammar/adqlGrammar201.jj b/src/adql/parser/grammar/adqlGrammar201.jj index 7ee135a2f5a80d3c0ad37017e8bbae4b48f494f1..ffceab0ffe6ad8cc35080dd7b12243db7c23cf95 100644 --- a/src/adql/parser/grammar/adqlGrammar201.jj +++ b/src/adql/parser/grammar/adqlGrammar201.jj @@ -31,7 +31,7 @@ * ParseException is thrown. * * Author: Grégory Mantelet (CDS) -* Version: 2.0 (08/2019) +* Version: 2.0 (09/2019) */ /* ########### */ @@ -107,7 +107,7 @@ import adql.query.operand.function.geometry.GeometryFunction.GeometryValue; * @see ADQLParser * * @author Grégory Mantelet (CDS) - * @version 2.0 (08/2019) + * @version 2.0 (09/2019) * @since 2.0 */ public class ADQLGrammar201 extends ADQLGrammarBase { @@ -206,7 +206,7 @@ SKIP : { < " " | "\t" | "\n" | "\r" | "\r\n" > } /* ************************************************************************** */ TOKEN : { - < SQL_RESERVED_WORD: ("ABSOLUTE"|"ACTION"|"ADD"|"ALLOCATE"|"ALTER"|"ANY"|"ARE"|"ASSERTION"|"AT"|"AUTHORIZATION"|"BEGIN"|"BIT"|"BIT_LENGTH"|"BOTH"|"CASCADE"|"CASCADED"|"CASE"|"CAST"|"CATALOG"|"CHAR"|"CHARACTER"|"CHAR_LENGTH"|"CHARACTER_LENGTH"|"CHECK"|"CLOSE"|"COALESCE"|"COLLATE"|"COLLATION"|"COLUMN"|"COMMIT"|"CONNECT"|"CONNECTION"|"CONSTRAINT"|"CONSTRAINTS"|"CONTINUE"|"CONVERT"|"CORRESPONDING"|"CREATE"|"CURRENT"|"CURRENT_DATE"|"CURRENT_TIME"|"CURRENT_TIMESTAMP"|"CURRENT_USER"|"CURSOR"|"DATE"|"DAY"|"DEALLOCATE"|"DECIMAL"|"DECLARE"|"DEFAULT"|"DEFERRABLE"|"DEFERRED"|"DELETE"|"DESCRIBE"|"DESCRIPTOR"|"DIAGNOSTICS"|"DISCONNECT"|"DOMAIN"|"DOUBLE"|"DROP"|"ELSE"|"END"|"END-EXEC"|"ESCAPE"|"EXCEPT"|"EXCEPTION"|"EXEC"|"EXECUTE"|"EXTERNAL"|"EXTRACT"|"FALSE"|"FETCH"|"FIRST"|"FLOAT"|"FOR"|"FOREIGN"|"FOUND"|"GET"|"GLOBAL"|"GO"|"GOTO"|"GRANT"|"HOUR"|"IDENTITY"|"IMMEDIATE"|"INDICATOR"|"INITIALLY"|"INPUT"|"INSENSITIVE"|"INSERT"|"INT"|"INTEGER"|"INTERSECT"|"INTERVAL"|"INTO"|"ISOLATION"|"KEY"|"LANGUAGE"|"LAST"|"LEADING"|"LEVEL"|"LOCAL"|"MATCH"|"MINUTE"|"MODULE"|"MONTH"|"NAMES"|"NATIONAL"|"NCHAR"|"NEXT"|"NO"|"NULLIF"|"NUMERIC"|"OCTET_LENGTH"|"OF"|"ONLY"|"OPEN"|"OPTION"|"OUTPUT"|"OVERLAPS"|"PAD"|"PARTIAL"|"POSITION"|"PRECISION"|"PREPARE"|"PRESERVE"|"PRIMARY"|"PRIOR"|"PRIVILEGES"|"PROCEDURE"|"PUBLIC"|"READ"|"REAL"|"REFERENCES"|"RELATIVE"|"RESTRICT"|"REVOKE"|"ROLLBACK"|"ROWS"|"SCHEMA"|"SCROLL"|"SECOND"|"SECTION"|"SESSION"|"SESSION_USER"|"SET"|"SIZE"|"SMALLINT"|"SOME"|"SPACE"|"SQL"|"SQLCODE"|"SQLERROR"|"SQLSTATE"|"SUBSTRING"|"SYSTEM_USER"|"TABLE"|"TEMPORARY"|"THEN"|"TIME"|"TIMESTAMP"|"TIMEZONE_HOUR"|"TIMEZONE_MINUTE"|"TO"|"TRAILING"|"TRANSACTION"|"TRANSLATE"|"TRANSLATION"|"TRIM"|"TRUE"|"UNION"|"UNIQUE"|"UNKNOWN"|"UPDATE"|"UPPER"|"USAGE"|"USER"|"VALUE"|"VALUES"|"VARCHAR"|"VARYING"|"VIEW"|"WHEN"|"WHENEVER"|"WITH"|"WORK"|"WRITE"|"YEAR"|"ZONE") > + < SQL_RESERVED_WORD: ("ABSOLUTE"|"ACTION"|"ADD"|"ALLOCATE"|"ALTER"|"ANY"|"ARE"|"ASSERTION"|"AT"|"AUTHORIZATION"|"BEGIN"|"BIT"|"BIT_LENGTH"|"BOTH"|"CASCADE"|"CASCADED"|"CASE"|"CAST"|"CATALOG"|"CHAR"|"CHARACTER"|"CHAR_LENGTH"|"CHARACTER_LENGTH"|"CHECK"|"CLOSE"|"COALESCE"|"COLLATE"|"COLLATION"|"COLUMN"|"COMMIT"|"CONNECT"|"CONNECTION"|"CONSTRAINT"|"CONSTRAINTS"|"CONTINUE"|"CONVERT"|"CORRESPONDING"|"CREATE"|"CURRENT"|"CURRENT_DATE"|"CURRENT_TIME"|"CURRENT_TIMESTAMP"|"CURRENT_USER"|"CURSOR"|"DATE"|"DAY"|"DEALLOCATE"|"DECIMAL"|"DECLARE"|"DEFAULT"|"DEFERRABLE"|"DEFERRED"|"DELETE"|"DESCRIBE"|"DESCRIPTOR"|"DIAGNOSTICS"|"DISCONNECT"|"DOMAIN"|"DOUBLE"|"DROP"|"ELSE"|"END"|"END-EXEC"|"ESCAPE"|"EXCEPT"|"EXCEPTION"|"EXEC"|"EXECUTE"|"EXTERNAL"|"EXTRACT"|"FALSE"|"FETCH"|"FIRST"|"FLOAT"|"FOR"|"FOREIGN"|"FOUND"|"GET"|"GLOBAL"|"GO"|"GOTO"|"GRANT"|"HOUR"|"IDENTITY"|"IMMEDIATE"|"INDICATOR"|"INITIALLY"|"INPUT"|"INSENSITIVE"|"INSERT"|"INT"|"INTEGER"|"INTERSECT"|"INTERVAL"|"INTO"|"ISOLATION"|"KEY"|"LANGUAGE"|"LAST"|"LEADING"|"LEVEL"|"LOCAL"|"MATCH"|"MINUTE"|"MODULE"|"MONTH"|"NAMES"|"NATIONAL"|"NCHAR"|"NEXT"|"NO"|"NULLIF"|"NUMERIC"|"OCTET_LENGTH"|"OF"|"ONLY"|"OPEN"|"OPTION"|"OUTPUT"|"OVERLAPS"|"PAD"|"PARTIAL"|"POSITION"|"PRECISION"|"PREPARE"|"PRESERVE"|"PRIMARY"|"PRIOR"|"PRIVILEGES"|"PROCEDURE"|"PUBLIC"|"READ"|"REAL"|"REFERENCES"|"RELATIVE"|"RESTRICT"|"REVOKE"|"ROLLBACK"|"ROWS"|"SCHEMA"|"SCROLL"|"SECOND"|"SECTION"|"SESSION"|"SESSION_USER"|"SET"|"SIZE"|"SMALLINT"|"SOME"|"SPACE"|"SQL"|"SQLCODE"|"SQLERROR"|"SQLSTATE"|"SUBSTRING"|"SYSTEM_USER"|"TABLE"|"TEMPORARY"|"THEN"|"TIME"|"TIMESTAMP"|"TIMEZONE_HOUR"|"TIMEZONE_MINUTE"|"TO"|"TRAILING"|"TRANSACTION"|"TRANSLATE"|"TRANSLATION"|"TRIM"|"TRUE"|"UNION"|"UNIQUE"|"UNKNOWN"|"UPDATE"|"UPPER"|"USAGE"|"USER"|"VALUE"|"VALUES"|"VARCHAR"|"VARYING"|"VIEW"|"WHEN"|"WHENEVER"|"WORK"|"WRITE"|"YEAR"|"ZONE") > { matchedToken.sqlReserved = true; } } @@ -305,7 +305,8 @@ TOKEN : { /* Other clauses' tokens */ /* ********************* */ TOKEN : { - < BY: "BY" > { matchedToken.adqlReserved = true; } + < WITH: "WITH" > { matchedToken.adqlReserved = true; } +| < BY: "BY" > { matchedToken.adqlReserved = true; } | < GROUP: "GROUP" > { matchedToken.adqlReserved = true; } | < HAVING: "HAVING" > { matchedToken.adqlReserved = true; } | < ORDER: "ORDER" > { matchedToken.adqlReserved = true; } @@ -452,6 +453,7 @@ ADQLQuery QueryExpression(): {TextPosition endPos = null;} { throw generateParseException(ex); } } + [With()] Select() From() {endPos = query.getFrom().getPosition();} [Where() {endPos = query.getWhere().getPosition();}] @@ -461,7 +463,10 @@ ADQLQuery QueryExpression(): {TextPosition endPos = null;} { [Offset() {endPos = new TextPosition(token);}] { // set the position of the query: - query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos)); + if (query.getWith().isEmpty()) + query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos)); + else + query.setPosition(new TextPosition(query.getWith().getPosition(), endPos)); // get the previous query (!= null if the current query is a sub-query): ADQLQuery previousQuery = stackQuery.pop(); @@ -482,6 +487,33 @@ ADQLQuery SubQueryExpression(): {ADQLQuery q = null; Token start, end;} { } } +ClauseADQL<WithItem> With(): { ClauseADQL<WithItem> withClause = query.getWith(); WithItem withItem; Token start,end; IdentifierItem id, colId; ArrayList<ADQLColumn> colLabels = new ArrayList<ADQLColumn>(10); ADQLQuery query; } { + try { + start=<WITH> id=Identifier() [<LEFT_PAR> colId=Identifier() { colLabels.add(queryFactory.createColumn(colId)); } (<COMMA> colId=Identifier() { colLabels.add(queryFactory.createColumn(colId)); })* <RIGHT_PAR>] <AS> <LEFT_PAR> query=QueryExpression() end=<RIGHT_PAR> + { + withItem = queryFactory.createWithItem(id, query, colLabels); + withItem.setPosition(new TextPosition(id.position, new TextPosition(end))); + withClause.add(withItem); + colLabels.clear(); + } + ( + <COMMA> id=Identifier() [<LEFT_PAR> colId=Identifier() { colLabels.add(queryFactory.createColumn(colId)); } (<COMMA> colId=Identifier() { colLabels.add(queryFactory.createColumn(colId)); })* <RIGHT_PAR>] <AS> <LEFT_PAR> query=QueryExpression() end=<RIGHT_PAR> + { + withItem = queryFactory.createWithItem(id, query, colLabels); + withItem.setPosition(new TextPosition(id.position, new TextPosition(end))); + withClause.add(withItem); + colLabels.clear(); + } + )* + { + withClause.setPosition(new TextPosition(start, end)); + return withClause; + } + }catch(Exception ex) { + throw generateParseException(ex); + } +} + void Select(): {ClauseSelect select = query.getSelect(); SelectItem item=null; Token start,t = null;} { start=<SELECT> [t=<QUANTIFIER> {select.setDistinctColumns(t.image.equalsIgnoreCase("DISTINCT"));}] @@ -503,7 +535,7 @@ void Select(): {ClauseSelect select = query.getSelect(); SelectItem item=null; T } } -SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true); IdentifierItem id = null, label = null; ADQLOperand op = null; SelectItem item; Token starToken;} { +SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true); IdentifierItem firstID = null, id = null, label = null; ADQLOperand op = null; SelectItem item; Token starToken;} { ( ( starToken=<ASTERISK> { @@ -514,7 +546,7 @@ SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true ) |LOOKAHEAD(7) ( - id=Identifier() <DOT> { identifiers.append(id); } + id=Identifier() <DOT> { identifiers.append(id); firstID = id; } ( id=Identifier() <DOT> { identifiers.append(id); } ( @@ -524,9 +556,11 @@ SelectItem SelectItem(): {IdentifierItems identifiers = new IdentifierItems(true starToken=<ASTERISK> { try{ - item = new SelectAllColumns( queryFactory.createTable(identifiers, null) ); - TextPosition firstPos = identifiers.get(0).position; - item.setPosition(new TextPosition(firstPos.beginLine, firstPos.beginColumn, starToken.endLine, (starToken.endColumn < 0) ? -1 : (starToken.endColumn + 1))); + ADQLTable table = queryFactory.createTable(identifiers, null); + table.setPosition(new TextPosition(firstID.position, id.position)); + + item = new SelectAllColumns(table); + item.setPosition(new TextPosition(firstID.position, new TextPosition(starToken))); return item; }catch(Exception ex) { throw generateParseException(ex); diff --git a/src/adql/query/ADQLList.java b/src/adql/query/ADQLList.java index 26cc05c8beccec3aa9a6374d48222680fa982cfb..465bb7a905321cc4ab1a5bf4114a21ffe5002882 100644 --- a/src/adql/query/ADQLList.java +++ b/src/adql/query/ADQLList.java @@ -34,7 +34,7 @@ import adql.parser.feature.LanguageFeature; * </p> * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (07/2019) + * @version 2.0 (08/2019) * * @see ClauseADQL * @see ClauseConstraints @@ -63,7 +63,27 @@ public abstract class ADQLList<T extends ADQLObject> implements ADQLObject, Iter * * @param name Prefix/Name of this list. */ - protected ADQLList(String name) { + protected ADQLList(final String name) { + this(name, null); + } + + /** + * Builds an ADQLList with only its name and its corresponding language + * feature. + * + * <p>The given name will always prefix the list.</p> + * + * <p> + * The language feature is optional. If omitted, a default non-optional + * one will be created using the list's name. + * </p> + * + * @param name Prefix/Name of this list. + * @param implementedFeature Language Feature implemented by this list. + * + * @since 2.0 + */ + protected ADQLList(String name, final LanguageFeature implementedFeature) { if (name != null) { name = name.trim(); if (name.length() == 0) @@ -72,7 +92,7 @@ public abstract class ADQLList<T extends ADQLObject> implements ADQLObject, Iter this.name = name; - this.FEATURE = new LanguageFeature(null, "CLAUSE" + (this.name == null ? "" : "_" + this.name), false, "An ADQL clause (e.g. SELECT, FROM, ...)."); + this.FEATURE = (implementedFeature != null) ? implementedFeature : new LanguageFeature(null, "CLAUSE" + (this.name == null ? "" : "_" + this.name), false, "An ADQL clause (e.g. SELECT, FROM, ...)."); } /** @@ -85,7 +105,7 @@ public abstract class ADQLList<T extends ADQLObject> implements ADQLObject, Iter */ @SuppressWarnings("unchecked") protected ADQLList(ADQLList<T> toCopy) throws Exception { - this(toCopy.getName()); + this(toCopy.getName(), toCopy.getFeatureDescription()); for(T obj : toCopy) add((T)obj.getCopy()); position = (toCopy.position != null) ? new TextPosition(toCopy.position) : null; diff --git a/src/adql/query/ADQLQuery.java b/src/adql/query/ADQLQuery.java index 303b75f5714858dec2c04a6f0049444b764640be..02e92628f56fd422b66e09c937cb30512301ee96 100644 --- a/src/adql/query/ADQLQuery.java +++ b/src/adql/query/ADQLQuery.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; import adql.db.DBColumn; +import adql.db.DBIdentifier; import adql.db.DBType; import adql.db.DBType.DBDatatype; import adql.db.DefaultDBColumn; @@ -64,6 +65,10 @@ public class ADQLQuery implements ADQLObject { * @since 2.0 */ private final ADQLVersion adqlVersion; + /** The ADQL clause WITH. + * @since 2.0 */ + private ClauseADQL<WithItem> with; + /** The ADQL clause SELECT. */ private ClauseSelect select; @@ -110,6 +115,7 @@ public class ADQLQuery implements ADQLObject { */ public ADQLQuery(final ADQLVersion version) { this.adqlVersion = (version == null ? ADQLParser.DEFAULT_VERSION : version); + with = new ClauseADQL<WithItem>("WITH"); select = new ClauseSelect(); from = null; where = new ClauseConstraints("WHERE"); @@ -129,6 +135,7 @@ public class ADQLQuery implements ADQLObject { @SuppressWarnings("unchecked") public ADQLQuery(ADQLQuery toCopy) throws Exception { adqlVersion = toCopy.adqlVersion; + with = (ClauseADQL<WithItem>)toCopy.with.getCopy(); select = (ClauseSelect)toCopy.select.getCopy(); from = (FromContent)toCopy.from.getCopy(); where = (ClauseConstraints)toCopy.where.getCopy(); @@ -157,6 +164,8 @@ public class ADQLQuery implements ADQLObject { * Clear all the clauses. */ public void reset() { + with.clear(); + select.clear(); select.setDistinctColumns(false); select.setNoLimit(); @@ -170,6 +179,38 @@ public class ADQLQuery implements ADQLObject { position = null; } + /** + * Gets the WITH clause of this query. + * + * @return Its WITH clause. + * + * @since 2.0 + */ + public final ClauseADQL<WithItem> getWith() { + return with; + } + + /** + * Replaces its WITH clause by the given one. + * + * <p><i><b>Note:</b> + * The position of the query is erased. + * </i></p> + * + * @param newWith The new WITH clause. + * + * @throws NullPointerException If the given WITH clause is NULL. + * + * @since 2.0 + */ + public void setWith(ClauseADQL<WithItem> newWith) throws NullPointerException { + if (newWith == null) + throw new NullPointerException("Impossible to replace the WITH clause of a query by NULL!"); + else + with = newWith; + position = null; + } + /** * Gets the SELECT clause of this query. * @@ -416,34 +457,38 @@ public class ADQLQuery implements ADQLObject { else columns.addAll(from.getDBColumns()); } catch(ParseException pe) { - // Here, this error should not occur any more, since it must have been caught by the DBChecker! + /* Here, this error should not occur any more, since it must + * have been caught by the DBChecker! */ } } else { // Create the DBColumn: DBColumn col = null; // ...whose the name will be set with the SELECT item's alias: if (item.hasAlias()) { - // put the alias in lower case if not written between "": - /* Note: This aims to avoid unexpected behavior at execution - * time in the DBMS (i.e. the case sensitivity is - * forced for every references to this column alias). */ - String alias = item.getAlias(); - if (!item.isCaseSensitive()) - alias = alias.toLowerCase(); + String alias; + + // If delimited, put the alias between double quotes. + if (item.isCaseSensitive()) + alias = DBIdentifier.denormalize(item.getAlias(), true); + // If not delimited, put the alias in lower-case. + else + alias = item.getAlias().toLowerCase(); // create the DBColumn: if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null) { col = ((ADQLColumn)operand).getDBLink(); - col = col.copy(col.getDBName(), alias, col.getTable()); + col = col.copy(alias, alias, col.getTable()); } else col = new DefaultDBColumn(alias, null); } // ...or whose the name will be the name of the SELECT item: else { - if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null) - col = ((ADQLColumn)operand).getDBLink(); - else - col = new DefaultDBColumn(item.getName(), null); + if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null) { + DBColumn formerCol = ((ADQLColumn)operand).getDBLink(); + // keep the same ADQL and DB name ; just change the table: + col = formerCol.copy(formerCol.getDBName(), (formerCol.isCaseSensitive() ? DBIdentifier.denormalize(formerCol.getADQLName(), true) : formerCol.getADQLName().toLowerCase()), formerCol.getTable()); + } else + col = new DefaultDBColumn((item.isCaseSensitive() ? DBIdentifier.denormalize(item.getName(), true) : item.getName().toLowerCase()), null); } /* For columns created by default (from functions and operations generally), @@ -505,24 +550,27 @@ public class ADQLQuery implements ADQLObject { index++; switch(index) { case 0: - currentClause = select; + currentClause = with; break; case 1: + currentClause = select; + break; + case 2: currentClause = null; return from; - case 2: + case 3: currentClause = where; break; - case 3: + case 4: currentClause = groupBy; break; - case 4: + case 5: currentClause = having; break; - case 5: + case 6: currentClause = orderBy; break; - case 6: + case 7: currentClause = null; return offset; default: @@ -533,7 +581,7 @@ public class ADQLQuery implements ADQLObject { @Override public boolean hasNext() { - return index + 1 < 7; + return index + 1 < 8; } @Override @@ -547,42 +595,48 @@ public class ADQLQuery implements ADQLObject { else { switch(index) { case 0: + if (replacer instanceof ClauseADQL) + with = (ClauseADQL<WithItem>)replacer; + else + throw new UnsupportedOperationException("Impossible to replace a ClauseADQL (" + with.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); + break; + case 1: if (replacer instanceof ClauseSelect) select = (ClauseSelect)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseSelect (" + select.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 1: + case 2: if (replacer instanceof FromContent) from = (FromContent)replacer; else throw new UnsupportedOperationException("Impossible to replace a FromContent (" + from.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 2: + case 3: if (replacer instanceof ClauseConstraints) where = (ClauseConstraints)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseConstraints (" + where.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 3: + case 4: if (replacer instanceof ClauseADQL) groupBy = (ClauseADQL<ADQLOperand>)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseADQL (" + groupBy.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 4: + case 5: if (replacer instanceof ClauseConstraints) having = (ClauseConstraints)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseConstraints (" + having.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 5: + case 6: if (replacer instanceof ClauseADQL) orderBy = (ClauseADQL<ADQLOrder>)replacer; else throw new UnsupportedOperationException("Impossible to replace a ClauseADQL (" + orderBy.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ")!"); break; - case 6: + case 7: if (replacer instanceof ClauseOffset) offset = (ClauseOffset)replacer; else @@ -598,13 +652,14 @@ public class ADQLQuery implements ADQLObject { if (index <= -1) throw new IllegalStateException("remove() impossible: next() has not yet been called!"); - if (index == 0 || index == 1) - throw new UnsupportedOperationException("Impossible to remove a " + ((index == 0) ? "SELECT" : "FROM") + " clause from a query!"); - else if (index == 6) { + if (index == 1 || index == 2) + throw new UnsupportedOperationException("Impossible to remove a " + ((index == 1) ? "SELECT" : "FROM") + " clause from a query!"); + else if (index == 7) { offset = null; position = null; } else { - currentClause.clear(); + if (currentClause != null) + currentClause.clear(); position = null; } } @@ -613,7 +668,13 @@ public class ADQLQuery implements ADQLObject { @Override public String toADQL() { - StringBuffer adql = new StringBuffer(select.toADQL()); + StringBuffer adql = new StringBuffer(); + + if (!with.isEmpty()) + adql.append(with.toADQL()).append('\n'); + + adql.append(select.toADQL()); + adql.append("\nFROM ").append(from.toADQL()); if (!where.isEmpty()) diff --git a/src/adql/query/ClauseADQL.java b/src/adql/query/ClauseADQL.java index 56d3ffbdc414c253c28a3bc91e2225ec46b9003d..a49c8d11cac702532c2f3b9246f1d700637630bd 100644 --- a/src/adql/query/ClauseADQL.java +++ b/src/adql/query/ClauseADQL.java @@ -16,34 +16,53 @@ package adql.query; * You should have received a copy of the GNU Lesser General Public License * along with ADQLLibrary. If not, see <http://www.gnu.org/licenses/>. * - * Copyright 2012-2013 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), + * Copyright 2012-2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ +import adql.parser.feature.LanguageFeature; + /** * Represents an ADQL clause (i.e. SELECT, FROM, WHERE, ...). * * @author Grégory Mantelet (CDS;ARI) - * @version 1.2 (12/2013) + * @version 2.0 (08/2019) */ public class ClauseADQL<T extends ADQLObject> extends ADQLList<T> { /** - * Builds an anonymous ClauseADQL. + * Builds an anonymous {@link ClauseADQL}. */ public ClauseADQL() { super((String)null); } /** - * Builds a ClauseADQL considering its name. + * Builds a {@link ClauseADQL} considering its name. * - * @param name List label. + * @param name Clause label. */ public ClauseADQL(String name) { super(name); } + /** + * Builds a {@link ClauseADQL} considering its name. + * + * <p> + * The language feature is optional. If omitted, a default non-optional + * one will be created using the list's name. + * </p> + * + * @param name Clause label. + * @param implementedFeature Language Feature implemented by this list. + * + * @since 2.0 + */ + protected ClauseADQL(final String name, final LanguageFeature implementedFeature) { + super(name, implementedFeature); + } + /** * Builds a ClauseADQL by copying the given one. It copies also all the list * items of the given ClauseADQL. diff --git a/src/adql/query/ColumnReference.java b/src/adql/query/ColumnReference.java index 07b21edcfbdacfb61e5d5d3abfec956d259d8b9c..6b2706fe575d9f305865a5fae860a2f90a15ee2e 100644 --- a/src/adql/query/ColumnReference.java +++ b/src/adql/query/ColumnReference.java @@ -187,7 +187,10 @@ public class ColumnReference implements ADQLObject { * @return Its source table if {@link #getColumnName()} is a column name * (not an alias), * or NULL otherwise. + * + * @deprecated Since v2.0. This function is never used. */ + @Deprecated public final ADQLTable getAdqlTable() { return adqlTable; } @@ -204,7 +207,10 @@ public class ColumnReference implements ADQLObject { * @param adqlTable Its source table if {@link #getColumnName()} is a column * name (not an alias), * or NULL otherwise. + * + * @deprecated Since v2.0. This piece of information is never used. */ + @Deprecated public final void setAdqlTable(ADQLTable adqlTable) { this.adqlTable = adqlTable; } diff --git a/src/adql/query/WithItem.java b/src/adql/query/WithItem.java new file mode 100644 index 0000000000000000000000000000000000000000..8300bdb37d3f060be599f3cd357b3794211b2fc4 --- /dev/null +++ b/src/adql/query/WithItem.java @@ -0,0 +1,353 @@ +package adql.query; + +/* + * 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 2019 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS) + */ + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; + +import adql.db.DBColumn; +import adql.db.DBIdentifier; +import adql.db.DBTable; +import adql.parser.feature.LanguageFeature; +import adql.query.operand.ADQLColumn; + +/** + * Object representation of the definition of a Common Table Expression (CTE). + * + * <p> + * A such table is defined inside the ADQL clause <code>WITH</code>. It must + * be an ADQL query with a name for the resulting temporary table. Labels of + * the resulting columns may be also provided. + * </p> + * + * @author Grégory Mantelet (CDS) + * @version 2.0 (09/2019) + * @since 2.0 + */ +public class WithItem implements ADQLObject { + + /** Description of this ADQL Feature. */ + public final static LanguageFeature FEATURE = new LanguageFeature(LanguageFeature.TYPE_ADQL_COMMON_TABLE, "WITH", true, "A Common Table Expression lets create a temporary named result set that can be referred to elsewhere in a main query."); + + /** Name of the resulting table. */ + protected String label; + + /** Flag indicating whether the table name is case sensitive or not. */ + protected boolean caseSensitive = false; + + /** Labels of the resulting columns. + * <p><i><b>Note:</b> + * If NULL or empty, the default output column names must be used. + * </i></p> */ + protected List<ADQLColumn> columnLabels = null; + + /** ADQL query providing the CTE's content. */ + protected ADQLQuery query; + + /** Position of this WITH item in the original ADQL query. */ + protected TextPosition position = null; + + /** Database description of the resulting (temporary) table. */ + protected DBTable dbLink = null; + + /** + * Create a WITH item with the minimum mandatory information. + * + * @param label Name of the resulting table/CTE. + * @param query ADQL query returning the content of this CTE. + */ + public WithItem(final String label, final ADQLQuery query) { + this(label, query, null); + } + + /** + * Create a WITH item with column labels. + * + * <p><i><b>Warning:</b> + * If the given list is NULL or empty, the default output column names will + * be used. However, if not NULL, the given list should contain as many + * elements as columns returned by the given query. + * </i></p> + * + * @param label Name of the resulting table/CTE. + * @param query ADQL query returning the content of this CTE. + * @param columnLabels Labels of the output columns. + */ + public WithItem(final String label, final ADQLQuery query, final Collection<ADQLColumn> columnLabels) { + if (label == null || label.trim().isEmpty()) + throw new NullPointerException("Missing label of the WITH item!"); + + if (query == null) + throw new NullPointerException("Missing query of the WITH item!"); + + setLabel(label); + this.query = query; + this.columnLabels = (columnLabels == null || columnLabels.isEmpty()) ? null : new ArrayList<>(columnLabels); + + } + + /** + * Create a deep copy of the given WITH item. + * + * @param toCopy The WITH item to duplicate. + */ + public WithItem(final WithItem toCopy) { + label = toCopy.label; + query = toCopy.query; + position = toCopy.position; + if (columnLabels != null) { + columnLabels = new ArrayList<>(toCopy.columnLabels.size()); + for(ADQLColumn colLabel : toCopy.columnLabels) + columnLabels.add(colLabel); + } + } + + @Override + public final String getName() { + return label; + } + + @Override + public final LanguageFeature getFeatureDescription() { + return FEATURE; + } + + /** + * Get the name of the resulting table. + * + * @return CTE's name. + */ + public final String getLabel() { + return label; + } + + /** + * Set the name of the resulting table. + * + * <p><i><b>Note:</b> + * The given name may be delimited (i.e. surrounded by double quotes). If + * so, it will be considered as case sensitive. Surrounding double quotes + * will be removed and inner escaped double quotes will be un-escaped. + * </i></p> + * + * @param label New CTE's name. + * + * @throws NullPointerException If the given name is NULL or empty. + */ + public final void setLabel(String label) throws NullPointerException { + String tmp = DBIdentifier.normalize(label); + if (tmp == null) + throw new NullPointerException("Missing CTE's label! (CTE = WITH's query)"); + else { + this.label = tmp; + this.caseSensitive = DBIdentifier.isDelimited(label); + } + } + + /** + * Tell whether the resulting table name is case sensitive or not. + * + * @return <code>true</code> if the CTE's name is case sensitive, + * <code>false</code> otherwise. + */ + public final boolean isLabelCaseSensitive() { + return caseSensitive; + } + + /** + * Specify whether the resulting table name should be case sensitive or not. + * + * @param caseSensitive <code>true</code> to make the CTE's name case + * sensitive, + * <code>false</code> otherwise. + */ + public final void setLabelCaseSensitive(final boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Get the specified labels of the output columns of this CTE. + * + * @return CTE's columns labels, + * or NULL if none is specified. + */ + public final List<ADQLColumn> getColumnLabels() { + return columnLabels; + } + + /** + * Specify the tables of all the output columns. + * + * @param newColumnLabels New labels of the CTE's output columns, + * or NULL (or an empty list) to use the default + * column names instead. + */ + public final void setColumnLabels(final Collection<ADQLColumn> newColumnLabels) { + columnLabels = (newColumnLabels == null || newColumnLabels.isEmpty()) ? null : new ArrayList<>(newColumnLabels); + } + + /** + * Get the query corresponding to this CTE. + * + * @return CTE's query. + */ + public final ADQLQuery getQuery() { + return query; + } + + /** + * Set the query returning the content of this CTE. + * + * @param query New CTE's query. + */ + public final void setQuery(ADQLQuery query) { + this.query = query; + } + + /** + * Database description of this CTE. + * + * @return CTE's metadata. + */ + public final DBTable getDBLink() { + return dbLink; + } + + /** + * Set the database description of this CTE. + * + * @param dbMeta The new CTE's metadata. + */ + public final void setDBLink(final DBTable dbMeta) { + this.dbLink = dbMeta; + } + + @Override + public final TextPosition getPosition() { + return position; + } + + public final void setPosition(final TextPosition newPosition) { + position = newPosition; + } + + @Override + public ADQLObject getCopy() throws Exception { + return new WithItem(this); + } + + @Override + public ADQLIterator adqlIterator() { + return new ADQLIterator() { + + private boolean queryReturned = false; + + @Override + public ADQLObject next() { + if (queryReturned) + throw new NoSuchElementException("Iteration already finished! No more element available."); + else { + queryReturned = true; + return query; + } + } + + @Override + public boolean hasNext() { + return !queryReturned; + } + + @Override + public void replace(final ADQLObject replacer) throws UnsupportedOperationException, IllegalStateException { + if (!queryReturned) + throw new IllegalStateException("No iteration yet started!"); + else if (replacer == null) + throw new UnsupportedOperationException("Impossible to remove the query from a WithItem object! You have to remove the WithItem from its ClauseWith for that."); + else if (!(replacer instanceof ADQLQuery)) + throw new UnsupportedOperationException("Impossible to replace an ADQLQuery by a " + replacer.getClass() + "!"); + else + query = (ADQLQuery)replacer; + } + }; + } + + @Override + public String toADQL() { + // Serialize the list of output columns: + StringBuffer bufOutColumns = new StringBuffer(); + if (columnLabels != null && !columnLabels.isEmpty()) { + for(ADQLColumn col : columnLabels) { + bufOutColumns.append(bufOutColumns.length() == 0 ? '(' : ','); + bufOutColumns.append(DBIdentifier.denormalize(col.getColumnName(), col.isCaseSensitive(IdentifierField.COLUMN))); + } + bufOutColumns.append(')'); + } + // And now serialize the whole WithItem: + return DBIdentifier.denormalize(label, caseSensitive) + bufOutColumns.toString() + " AS (\n" + query.toADQL() + "\n)"; + } + + /** + * Get the description of all output columns. + * + * <p><i><b>Note 1:</b> + * All resulting columns are returned, even if no column's label is + * provided. + * </i></p> + * + * <p><i><b>Note 2:</b> + * List generated on the fly! + * </i></p> + * + * @return List and description of all output columns. + */ + public DBColumn[] getResultingColumns() { + // Fetch all resulting columns from the query: + DBColumn[] dbColumns = query.getResultingColumns(); + + // Force the writing of the column names: + boolean caseSensitive; + String newColLabel; + for(int i = 0; i < dbColumns.length; i++) { + // fetch the default column name and case sensitivity: + caseSensitive = dbColumns[i].isCaseSensitive(); + newColLabel = dbColumns[i].getADQLName(); + + // if an explicit label is given, use it instead: + if (columnLabels != null && i < columnLabels.size()) { + caseSensitive = columnLabels.get(i).isCaseSensitive(IdentifierField.COLUMN); + newColLabel = columnLabels.get(i).getColumnName(); + } + + // reformat the column label in function of its case sensitivity: + if (caseSensitive) + newColLabel = DBIdentifier.denormalize(newColLabel, true); + else + newColLabel = newColLabel.toLowerCase(); + + // finally, copy the original column with this new name: + dbColumns[i] = dbColumns[i].copy(newColLabel, newColLabel, dbColumns[i].getTable()); + } + + return dbColumns; + } + +} diff --git a/src/adql/query/operand/ADQLColumn.java b/src/adql/query/operand/ADQLColumn.java index 1dd0ec5cae92e2ce7b16e82d54180a6192ed0236..b4aa7e51568347090005636961563859d48ff08f 100644 --- a/src/adql/query/operand/ADQLColumn.java +++ b/src/adql/query/operand/ADQLColumn.java @@ -34,7 +34,7 @@ import adql.query.from.ADQLTable; * ({schema(s)}.{table}.{column}). * * @author Grégory Mantelet (CDS;ARI) - * @version 2.0 (07/2019) + * @version 2.0 (09/2019) */ public class ADQLColumn implements ADQLOperand, UnknownType { @@ -67,7 +67,9 @@ public class ADQLColumn implements ADQLOperand, UnknownType { /** The {@link ADQLTable} which is supposed to contain this column. * By default, this field is automatically filled by - * {@link adql.db.DBChecker}. */ + * {@link adql.db.DBChecker}. + * @deprecated Since v2.0. This piece of information is never used. */ + @Deprecated private ADQLTable adqlTable = null; /** Type expected by the parser. @@ -472,7 +474,10 @@ public class ADQLColumn implements ADQLOperand, UnknownType { * Gets the {@link ADQLTable} from which this column is supposed to come. * * @return Its source table. + * + * @deprecated Since v2.0. This function is never used. */ + @Deprecated public final ADQLTable getAdqlTable() { return adqlTable; } @@ -487,7 +492,10 @@ public class ADQLColumn implements ADQLOperand, UnknownType { * </p> * * @param adqlTable Its source table. + * + * @deprecated Since v2.0. This piece of information is never used. */ + @Deprecated public final void setAdqlTable(ADQLTable adqlTable) { this.adqlTable = adqlTable; } @@ -527,7 +535,10 @@ public class ADQLColumn implements ADQLOperand, UnknownType { @Override public String getName() { - return getColumnName(); + if (dbLink != null) + return (dbLink.isCaseSensitive() ? dbLink.getADQLName() : dbLink.getADQLName().toLowerCase()); + else + return getColumnName(); } @Override diff --git a/src/adql/translator/ADQLTranslator.java b/src/adql/translator/ADQLTranslator.java index 9b0c651d2c40b07b6ae5c2b64f21d2b93587685e..d915931f69d59f50ebe0e3b5a3a5f3aa17b873f1 100644 --- a/src/adql/translator/ADQLTranslator.java +++ b/src/adql/translator/ADQLTranslator.java @@ -26,11 +26,13 @@ import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ADQLOrder; import adql.query.ADQLQuery; +import adql.query.ClauseADQL; import adql.query.ClauseConstraints; import adql.query.ClauseSelect; import adql.query.ColumnReference; import adql.query.SelectAllColumns; import adql.query.SelectItem; +import adql.query.WithItem; import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -51,9 +53,9 @@ import adql.query.operand.Operation; import adql.query.operand.StringConstant; import adql.query.operand.WrappedOperand; import adql.query.operand.function.ADQLFunction; +import adql.query.operand.function.InUnitFunction; import adql.query.operand.function.MathFunction; import adql.query.operand.function.SQLFunction; -import adql.query.operand.function.InUnitFunction; import adql.query.operand.function.UserDefinedFunction; import adql.query.operand.function.geometry.AreaFunction; import adql.query.operand.function.geometry.BoxFunction; @@ -104,10 +106,16 @@ public interface ADQLTranslator { /* ***** LIST & CLAUSE ***** */ public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException; + /** @since 2.0 */ + public String translate(ClauseADQL<WithItem> clause) throws TranslationException; + public String translate(ClauseSelect clause) throws TranslationException; public String translate(ClauseConstraints clause) throws TranslationException; + /** @since 2.0 */ + public String translate(WithItem item) throws TranslationException; + public String translate(SelectItem item) throws TranslationException; public String translate(SelectAllColumns item) throws TranslationException; diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java index bc482437f23a4b11a5a176f32a2a73fedf73eaca..f2da12c19cde507a298df47c0bea2496e5bf0b80 100644 --- a/src/adql/translator/JDBCTranslator.java +++ b/src/adql/translator/JDBCTranslator.java @@ -21,8 +21,10 @@ package adql.translator; */ import java.util.Iterator; +import java.util.List; import adql.db.DBColumn; +import adql.db.DBIdentifier; import adql.db.DBTable; import adql.db.DBTableAlias; import adql.db.DBType; @@ -33,12 +35,14 @@ import adql.query.ADQLList; import adql.query.ADQLObject; import adql.query.ADQLOrder; import adql.query.ADQLQuery; +import adql.query.ClauseADQL; import adql.query.ClauseConstraints; import adql.query.ClauseSelect; import adql.query.ColumnReference; import adql.query.IdentifierField; import adql.query.SelectAllColumns; import adql.query.SelectItem; +import adql.query.WithItem; import adql.query.constraint.ADQLConstraint; import adql.query.constraint.Between; import adql.query.constraint.Comparison; @@ -329,8 +333,8 @@ public abstract class JDBCTranslator implements ADQLTranslator { * @return The string buffer + identifier. */ public StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive) { - if (caseSensitive && !id.matches("\"[^\"]*\"")) - return str.append('"').append(id).append('"'); + if (caseSensitive && !DBIdentifier.isDelimited(id)) + return str.append(DBIdentifier.denormalize(id, true)); else return str.append(id); } @@ -354,13 +358,20 @@ public abstract class JDBCTranslator implements ADQLTranslator { return translate((ADQLOperand)obj); else if (obj instanceof ADQLConstraint) return translate((ADQLConstraint)obj); + else if (obj instanceof WithItem) + return translate((WithItem)obj); else return obj.toADQL(); } @Override public String translate(ADQLQuery query) throws TranslationException { - StringBuffer sql = new StringBuffer(translate(query.getSelect())); + StringBuffer sql = new StringBuffer(); + + if (!query.getWith().isEmpty()) + sql.append(translate(query.getWith())).append('\n'); + + sql.append(translate(query.getSelect())); sql.append("\nFROM ").append(translate(query.getFrom())); @@ -444,6 +455,11 @@ public abstract class JDBCTranslator implements ADQLTranslator { return sql; } + @Override + public String translate(final ClauseADQL<WithItem> clause) throws TranslationException { + return getDefaultADQLList(clause); + } + @Override public String translate(ClauseSelect clause) throws TranslationException { String sql = null; @@ -468,6 +484,44 @@ public abstract class JDBCTranslator implements ADQLTranslator { return getDefaultADQLList(clause); } + @Override + public String translate(final WithItem item) throws TranslationException { + StringBuffer translation = new StringBuffer(); + + // query name/label: + if (item.getDBLink() != null) + appendIdentifier(translation, (item.getDBLink().isCaseSensitive() ? item.getDBLink().getDBName() : item.getDBLink().getDBName().toLowerCase()), true); + else + appendIdentifier(translation, (item.isLabelCaseSensitive() ? item.getLabel() : item.getLabel().toLowerCase()), true); + + // output column labels (if any): + if (item.getDBLink() != null) { + boolean firstDone = false; + for(DBColumn dbCol : item.getDBLink()) { + translation.append(firstDone ? ',' : '('); + appendIdentifier(translation, dbCol.getADQLName(), true); + firstDone = true; + } + translation.append(')'); + } else { + List<ADQLColumn> colLabels = item.getColumnLabels(); + if (colLabels != null && !colLabels.isEmpty()) { + boolean firstDone = false; + for(ADQLColumn col : colLabels) { + translation.append(firstDone ? ',' : '('); + appendIdentifier(translation, (col.isCaseSensitive(IdentifierField.COLUMN) ? col.getColumnName() : col.getColumnName().toLowerCase()), true); + firstDone = true; + } + translation.append(')'); + } + } + + // query itself: + translation.append(" AS (\n").append(translate(item.getQuery())).append("\n)"); + + return translation.toString(); + } + @Override public String translate(SelectItem item) throws TranslationException { if (item instanceof SelectAllColumns) @@ -476,10 +530,7 @@ public abstract class JDBCTranslator implements ADQLTranslator { StringBuffer translation = new StringBuffer(translate(item.getOperand())); if (item.hasAlias()) { translation.append(" AS "); - if (item.isCaseSensitive()) - appendIdentifier(translation, item.getAlias(), true); - else - appendIdentifier(translation, item.getAlias().toLowerCase(), true); + appendIdentifier(translation, (item.isCaseSensitive() ? item.getAlias() : item.getAlias().toLowerCase()), true); } else { translation.append(" AS "); appendIdentifier(translation, item.getName(), true); @@ -508,7 +559,7 @@ public abstract class JDBCTranslator implements ADQLTranslator { StringBuffer cols = new StringBuffer(); for(DBColumn col : dbCols) { if (cols.length() > 0) - cols.append(','); + cols.append(" , "); if (col.getTable() != null) { if (col.getTable() instanceof DBTableAlias) cols.append(getTableName(col.getTable(), false)).append('.'); @@ -516,7 +567,8 @@ public abstract class JDBCTranslator implements ADQLTranslator { cols.append(getQualifiedTableName(col.getTable())).append('.'); } appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN); - cols.append(" AS \"").append(col.getADQLName()).append('\"'); + cols.append(" AS "); + appendIdentifier(cols, (col.isCaseSensitive() ? col.getADQLName() : col.getADQLName().toLowerCase()), true); } return (cols.length() > 0) ? cols.toString() : item.toADQL(); } else { @@ -595,16 +647,19 @@ public abstract class JDBCTranslator implements ADQLTranslator { /* In case where metadata are known, the alias must always be * written case sensitively in order to ensure a translation * stability (i.e. all references clearly point toward this alias - * whatever is their character case). */ + * whatever is their character case). * if (table.getDBLink() != null) { - if (table.isCaseSensitive(IdentifierField.ALIAS)) - appendIdentifier(sql, table.getAlias(), true); + if (table.getDBLink().isCaseSensitive()) + appendIdentifier(sql, table.getAlias(), IdentifierField.TABLE); else - appendIdentifier(sql, table.getAlias().toLowerCase(), true); + appendIdentifier(sql, table.getAlias().toLowerCase(), IdentifierField.TABLE); } - /* Otherwise, just write what is written in ADQL: */ + /* Otherwise, just write what is written in ADQL: * + else*/ + if (table.getDBLink() != null) + appendIdentifier(sql, (table.getDBLink().isCaseSensitive() ? table.getDBLink().getDBName() : table.getDBLink().getDBName().toLowerCase()), true); else - appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS)); + appendIdentifier(sql, (table.isCaseSensitive(IdentifierField.ALIAS) ? table.getAlias() : table.getAlias().toLowerCase()), true); } return sql.toString(); @@ -676,14 +731,8 @@ public abstract class JDBCTranslator implements ADQLTranslator { StringBuffer colName = new StringBuffer(); // Use the DBTable if any: - if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) { - /* Note: if the table is aliased, ensure no schema is prefixing - * this alias thanks to getTableName(..., false). */ - if (dbCol.getTable() instanceof DBTableAlias) - colName.append(getTableName(dbCol.getTable(), false)).append('.'); - else - colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); - } + if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null) + colName.append(getQualifiedTableName(dbCol.getTable())).append('.'); // Otherwise, use the prefix of the column given in the ADQL query: else if (column.getTableName() != null) diff --git a/src/cds/utils/TextualSearchList.java b/src/cds/utils/TextualSearchList.java index 2cd9c8df0eb13f7977101ef875077d83c75a6cbc..fbb05efdd20dc7ac7bbe28fc30d8a79466e8bfcf 100644 --- a/src/cds/utils/TextualSearchList.java +++ b/src/cds/utils/TextualSearchList.java @@ -2,20 +2,20 @@ package cds.utils; /* * 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 2012-2017 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ @@ -37,108 +37,108 @@ import java.util.List; * by the {@link Object#toString() toString()} function will be used as key. * </p> * <p><b><u>WARNING:</u> The extracted key MUST be CASE-SENSITIVE and UNIQUE !</b></p> - * + * * @param <E> Type of object to manage in this list. * @author Grégory Mantelet (CDS;ARI) * @version 1.4 (09/2017) */ -public class TextualSearchList< E > extends ArrayList<E> { +public class TextualSearchList<E> extends ArrayList<E> { private static final long serialVersionUID = 1L; /** Object to use to extract an unique textual string. */ public final KeyExtractor<E> keyExtractor; /** Map which associates objects of type E with its textual string (case-sensitive). */ - protected final HashMap<String,ArrayList<E>> csMap; + protected final HashMap<String, ArrayList<E>> csMap; /** Map which associates objects of type E with their lower-case textual string. */ - protected final HashMap<String,ArrayList<E>> ncsMap; + protected final HashMap<String, ArrayList<E>> ncsMap; /* ************ */ /* CONSTRUCTORS */ /* ************ */ /** * <p>Builds an empty TextualSearchList.</p> - * + * * <p><i><u>Note:</u> * the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function. * </i></p> - * + * * @see #TextualSearchList(KeyExtractor) */ - public TextualSearchList(){ + public TextualSearchList() { this(new DefaultKeyExtractor<E>()); } /** * Builds an empty TextualSearchList. - * + * * @param keyExtractor The object to use to extract a textual key from objects to insert. - * + * * @see ArrayList#ArrayList() */ - public TextualSearchList(final KeyExtractor<E> keyExtractor){ + public TextualSearchList(final KeyExtractor<E> keyExtractor) { super(); this.keyExtractor = keyExtractor; - csMap = new HashMap<String,ArrayList<E>>(); - ncsMap = new HashMap<String,ArrayList<E>>(); + csMap = new HashMap<String, ArrayList<E>>(); + ncsMap = new HashMap<String, ArrayList<E>>(); } /** * <p>Builds an empty TextualSearchList with an initial capacity.</p> - * + * * <p><i><u>Note:</u> * the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function. * </i></p> - * + * * @param initialCapacity Initial capacity of this list. - * + * * @see #TextualSearchList(int, KeyExtractor) */ - public TextualSearchList(int initialCapacity){ + public TextualSearchList(int initialCapacity) { this(initialCapacity, new DefaultKeyExtractor<E>()); } /** * Builds an empty TextualSearchList with an initial capacity. - * + * * @param initialCapacity Initial capacity of this list. * @param keyExtractor The object to use to extract a textual key from objects to insert. - * + * * @see ArrayList#ArrayList(int) */ - public TextualSearchList(final int initialCapacity, final KeyExtractor<E> keyExtractor){ + public TextualSearchList(final int initialCapacity, final KeyExtractor<E> keyExtractor) { super(initialCapacity); this.keyExtractor = keyExtractor; - csMap = new HashMap<String,ArrayList<E>>(initialCapacity); - ncsMap = new HashMap<String,ArrayList<E>>(initialCapacity); + csMap = new HashMap<String, ArrayList<E>>(initialCapacity); + ncsMap = new HashMap<String, ArrayList<E>>(initialCapacity); } /** * <p>Builds a TextualSearchList filled with the objects of the given collection.</p> - * + * * <p><i><u>Note:</u> * the key of inserted objects will be the string returned by their {@link Object#toString() toString()} function. * </i></p> - * + * * @param c Collection to copy into this list. */ - public TextualSearchList(Collection<? extends E> c){ + public TextualSearchList(Collection<? extends E> c) { this(c, new DefaultKeyExtractor<E>()); } /** * Builds a TextualSearchList filled with the objects of the given collection. - * + * * @param c Collection to copy into this list. * @param keyExtractor The object object to use to extract a textual key from objects to insert. - * + * * @see #addAll(Collection) */ - public TextualSearchList(Collection<? extends E> c, final KeyExtractor<E> keyExtractor){ + public TextualSearchList(Collection<? extends E> c, final KeyExtractor<E> keyExtractor) { this.keyExtractor = keyExtractor; - csMap = new HashMap<String,ArrayList<E>>(c.size()); - ncsMap = new HashMap<String,ArrayList<E>>(c.size()); + csMap = new HashMap<String, ArrayList<E>>(c.size()); + ncsMap = new HashMap<String, ArrayList<E>>(c.size()); addAll(c); } @@ -146,48 +146,48 @@ public class TextualSearchList< E > extends ArrayList<E> { * Returns true if this list contains the specified element. * More formally, returns true if and only if this list contains at least one element * e such that (keyExtractor.getKey(o).equals(keyExtractor.getKey(e))). - * + * * @see java.util.ArrayList#contains(java.lang.Object) * @see #getKey(Object) - * + * * @since 1.1 */ @SuppressWarnings("unchecked") @Override - public boolean contains(Object o){ - try{ + public boolean contains(Object o) { + try { if (o == null) return false; - else{ + else { E object = (E)o; return !get(getKey(object)).isEmpty(); } - }catch(Exception e){ + } catch(Exception e) { return false; } } /** * Searches (CASE-INSENSITIVE) the object which has the given key. - * + * * @param key Textual key of the object to search. - * + * * @return The corresponding object or <code>null</code>. */ - public final List<E> get(final String key){ + public final List<E> get(final String key) { return get(key, false); } /** * Searches of all the object which has the given key. - * + * * @param key Textual key of the object to search. * @param caseSensitive <i>true</i> to consider the case of the key, <i>false</i> otherwise. - * + * * @return All the objects whose the key is the same as the given one. */ @SuppressWarnings("unchecked") - public List<E> get(final String key, final boolean caseSensitive){ + public List<E> get(final String key, final boolean caseSensitive) { if (key == null) return new ArrayList<E>(0); @@ -200,15 +200,15 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Generates and checks the key of the given object. - * + * * @param value The object whose the key must be generated. - * + * * @return Its corresponding key or <i>null</i> if this object already exist in this list. - * + * * @throws NullPointerException If the given object or its extracted key is <code>null</code>. * @throws IllegalArgumentException If the extracted key is already used by another object in this list. */ - private final String getKey(final E value) throws NullPointerException, IllegalArgumentException{ + private final String getKey(final E value) throws NullPointerException, IllegalArgumentException { String key = keyExtractor.getKey(value); if (key == null) throw new NullPointerException("Null keys are not allowed in a TextualSearchList !"); @@ -217,11 +217,11 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Adds the given object in the two maps with the given key. - * + * * @param key The key with which the given object must be associated. * @param value The object to add. */ - private final void putIntoMaps(final String key, final E value){ + private final void putIntoMaps(final String key, final E value) { // update the case-sensitive map: putIntoMap(csMap, key, value); // update the case-INsensitive map: @@ -230,35 +230,25 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Adds the given object in the given map with the given key. - * + * * @param map The map in which the given value must be added. * @param key The key with which the given object must be associated. * @param value The object to add. - * + * * @param <E> The type of objects managed in the given map. */ - private static final < E > void putIntoMap(final HashMap<String,ArrayList<E>> map, final String key, final E value){ + private static final <E> void putIntoMap(final HashMap<String, ArrayList<E>> map, final String key, final E value) { ArrayList<E> lst = map.get(key); - if (lst == null){ + if (lst == null) { lst = new ArrayList<E>(); lst.add(value); map.put(key, lst); - }else + } else lst.add(value); } - /** - * Adds the given object at the end of this list. - * - * @param obj Object to add (different from NULL). - * - * @throws NullPointerException If the given object or its extracted key is <code>null</code>. - * @throws IllegalArgumentException If the extracted key is already used by another object in this list. - * - * @see java.util.ArrayList#add(java.lang.Object) - */ @Override - public boolean add(E obj) throws NullPointerException, IllegalArgumentException{ + public boolean add(E obj) throws NullPointerException, IllegalArgumentException { if (obj == null) throw new NullPointerException("Null objects are not allowed in a TextualSearchList !"); @@ -266,27 +256,27 @@ public class TextualSearchList< E > extends ArrayList<E> { if (key == null) return false; - if (super.add(obj)){ + if (super.add(obj)) { putIntoMaps(key, obj); return true; - }else + } else return false; } /** * Adds the given object at the given position in this list. - * + * * @param index Index at which the given object must be added. * @param obj Object to add (different from NULL). - * + * * @throws NullPointerException If the given object or its extracted key is <code>null</code>. * @throws IllegalArgumentException If the extracted key is already used by another object in this list. * @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list. - * + * * @see java.util.ArrayList#add(int, java.lang.Object) */ @Override - public void add(int index, E obj) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException{ + public void add(int index, E obj) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException { if (obj == null) throw new NullPointerException("Null objects are not allowed in a TextualSearchList !"); @@ -301,19 +291,19 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Appends all the objects of the given collection in this list. - * + * * @param c Collection of objects to add. - * + * * @return <code>true</code> if this list changed as a result of the call, <code>false</code> otherwise. - * + * * @throws NullPointerException If an object to add or its extracted key is <code>null</code>. * @throws IllegalArgumentException If the extracted key is already used by another object in this list. - * + * * @see java.util.ArrayList#addAll(java.util.Collection) * @see #add(Object) */ @Override - public boolean addAll(Collection<? extends E> c) throws NullPointerException, IllegalArgumentException{ + public boolean addAll(Collection<? extends E> c) throws NullPointerException, IllegalArgumentException { if (c == null) return false; @@ -326,27 +316,27 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Appends all the objects of the given collection in this list after the given position. - * + * * @param index Position from which objects of the given collection must be added. * @param c Collection of objects to add. - * + * * @return <code>true</code> if this list changed as a result of the call, <code>false</code> otherwise. - * + * * @throws NullPointerException If an object to add or its extracted key is <code>null</code>. * @throws IllegalArgumentException If the extracted key is already used by another object in this list. * @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list. - * + * * @see java.util.ArrayList#addAll(int, java.util.Collection) * @see #add(int, Object) */ @Override - public boolean addAll(int index, Collection<? extends E> c) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException{ + public boolean addAll(int index, Collection<? extends E> c) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException { if (c == null) return false; boolean modified = false; int ind = index; - for(E obj : c){ + for(E obj : c) { add(ind++, obj); modified = get(ind).equals(obj); } @@ -356,20 +346,20 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Replaces the element at the specified position in this list with the specified element. - * + * * @param index Position of the object to replace. * @param obj Object to be stored at the given position (different from NULL). - * + * * @return Replaced object. - * + * * @throws NullPointerException If the object to add or its extracted key is <code>null</code>. * @throws IllegalArgumentException If the extracted key is already used by another object in this list. * @throws IndexOutOfBoundsException If the given index is negative or greater than the size of this list. - * + * * @see java.util.ArrayList#set(int, java.lang.Object) */ @Override - public E set(int index, E obj) throws NullPointerException, IllegalArgumentException{ + public E set(int index, E obj) throws NullPointerException, IllegalArgumentException { if (obj == null) throw new NullPointerException("Null objects are not allowed in a TextualSearchList !"); @@ -391,7 +381,7 @@ public class TextualSearchList< E > extends ArrayList<E> { } @Override - public void clear(){ + public void clear() { super.clear(); csMap.clear(); ncsMap.clear(); @@ -399,11 +389,11 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Removes the given object associated with the given key from the two maps. - * + * * @param key The key associated with the given object. * @param value The object to remove. */ - private final void removeFromMaps(final String key, final E value){ + private final void removeFromMaps(final String key, final E value) { // update the case-sensitive map: removeFromMap(csMap, key, value); // update the case-insensitive map: @@ -412,16 +402,16 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Removes the given object associated with the given key from the given map. - * + * * @param map The map from which the given object must be removed. * @param key The key associated with the given object. * @param value The object to remove. - * + * * @param <E> The type of objects managed in the given map. */ - private static final < E > void removeFromMap(final HashMap<String,ArrayList<E>> map, final String key, final E value){ + private static final <E> void removeFromMap(final HashMap<String, ArrayList<E>> map, final String key, final E value) { List<E> lst = map.get(key); - if (lst != null){ + if (lst != null) { lst.remove(value); if (lst.isEmpty()) map.remove(key); @@ -429,9 +419,9 @@ public class TextualSearchList< E > extends ArrayList<E> { } @Override - public E remove(int index){ + public E remove(int index) { E removed = super.remove(index); - if (removed != null){ + if (removed != null) { String key = keyExtractor.getKey(removed); removeFromMaps(key, removed); } @@ -440,9 +430,9 @@ public class TextualSearchList< E > extends ArrayList<E> { @SuppressWarnings("unchecked") @Override - public boolean remove(Object obj){ + public boolean remove(Object obj) { boolean removed = super.remove(obj); - if (removed){ + if (removed) { String key = keyExtractor.getKey((E)obj); removeFromMaps(key, (E)obj); } @@ -450,7 +440,7 @@ public class TextualSearchList< E > extends ArrayList<E> { } @Override - protected void removeRange(int fromIndex, int toIndex) throws IndexOutOfBoundsException{ + protected void removeRange(int fromIndex, int toIndex) throws IndexOutOfBoundsException { if (fromIndex < 0 || fromIndex >= size() || toIndex < 0 || toIndex >= size() || fromIndex > toIndex) throw new IndexOutOfBoundsException("Incorrect range indexes: from " + fromIndex + " to " + toIndex + " !"); @@ -463,12 +453,12 @@ public class TextualSearchList< E > extends ArrayList<E> { /* ************************************************ */ /** * Lets extract an unique textual key (case-sensitive) from a given type of object. - * + * * @author Gégory Mantelet (CDS) * @param <E> Type of object from which a textual key must be extracted. * @see TextualSearchList */ - public static interface KeyExtractor< E > { + public static interface KeyExtractor<E> { /** * Extracts an UNIQUE textual key (case-sensitive) from the given object. * @param obj Object from which a textual key must be extracted. @@ -480,13 +470,13 @@ public class TextualSearchList< E > extends ArrayList<E> { /** * Default implementation of {@link KeyExtractor}. * The extracted key is the string returned by the {@link Object#toString() toString()} function. - * + * * @author Grégory Mantelet (CDS) * @param <E> Type of object from which a textual key must be extracted. */ - protected static class DefaultKeyExtractor< E > implements KeyExtractor<E> { + protected static class DefaultKeyExtractor<E> implements KeyExtractor<E> { @Override - public String getKey(final E obj){ + public String getKey(final E obj) { return obj.toString(); } } diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java index 6f2ac2fe38acd0edeadcfbdf5de597f7f86dbafd..1aaff3bdfcd76283466cd09802f36530a4a4fd81 100644 --- a/src/tap/db/JDBCConnection.java +++ b/src/tap/db/JDBCConnection.java @@ -39,6 +39,7 @@ import java.util.Map; import java.util.Properties; import adql.db.DBColumn; +import adql.db.DBIdentifier; import adql.db.DBType; import adql.db.DBType.DBDatatype; import adql.db.STCS; @@ -1081,7 +1082,7 @@ public class JDBCConnection implements DBConnection { // create the new schema: TAPSchema newSchema = new TAPSchema(schemaName, nullifyIfNeeded(description), nullifyIfNeeded(utype)); - if (dbName != null && dbName.trim().length() > 0) + if (DBIdentifier.normalize(dbName) != null) newSchema.setDBName(dbName); newSchema.setIndex(schemaIndex); diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java index 6516976426b494eb8936357b95b132c9acfac054..ef4689ff369c34aeb430ca24d8fc6738e574b444 100644 --- a/src/tap/metadata/TAPColumn.java +++ b/src/tap/metadata/TAPColumn.java @@ -26,10 +26,10 @@ import java.util.Iterator; import java.util.Map; import adql.db.DBColumn; +import adql.db.DBIdentifier; import adql.db.DBTable; import adql.db.DBType; import adql.db.DBType.DBDatatype; -import adql.db.DefaultDBTable; /** * Represent a column as described by the IVOA standard in the TAP protocol @@ -82,21 +82,7 @@ import adql.db.DefaultDBTable; * @author Grégory Mantelet (CDS;ARI) * @version 2.4 (09/2019) */ -public class TAPColumn implements DBColumn { - - /** ADQL name of this column. */ - private final String adqlName; - - /** Indicate whether the ADQL column name is case sensitive. In such case, - * this name will be put between double quotes in ADQL. - * @since 2.4 */ - private boolean columnCaseSensitive = false; - - /** Name that this column have in the database. - * <p><i><b>Note:</b> - * It CAN NOT be NULL. By default, it is the ADQL name. - * </i></p> */ - private String dbName = null; +public class TAPColumn extends DBIdentifier implements DBColumn { /** Table which owns this column. * <p><i><b>Note:</b> @@ -228,20 +214,10 @@ public class TAPColumn implements DBColumn { * * @param columnName ADQL name of this column. * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ - public TAPColumn(String columnName) throws NullPointerException { - if (columnName == null) - throw new NullPointerException("Missing column name!"); - - columnName = columnName.trim(); - columnCaseSensitive = DefaultDBTable.isDelimited(columnName); - adqlName = (columnCaseSensitive ? columnName.substring(1, columnName.length() - 1).replaceAll("\"\"", "\"") : columnName); - - if (adqlName.trim().length() == 0) - throw new NullPointerException("Missing column name!"); - - dbName = null; + public TAPColumn(final String columnName) throws NullPointerException { + super(columnName); lstTargets = new ArrayList<TAPForeignKey>(1); lstSources = new ArrayList<TAPForeignKey>(1); @@ -277,7 +253,7 @@ public class TAPColumn implements DBColumn { * @param columnName ADQL name of this column. * @param type Datatype of this column. * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. * * @see #setDatatype(DBType) */ @@ -318,7 +294,7 @@ public class TAPColumn implements DBColumn { * @param description Description of the column's content. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, String description) throws NullPointerException { this(columnName, (DBType)null, description); @@ -357,7 +333,7 @@ public class TAPColumn implements DBColumn { * @param description Description of the column's content. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, DBType type, String description) throws NullPointerException { this(columnName, type); @@ -398,7 +374,7 @@ public class TAPColumn implements DBColumn { * @param unit Unit of the column's values. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, String description, String unit) throws NullPointerException { this(columnName, null, description, unit); @@ -438,7 +414,7 @@ public class TAPColumn implements DBColumn { * @param unit Unit of the column's values. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, DBType type, String description, String unit) throws NullPointerException { this(columnName, type, description); @@ -482,7 +458,7 @@ public class TAPColumn implements DBColumn { * @param utype UType associating this column with a data-model. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, String description, String unit, String ucd, String utype) throws NullPointerException { this(columnName, null, description, unit, ucd, utype); @@ -526,7 +502,7 @@ public class TAPColumn implements DBColumn { * @param utype UType associating this column with a data-model. * <i>May be NULL</i> * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPColumn(String columnName, DBType type, String description, String unit, String ucd, String utype) throws NullPointerException { this(columnName, type, description, unit); @@ -546,11 +522,6 @@ public class TAPColumn implements DBColumn { return getADQLName(); } - @Override - public final String getADQLName() { - return adqlName; - } - /** * Get the ADQL name of this column, as it has been provided at * initialization. @@ -560,36 +531,7 @@ public class TAPColumn implements DBColumn { * @since 2.1 */ public final String getRawName() { - return (columnCaseSensitive ? "\"" + adqlName.replaceAll("\"", "\"\"") + "\"" : adqlName); - } - - @Override - public final boolean isCaseSensitive() { - return columnCaseSensitive; - } - - @Override - public final String getDBName() { - return (dbName == null) ? getADQLName() : dbName; - } - - /** - * Change the name that this column MUST have in the database (i.e. in SQL - * queries). - * - * <p><i><b>Note:</b> - * If the given value is NULL or an empty string, nothing is done ; the DB - * name keeps is former value. - * </i></p> - * - * @param name The new database name of this column. - */ - public final void setDBName(String name) { - name = (name != null) ? name.trim() : name; - if (name != null && name.length() > 0) - dbName = name; - else - dbName = null; + return denormalize(getADQLName(), isCaseSensitive()); } @Override @@ -1033,8 +975,7 @@ public class TAPColumn implements DBColumn { */ @Override public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable) { - TAPColumn copy = new TAPColumn((adqlName == null) ? this.adqlName : adqlName, datatype, description, unit, ucd, utype); - copy.columnCaseSensitive = this.columnCaseSensitive; + TAPColumn copy = new TAPColumn((adqlName == null) ? getRawName() : adqlName, datatype, description, unit, ucd, utype); copy.setDBName((dbName == null) ? this.getDBName() : dbName); copy.setTable(dbTable); @@ -1060,7 +1001,7 @@ public class TAPColumn implements DBColumn { */ public DBColumn copy() { TAPColumn copy = new TAPColumn(adqlName, datatype, description, unit, ucd, utype); - copy.columnCaseSensitive = this.columnCaseSensitive; + copy.setCaseSensitive(isCaseSensitive()); copy.setDBName(dbName); copy.setTable(table); copy.setIndexed(indexed); @@ -1076,12 +1017,12 @@ public class TAPColumn implements DBColumn { return false; TAPColumn col = (TAPColumn)obj; - return col.getTable().equals(table) && col.getADQLName().equals(adqlName) && col.columnCaseSensitive == this.columnCaseSensitive; + return col.getTable().equals(table) && col.getADQLName().equals(adqlName) && col.isCaseSensitive() == this.isCaseSensitive(); } @Override public String toString() { - return (table != null ? table.toString() : "") + getRawName(); + return (table != null ? table.toString() + "." : "") + getRawName(); } } diff --git a/src/tap/metadata/TAPSchema.java b/src/tap/metadata/TAPSchema.java index f1e6e0c3c33f721f6096be44d6ea392f511f4430..0fe26e827daa5dbbcbb453f03c826c6cbd216b68 100644 --- a/src/tap/metadata/TAPSchema.java +++ b/src/tap/metadata/TAPSchema.java @@ -25,7 +25,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import adql.db.DefaultDBTable; +import adql.db.DBIdentifier; import tap.metadata.TAPTable.TableType; /** @@ -53,23 +53,7 @@ import tap.metadata.TAPTable.TableType; * @author Grégory Mantelet (CDS;ARI) * @version 2.4 (09/2019) */ -public class TAPSchema implements Iterable<TAPTable> { - - /** Name that this schema MUST have in ADQL queries. */ - private final String adqlName; - - /** Indicate whether the ADQL schema name must be considered as case - * sensitive. In such case, it should be provided between double quotes in - * the constructor parameter. - * @since 2.4 */ - private boolean schemaCaseSensitive; - - /** Name that this schema have in the database. - * <p><i><b>Note:</b> - * NULL by default. When NULL, {@link #getDBName()} returns exactly what - * {@link #getADQLName()} returns. - * </i></p> */ - private String dbName = null; +public class TAPSchema extends DBIdentifier implements Iterable<TAPTable> { /** Descriptive, human-interpretable name of the schema. * <p><i><b>Note:</b> @@ -135,18 +119,10 @@ public class TAPSchema implements Iterable<TAPTable> { * * @param schemaName ADQL name of this schema. * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPSchema(String schemaName) throws NullPointerException { - if (schemaName == null) - throw new NullPointerException("Missing schema name!"); - - schemaName = schemaName.trim(); - schemaCaseSensitive = DefaultDBTable.isDelimited(schemaName); - adqlName = (schemaCaseSensitive ? schemaName.substring(1, schemaName.length() - 1).replaceAll("\"\"", "\"") : schemaName); - - if (getADQLName().trim().length() == 0) - throw new NullPointerException("Missing schema name!"); + super(schemaName); dbName = getADQLName(); @@ -236,15 +212,6 @@ public class TAPSchema implements Iterable<TAPTable> { return getADQLName(); } - /** - * Get the (non delimited) ADQL name of this schema. - * - * @return Its ADQL name. - */ - public final String getADQLName() { - return adqlName; - } - /** * Get the full ADQL name of this schema, as it has been provided at * initialization (i.e. delimited if {@link #isCaseSensitive() case sensitive}). @@ -257,33 +224,12 @@ public class TAPSchema implements Iterable<TAPTable> { return toString(); } - /** - * Tell whether the ADQL name of this schema should be considered as case - * sensitive or not. - * - * @return <code>true</code> if the ADQL name is case sensitive, - * <code>false</code> otherwise. - */ - public final boolean isCaseSensitive() { - return schemaCaseSensitive; - } - - /** - * Let specify whether the ADQL name of this schema should be considered as - * case sensitive or not. - * - * @param sensitive <code>true</code> to make the ADQL name case sensitive, - * <code>false</code> otherwise. - */ - public final void setCaseSensitive(final boolean sensitive) { - schemaCaseSensitive = sensitive; - } - /** * Get the name this schema MUST have in the database. * * @return Its DB name. <i>MAY be NULL</i> */ + @Override public final String getDBName() { return dbName; } @@ -308,6 +254,7 @@ public class TAPSchema implements Iterable<TAPTable> { * * @param name Its new DB name. <i>MAY be NULL</i> */ + @Override public final void setDBName(String name) { name = (name != null) ? name.trim() : name; dbName = name; @@ -613,7 +560,7 @@ public class TAPSchema implements Iterable<TAPTable> { @Override public String toString() { - return (schemaCaseSensitive ? "\"" + adqlName.replaceAll("\"", "\"\"") + "\"" : adqlName); + return denormalize(getADQLName(), isCaseSensitive()); } } diff --git a/src/tap/metadata/TAPTable.java b/src/tap/metadata/TAPTable.java index 312905ae4da8a0699b6c05e1b95d3a91ee87f6bc..98e03d2b8c25b86314ab85a40144bcd6a50e35d2 100644 --- a/src/tap/metadata/TAPTable.java +++ b/src/tap/metadata/TAPTable.java @@ -28,9 +28,9 @@ import java.util.LinkedHashMap; import java.util.Map; import adql.db.DBColumn; +import adql.db.DBIdentifier; import adql.db.DBTable; import adql.db.DBType; -import adql.db.DefaultDBTable; import tap.TAPException; /** @@ -63,7 +63,7 @@ import tap.TAPException; * @author Grégory Mantelet (CDS;ARI) * @version 2.4 (09/2019) */ -public class TAPTable implements DBTable { +public class TAPTable extends DBIdentifier implements DBTable { /** * Different types of table according to the TAP protocol. @@ -78,26 +78,10 @@ public class TAPTable implements DBTable { output, table, view; } - /** ADQL name of this table. - * <p><i>This name is neither qualified nor delimited.</i></p> */ - private String adqlName; - /** Name of this table as provided at creation. * <p><i>This name may be qualified and/or delimited.</i></p> * @since 2.4 */ - private final String rawName; - - /** Indicate whether the ADQL table name is case sensitive. In such case, - * this name will be put between double quotes in ADQL. - * @since 2.4 */ - private boolean tableNameCaseSensitive = false; - - /** Name that this table have in the database. - * <p><i><b>Note:</b> - * If NULL, {@link #getDBName()} returns what {@link #getADQLName()} - * returns. - * </i></p> */ - private String dbName = null; + private String rawName; /** The schema which owns this table. * <p><i><b>Note:</b> @@ -181,7 +165,7 @@ public class TAPTable implements DBTable { * Double quotes may surround the table name. In such case, the ADQL * name of this table will be considered as case sensitive and these * double quotes will be automatically removed. - * <em>Note that this case sensitivity may not be identified just after + * <em>Note that this case sensitivity may be not identified just after * this constructor ; you may have to specify the schema * (see {@link #setSchema(TAPSchema)}) so that the schema prefix is * removed first.</em> @@ -190,19 +174,12 @@ public class TAPTable implements DBTable { * * @param tableName ADQL name of this table. * - * @throws NullPointerException If the given name is NULL or an empty string. + * @throws NullPointerException If the given name is NULL or empty. */ public TAPTable(final String tableName) throws NullPointerException { - if (tableName == null) - throw new NullPointerException("Missing table name!"); + super(tableName); rawName = tableName.trim(); - updateADQLName(); - - if (adqlName.trim().length() == 0) - throw new NullPointerException("Missing table name!"); - - dbName = null; columns = new LinkedHashMap<String, TAPColumn>(); foreignKeys = new ArrayList<TAPForeignKey>(); @@ -312,7 +289,7 @@ public class TAPTable implements DBTable { * @return Qualified and delimited (if needed) ADQL name of this table. */ public final String getFullName() { - return (schema != null ? schema.getADQLName() + "." : "") + (tableNameCaseSensitive ? "\"" + getADQLName().replaceAll("\"", "\"\"") + "\"" : getADQLName()); + return (schema != null ? schema.getADQLName() + "." : "") + denormalize(getADQLName(), isCaseSensitive()); } /** @@ -329,11 +306,6 @@ public class TAPTable implements DBTable { return getADQLName(); } - @Override - public final String getADQLName() { - return adqlName; - } - /** * Get the full ADQL name of this table, as it has been provided at * initialization. @@ -358,62 +330,38 @@ public class TAPTable implements DBTable { * * @since 2.4 */ - private void updateADQLName() { - String tmp = rawName; + @Override + public void setADQLName(final String name) throws NullPointerException { + /* Start by setting the new ADQL name (ignoring prefix if any + * + detection of NULL and empty string): */ + super.setADQLName(name); + + // Memorize the new raw name: + rawName = name.trim(); // If a schema is specified, remove the schema prefix (if any): if (schema != null) { + String tmp = name; + // strict comparison if schema is case sensitive: if (schema.isCaseSensitive()) { if (tmp.startsWith(schema.getRawName() + ".")) tmp = tmp.substring(schema.getRawName().length() + 1).trim(); } + // if no case sensitivity... else { // ...search not-case-sensitively for a prefix: if (tmp.toLowerCase().startsWith(schema.getADQLName().toLowerCase() + ".")) tmp = tmp.substring(schema.getADQLName().length() + 1).trim(); // ...otherwise, try with a strict comparison (as if schema was case sensitive): - else if (tmp.toLowerCase().startsWith("\"" + schema.getADQLName().toLowerCase().replaceAll("\"", "\"\"") + "\".")) - tmp = tmp.substring(schema.getADQLName().replaceAll("\"", "\"\"").length() + 3).trim(); + else if (tmp.toLowerCase().startsWith(denormalize(schema.getADQLName().toLowerCase(), true) + ".")) + tmp = tmp.substring(denormalize(schema.getADQLName(), true).length() + 1).trim(); } - } - // Detect if delimited (i.e. case sensitive): - if ((tableNameCaseSensitive = DefaultDBTable.isDelimited(tmp))) - tmp = tmp.substring(1, tmp.length() - 1).replaceAll("\"\"", "\""); - - // Finally, set the ADQL name: - adqlName = tmp; - } - - @Override - public final boolean isCaseSensitive() { - return tableNameCaseSensitive; - } - - @Override - public final String getDBName() { - return (dbName == null) ? getADQLName() : dbName; - } - - /** - * Change the name that this table MUST have in the database (i.e. in SQL - * queries). - * - * <p><i><b>Note:</b> - * If the given value is NULL or an empty string, {@link #getDBName()} will - * return exactly what {@link #getADQLName()} returns. - * </i></p> - * - * @param name The new database name of this table. - */ - public final void setDBName(String name) { - name = (name != null) ? name.trim() : name; - if (name != null && name.length() > 0) - dbName = name; - else - dbName = null; + // Finally, re-update the ADQL name (with prefix removed): + super.setADQLName(tmp); + } } @Override @@ -471,7 +419,7 @@ public class TAPTable implements DBTable { /* Update the ADQL name of this table: * (i.e. whether or not schema prefix should be removed) */ - updateADQLName(); + setADQLName(rawName); } /** @@ -1131,7 +1079,7 @@ public class TAPTable implements DBTable { @Override public String toString() { - return ((schema != null) ? (schema.toString() + ".") : "") + (tableNameCaseSensitive ? "\"" + adqlName.replaceAll("\"", "\"\"") + "\"" : getADQLName()); + return ((schema != null) ? (schema.toString() + ".") : "") + denormalize(getADQLName(), isCaseSensitive()); } @Override diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java index 391d2320da034bf4a5cbc9f76784b192b3893e1c..7d5288f7ea214fb26a6bc5c91745ff0e19e81288 100644 --- a/test/adql/db/TestDBChecker.java +++ b/test/adql/db/TestDBChecker.java @@ -46,7 +46,7 @@ public class TestDBChecker { tables = new ArrayList<DBTable>(); DefaultDBTable fooTable = new DefaultDBTable(null, "aschema", "foo"); - DBColumn col = new DefaultDBColumn("colS", new DBType(DBDatatype.VARCHAR), fooTable); + DBColumn col = new DefaultDBColumn("\"colS\"", new DBType(DBDatatype.VARCHAR), fooTable); fooTable.addColumn(col); col = new DefaultDBColumn("colI", new DBType(DBDatatype.INTEGER), fooTable); fooTable.addColumn(col); @@ -74,6 +74,60 @@ public class TestDBChecker { public void tearDown() throws Exception { } + @Test + public void testWithClause() { + + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + parser.setQueryChecker(new DBChecker(tables)); + + try { + + // CASE: Only 1 CTE, no column label, no CTE case sensibility + assertNotNull(parser.parseQuery("WITH myfoo AS (SELECT * FROM foo) SELECT * FROM myfoo")); + + // CASE: CTE case sensibility respected + assertNotNull(parser.parseQuery("WITH \"MyFoo\" AS (SELECT * FROM foo) SELECT * FROM \"MyFoo\"")); + assertNotNull(parser.parseQuery("WITH \"MyFoo\" AS (SELECT * FROM foo) SELECT * FROM MyFoo")); + + // CASE: correct number of column labels + assertNotNull(parser.parseQuery("WITH MyFoo(col1, col2, col3) AS (SELECT * FROM foo) SELECT * FROM MyFoo")); + + // CASE: reference between WITH clause in the correct order + assertNotNull(parser.parseQuery("WITH MyFoo AS (SELECT * FROM foo), MyOtherFoo AS (SELECT * FROM MyFoo WHERE colS IS NULL) SELECT * FROM MyOtherFoo")); + + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected error while parsing+checking a valid ADQL query! (see console for more details)"); + } + + // CASE: CTE case sensibility not respected + try { + parser.parseQuery("WITH \"MyFoo\" AS (SELECT * FROM foo) SELECT * FROM \"myfoo\""); + fail("WITH item's label is case sensitive....references to this CTE should also be case sensitive."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: myfoo [l.1 c.51 - l.1 c.58]!\n - Unknown table \"\"myfoo\"\" !", ex.getMessage()); + } + + // CASE: less column labels than available columns + try { + parser.parseQuery("WITH MyFoo(col1) AS (SELECT * FROM foo) SELECT * FROM MyFoo"); + fail("WITH item's label is case sensitive....references to this CTE should also be case sensitive."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers!\n - The WITH query \"MyFoo\" specifies LESS columns (1) than available (3)!", ex.getMessage()); + } + // CASE: more column labels than available columns + try { + parser.parseQuery("WITH MyFoo(col1, col2, col3, col4) AS (SELECT * FROM foo) SELECT * FROM MyFoo"); + fail("WITH item's label is case sensitive....references to this CTE should also be case sensitive."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers!\n - The WITH query \"MyFoo\" specifies MORE columns (4) than available (3)!", ex.getMessage()); + } + + } + @Test public void testSplitTableName() { String[] names = DefaultDBTable.splitTableName("foo"); diff --git a/test/adql/db/TestDBIdentifier.java b/test/adql/db/TestDBIdentifier.java new file mode 100644 index 0000000000000000000000000000000000000000..20355500423213964fc27b3cdb175ad16594b090 --- /dev/null +++ b/test/adql/db/TestDBIdentifier.java @@ -0,0 +1,148 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +public class TestDBIdentifier { + + @Test + public void testIsDelimited() { + // CASE: All correctly delimited names + assertTrue(DBIdentifier.isDelimited("\"\"")); + assertTrue(DBIdentifier.isDelimited("\" \"")); + assertTrue(DBIdentifier.isDelimited("\"a\"")); + assertTrue(DBIdentifier.isDelimited("\"\"\"\"")); + assertTrue(DBIdentifier.isDelimited("\"foo.bar\"")); + assertTrue(DBIdentifier.isDelimited("\"foo\"\".\"\"bar\"")); + + // CASE: NOT delimited names + assertFalse(DBIdentifier.isDelimited(null)); + assertFalse(DBIdentifier.isDelimited("")); + assertFalse(DBIdentifier.isDelimited("foo")); + assertFalse(DBIdentifier.isDelimited("\"foo")); + assertFalse(DBIdentifier.isDelimited("foo\"")); + assertFalse(DBIdentifier.isDelimited("\"foo\".\"bar\"")); + } + + @Test + public void testNormalize() { + // CASE: NULL, empty string, delimited empty string => NULL + for(String str : new String[]{ null, "", " ", " \t \r \n ", "\"\"", " \"\" ", "\" \t \n \r \"" }) + assertNull(DBIdentifier.normalize(str)); + + // CASE: Non-delimited string => same, just trimmed + assertEquals("IDent", DBIdentifier.normalize(" \t IDent \n")); + assertEquals("ID\"ent\"", DBIdentifier.normalize(" \t ID\"ent\" \n")); + assertEquals("\" ID\"ent\" \"", DBIdentifier.normalize("\" ID\"ent\" \"")); + + // CASE: Delimited string => remove double quotes + assertEquals("IDent", DBIdentifier.normalize("\"IDent\"")); + assertEquals(" IDent ", DBIdentifier.normalize(" \t \" IDent \" \n")); + assertEquals(" ID\"ent\" ", DBIdentifier.normalize("\" ID\"\"ent\"\" \"")); + } + + @Test + public void testDenormalize() { + // CASE: NULL => NULL + assertNull(DBIdentifier.denormalize(null, true)); + assertNull(DBIdentifier.denormalize(null, false)); + + // CASE: Non-case-sensitive string => exactly same as provided + assertEquals(" \t IDent \n", DBIdentifier.denormalize(" \t IDent \n", false)); + + // CASE: Case-sensitive string => surrounded by double quotes + assertEquals("\" ID\"\"ent\"\"\"", DBIdentifier.denormalize(" ID\"ent\"", true)); + } + + @Test + public void testSetADQLName() { + DBIdentifier dbid = new DBIdentifier4Test("foo"); + assertEquals("foo", dbid.adqlName); + assertFalse(dbid.adqlCaseSensitive); + assertNull(dbid.dbName); + + // CASE: missing ADQL name => NullPointerException + for(String str : new String[]{ null, "", " ", " \t \r \n ", "\"\"", " \"\" ", "\" \t \n \r \"" }) { + try { + dbid.setADQLName(str); + fail("Setting a NULL or empty ADQL name should have failed with a NullPointerException!"); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing ADQL name!", ex.getMessage()); + } + } + + // CASE: Non-delimited ADQL name + dbid.setADQLName("Ident"); + assertEquals("Ident", dbid.getADQLName()); + assertFalse(dbid.isCaseSensitive()); + assertNull(dbid.dbName); + assertEquals(dbid.getADQLName(), dbid.getDBName()); + + // CASE: Delimited ADQL name + dbid.setADQLName("\"Ident\""); + assertEquals("Ident", dbid.getADQLName()); + assertTrue(dbid.isCaseSensitive()); + assertNull(dbid.dbName); + assertEquals(dbid.getADQLName(), dbid.getDBName()); + } + + @Test + public void testSetDBName() { + DBIdentifier dbid = new DBIdentifier4Test("foo", "dbFoo"); + assertEquals("foo", dbid.adqlName); + assertFalse(dbid.adqlCaseSensitive); + assertEquals("dbFoo", dbid.dbName); + + // CASE: missing ADQL name => NullPointerException + for(String str : new String[]{ null, "", " ", " \t \r \n ", "\"\"", " \"\" ", "\" \t \n \r \"" }) { + dbid.setDBName(str); + assertNull(dbid.dbName); + assertEquals(dbid.getADQLName(), dbid.getDBName()); + } + + // CASE: Non-delimited DB name + dbid.setDBName("Ident"); + assertEquals("Ident", dbid.dbName); + assertEquals(dbid.dbName, dbid.getDBName()); + + // CASE: Delimited DB name + dbid.setDBName("\"Ident\""); + assertEquals("Ident", dbid.getDBName()); + assertEquals("Ident", dbid.dbName); + assertEquals(dbid.dbName, dbid.getDBName()); + } + + @Test + public void testSetCaseSensitive() { + DBIdentifier dbid = new DBIdentifier4Test("foo", "dbFoo"); + assertFalse(dbid.isCaseSensitive()); + + // CASE: set case-sensitive + dbid.setCaseSensitive(true); + assertTrue(dbid.isCaseSensitive()); + + // CASE: set INcase-sensitive + dbid.setCaseSensitive(false); + assertFalse(dbid.isCaseSensitive()); + + } + + private final static class DBIdentifier4Test extends DBIdentifier { + + public DBIdentifier4Test(String adqlName, String dbName) throws NullPointerException { + super(adqlName, dbName); + } + + public DBIdentifier4Test(String adqlName) throws NullPointerException { + super(adqlName); + } + + } + +} diff --git a/test/adql/db/TestDefaultDBColumn.java b/test/adql/db/TestDefaultDBColumn.java new file mode 100644 index 0000000000000000000000000000000000000000..3db624c67f760882366fbfe22fa932cf8304d0b3 --- /dev/null +++ b/test/adql/db/TestDefaultDBColumn.java @@ -0,0 +1,71 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TestDefaultDBColumn { + + @Test + public void testDefaultDBColumnStringString() { + DefaultDBTable table = new DefaultDBTable("table"); + + // CASE: No DB name, no case sensitivity + DefaultDBColumn col = new DefaultDBColumn("adqlName", table); + assertEquals("adqlName", col.getADQLName()); + assertEquals(col.getADQLName(), col.getDBName()); + assertFalse(col.isCaseSensitive()); + + // CASE: No DB name, case sensitivity + col = new DefaultDBColumn("\"adqlName\"", table); + assertEquals("adqlName", col.getADQLName()); + assertEquals(col.getADQLName(), col.getDBName()); + assertTrue(col.isCaseSensitive()); + + // CASE: DB name, no case sensitivity + col = new DefaultDBColumn("adqlName", "dbName", table); + assertEquals("adqlName", col.getADQLName()); + assertEquals("dbName", col.getDBName()); + assertFalse(col.isCaseSensitive()); + + // CASE: DN name, case sensitivity + col = new DefaultDBColumn("\"adqlName\"", "dbName", table); + assertEquals("adqlName", col.getADQLName()); + assertEquals("dbName", col.getDBName()); + assertTrue(col.isCaseSensitive()); + } + + @Test + public void testSetADQLName() { + DefaultDBTable table = new DefaultDBTable("table"); + + // CASE: no case sensitivity, no DB name + DefaultDBColumn col = new DefaultDBColumn("adqlName", table); + assertEquals("adqlName", col.getADQLName()); + assertEquals(col.getADQLName(), col.getDBName()); + assertFalse(col.isCaseSensitive()); + + // CASE: undelimited name => OK + col.setADQLName("myColumn"); + assertEquals("myColumn", col.getADQLName()); + assertFalse(col.isCaseSensitive()); + + // CASE: delimited name => stored undelimited + col.setADQLName("\"MyColumn\""); + assertEquals("MyColumn", col.getADQLName()); + assertTrue(col.isCaseSensitive()); + + // CASE: missing DB name => ERROR! + for(String n : new String[]{ null, "", " ", "\"\"", "\" \"" }) { + try { + new DefaultDBColumn(n, table); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing ADQL name!", ex.getMessage()); + } + } + } + +} diff --git a/test/adql/db/TestDefaultDBTable.java b/test/adql/db/TestDefaultDBTable.java index 016ee2bade4a7be8c80011b3aa4dca14ae739aa3..539f8535a88ba3db7a769507254f80ae9468b000 100644 --- a/test/adql/db/TestDefaultDBTable.java +++ b/test/adql/db/TestDefaultDBTable.java @@ -9,29 +9,39 @@ import org.junit.Test; public class TestDefaultDBTable { @Test - public void testIsDelimited() { - // CASE: All correctly delimited names - assertTrue(DefaultDBTable.isDelimited("\"\"")); - assertTrue(DefaultDBTable.isDelimited("\" \"")); - assertTrue(DefaultDBTable.isDelimited("\"a\"")); - assertTrue(DefaultDBTable.isDelimited("\"\"\"\"")); - assertTrue(DefaultDBTable.isDelimited("\"foo.bar\"")); - assertTrue(DefaultDBTable.isDelimited("\"foo\"\".\"\"bar\"")); - - // CASE: NOT delimited names - assertFalse(DefaultDBTable.isDelimited(null)); - assertFalse(DefaultDBTable.isDelimited("")); - assertFalse(DefaultDBTable.isDelimited("foo")); - assertFalse(DefaultDBTable.isDelimited("\"foo")); - assertFalse(DefaultDBTable.isDelimited("foo\"")); - assertFalse(DefaultDBTable.isDelimited("\"foo\".\"bar\"")); + public void testDefaultDBTableStringString() { + // CASE: No DN name, no case sensitivity + DefaultDBTable table = new DefaultDBTable("adqlName"); + assertEquals("adqlName", table.getADQLName()); + assertEquals(table.getADQLName(), table.getDBName()); + assertFalse(table.isCaseSensitive()); + + // CASE: No DB name, case sensitivity + table = new DefaultDBTable("\"adqlName\""); + assertEquals("adqlName", table.getADQLName()); + assertEquals(table.getADQLName(), table.getDBName()); + assertTrue(table.isCaseSensitive()); + + // CASE: DB name, no case sensitivity + table = new DefaultDBTable("adqlName", "dbName"); + assertEquals("adqlName", table.getADQLName()); + assertEquals("dbName", table.getDBName()); + assertFalse(table.isCaseSensitive()); + + // CASE: DB name, case sensitivity + table = new DefaultDBTable("\"adqlName\"", "dbName"); + assertEquals("adqlName", table.getADQLName()); + assertEquals("dbName", table.getDBName()); + assertTrue(table.isCaseSensitive()); } @Test public void testSetADQLName() { - DefaultDBTable table = new DefaultDBTable("dbName"); - assertEquals(table.getDBName(), table.getADQLName()); + // CASE: no case sensitivity, no DB name + DefaultDBTable table = new DefaultDBTable("adqlName"); + assertEquals("adqlName", table.getADQLName()); + assertEquals(table.getADQLName(), table.getDBName()); assertFalse(table.isCaseSensitive()); // CASE: undelimited name => OK @@ -39,9 +49,9 @@ public class TestDefaultDBTable { assertEquals("myTable", table.getADQLName()); assertFalse(table.isCaseSensitive()); - // CASE: No name => use the DBName - table.setADQLName(null); - assertEquals(table.getDBName(), table.getADQLName()); + // CASE: No DB name => use the ADQLName + table.setDBName(null); + assertEquals(table.getADQLName(), table.getDBName()); assertFalse(table.isCaseSensitive()); // CASE: delimited name => stored undelimited @@ -49,22 +59,26 @@ public class TestDefaultDBTable { assertEquals("MyTable", table.getADQLName()); assertTrue(table.isCaseSensitive()); - // CASE: Empty string => use the DBName (as name=NULL) - table.setADQLName(""); - assertEquals(table.getDBName(), table.getADQLName()); - assertFalse(table.isCaseSensitive()); + // CASE: Empty string => use the ADQLName (as name=NULL) + table.setDBName(""); + assertEquals(table.getADQLName(), table.getDBName()); + assertTrue(table.isCaseSensitive()); - // CASE: dbName delimited and no ADQL name => adqlName = undelimited dbName - table = new DefaultDBTable("\"DBName\""); - table.setADQLName(null); - assertEquals("DBName", table.getADQLName()); + // CASE: adqlName delimited and no DB name => dbName = undelimited adqlName + table = new DefaultDBTable("\"ADQLName\""); + table.setDBName(null); + assertEquals("ADQLName", table.getDBName()); assertTrue(table.isCaseSensitive()); - // CASE: dbName delimited but empty and no ADQL name => adqlName = delimited dbName - table = new DefaultDBTable("\" \""); - table.setADQLName(null); - assertEquals(table.getDBName(), table.getADQLName()); - assertFalse(table.isCaseSensitive()); + // CASE: missing DB name => ERROR! + for(String n : new String[]{ null, "", " ", "\"\"", "\" \"" }) { + try { + new DefaultDBTable(n); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing ADQL name!", ex.getMessage()); + } + } } } diff --git a/test/adql/db/TestIdentifierCaseSensitivity.java b/test/adql/db/TestIdentifierCaseSensitivity.java new file mode 100644 index 0000000000000000000000000000000000000000..dc7a0b77142b5638259b807e53c2b6ec0ced7e24 --- /dev/null +++ b/test/adql/db/TestIdentifierCaseSensitivity.java @@ -0,0 +1,463 @@ +package adql.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import adql.db.exception.UnresolvedIdentifiersException; +import adql.parser.ADQLParser; +import adql.parser.ADQLParser.ADQLVersion; +import adql.query.ADQLQuery; +import adql.translator.PostgreSQLTranslator; + +public class TestIdentifierCaseSensitivity { + + @Test + public void testDBTables() { + List<DBTable> testTables = new ArrayList<DBTable>(2); + testTables.add(new DefaultDBTable("NCS_ADQLTable", "dbTable1")); + testTables.add(new DefaultDBTable("\"CS_ADQLTable\"", "dbTable2")); + + for(ADQLVersion adqlVersion : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(adqlVersion); + parser.setQueryChecker(new DBChecker(testTables)); + + /* CASES: NON-CASE SENSITIVE TABLE NAME. + * It should match only if: + * - in ANY case NOT between double quotes + * - OR in LOWER case BETWEEN double quotes. */ + try { + assertNotNull(parser.parseQuery("SELECT * FROM ncs_adqltable")); + assertNotNull(parser.parseQuery("SELECT * FROM NCS_ADQLTABLE")); + assertNotNull(parser.parseQuery("SELECT * FROM NCS_ADQLTable")); + assertNotNull(parser.parseQuery("SELECT * FROM \"ncs_adqltable\"")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This non-case sensitive table should have matched! (see console for more details)"); + } + try { + parser.parseQuery("SELECT * FROM \"NCS_ADQLTable\""); + fail("[ADQL-" + adqlVersion + "] The table name is NOT case sensitive. This test should have failed if the table name is not fully in lowercase."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: NCS_ADQLTable [l.1 c.15 - l.1 c.30]!\n - Unknown table \"\"NCS_ADQLTable\"\" !", ex.getMessage()); + } + + /* CASES: CASE SENSITIVE TABLE NAME. + * It should match only if: + * - in ANY case NOT between double quotes (if non-ambiguous name) + * - OR in EXACT case BETWEEN double quotes. */ + try { + assertNotNull(parser.parseQuery("SELECT * FROM cs_adqltable")); + assertNotNull(parser.parseQuery("SELECT * FROM CS_ADQLTABLE")); + assertNotNull(parser.parseQuery("SELECT * FROM CS_ADQLTable")); + assertNotNull(parser.parseQuery("SELECT * FROM \"CS_ADQLTable\"")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This case sensitive table should have matched! (see console for more details)"); + } + try { + parser.parseQuery("SELECT * FROM \"cs_adqltable\""); + fail("[ADQL-" + adqlVersion + "] The table name is case sensitive. This test should have failed if the table name is not written with the exact case."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: cs_adqltable [l.1 c.15 - l.1 c.29]!\n - Unknown table \"\"cs_adqltable\"\" !", ex.getMessage()); + } + } + } + + @Test + public void testDBColumns() { + DefaultDBTable testT = new DefaultDBTable("adqlTable", "dbTableName"); + testT.addColumn(new DefaultDBColumn("NCS_ADQLColumn", "dbCol1", testT)); + testT.addColumn(new DefaultDBColumn("\"CS_ADQLColumn\"", "dbCol2", testT)); + + List<DBTable> testTables = new ArrayList<DBTable>(1); + testTables.add(testT); + + for(ADQLVersion adqlVersion : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(adqlVersion); + parser.setQueryChecker(new DBChecker(testTables)); + + /* CASES: NON-CASE SENSITIVE COLUMN NAME. + * It should match only if: + * - in ANY case NOT between double quotes + * - OR in LOWER case BETWEEN double quotes. */ + try { + assertNotNull(parser.parseQuery("SELECT ncs_adqlcolumn FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT NCS_ADQLCOLUMN FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT NCS_ADQLColumn FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT \"ncs_adqlcolumn\" FROM adqltable")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This non-case sensitive column should have matched! (see console for more details)"); + } + try { + parser.parseQuery("SELECT \"NCS_ADQLColumn\" FROM adqltable"); + fail("[ADQL-" + adqlVersion + "] The column name is NOT case sensitive. This test should have failed if the column name is not fully in lowercase."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: NCS_ADQLColumn [l.1 c.8 - l.1 c.24]!\n - Unknown column \"\"NCS_ADQLColumn\"\" !", ex.getMessage()); + } + + /* CASES: CASE SENSITIVE COLUMN NAME. + * It should match only if: + * - in ANY case NOT between double quotes (if non-ambiguous name) + * - OR in EXACT case BETWEEN double quotes. */ + try { + assertNotNull(parser.parseQuery("SELECT cs_adqlcolumn FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT CS_ADQLCOLUMN FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT CS_ADQLColumn FROM adqltable")); + assertNotNull(parser.parseQuery("SELECT \"CS_ADQLColumn\" FROM adqltable")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This case sensitive column should have matched! (see console for more details)"); + } + try { + parser.parseQuery("SELECT \"cs_adqlcolumn\" FROM adqltable"); + fail("[ADQL-" + adqlVersion + "] The column name is case sensitive. This test should have failed if the column name is not written with the exact case."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: cs_adqlcolumn [l.1 c.8 - l.1 c.23]!\n - Unknown column \"\"cs_adqlcolumn\"\" !", ex.getMessage()); + } + } + } + + @Test + public void testFROMNames() { + List<DBTable> testTables = new ArrayList<DBTable>(); + testTables.add(new DefaultDBTable("adqlTable", "dbTable")); + + for(ADQLVersion adqlVersion : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(adqlVersion); + parser.setQueryChecker(new DBChecker(testTables)); + + /* CASES: NON-CASE SENSITIVE TABLE NAME. + * (see testDBTables()) */ + try { + // SUB-CASE: table reference + assertNotNull(parser.parseQuery("SELECT atable.* FROM adqltable AS aTable")); + assertNotNull(parser.parseQuery("SELECT ATABLE.* FROM adqltable AS aTable")); + assertNotNull(parser.parseQuery("SELECT aTable.* FROM adqltable AS aTable")); + assertNotNull(parser.parseQuery("SELECT \"atable\".* FROM adqltable AS aTable")); + // SUB-CASE: sub-query + assertNotNull(parser.parseQuery("SELECT atable.* FROM (SELECT * FROM adqltable) AS aTable")); + assertNotNull(parser.parseQuery("SELECT ATABLE.* FROM (SELECT * FROM adqltable) AS aTable")); + assertNotNull(parser.parseQuery("SELECT aTable.* FROM (SELECT * FROM adqltable) AS aTable")); + assertNotNull(parser.parseQuery("SELECT \"atable\".* FROM (SELECT * FROM adqltable) AS aTable")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This non-case sensitive table should have matched! (see console for more details)"); + } + try { + // SUB-CASE: table reference + parser.parseQuery("SELECT \"aTable\".* FROM adqltable AS aTable"); + fail("[ADQL-" + adqlVersion + "] The table name is NOT case sensitive. This test should have failed if the table name is not fully in lowercase."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: aTable [l.1 c.8 - l.1 c.16]!\n - Unknown table \"\"aTable\"\" !", ex.getMessage()); + } + try { + // SUB-CASE: sub-query + parser.parseQuery("SELECT \"aTable\".* FROM (SELECT * FROM adqltable) AS aTable"); + fail("[ADQL-" + adqlVersion + "] The table name is NOT case sensitive. This test should have failed if the table name is not fully in lowercase."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: aTable [l.1 c.8 - l.1 c.16]!\n - Unknown table \"\"aTable\"\" !", ex.getMessage()); + } + + /* CASES: CASE SENSITIVE TABLE NAME. + * (see testDBTables()) */ + try { + // SUB-CASE: table reference + assertNotNull(parser.parseQuery("SELECT atable.* FROM adqltable AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT ATABLE.* FROM adqltable AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT aTable.* FROM adqltable AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT \"aTable\".* FROM adqltable AS \"aTable\"")); + // SUB-CASE: sub-query + assertNotNull(parser.parseQuery("SELECT atable.* FROM (SELECT * FROM adqltable) AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT ATABLE.* FROM (SELECT * FROM adqltable) AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT aTable.* FROM (SELECT * FROM adqltable) AS \"aTable\"")); + assertNotNull(parser.parseQuery("SELECT \"aTable\".* FROM (SELECT * FROM adqltable) AS \"aTable\"")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("[ADQL-" + adqlVersion + "] This case sensitive table should have matched! (see console for more details)"); + } + try { + // SUB-CASE: table reference + parser.parseQuery("SELECT \"atable\".* FROM adqltable AS \"aTable\""); + fail("[ADQL-" + adqlVersion + "] The table name is case sensitive. This test should have failed if the table name is not written with the exact case."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: atable [l.1 c.8 - l.1 c.16]!\n - Unknown table \"\"atable\"\" !", ex.getMessage()); + } + try { + // SUB-CASE: sub-query + parser.parseQuery("SELECT \"atable\".* FROM (SELECT * FROM adqltable) AS \"aTable\""); + fail("[ADQL-" + adqlVersion + "] The table name is case sensitive. This test should have failed if the table name is not written with the exact case."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: atable [l.1 c.8 - l.1 c.16]!\n - Unknown table \"\"atable\"\" !", ex.getMessage()); + } + } + } + + @Test + public void testCTENames() { + List<DBTable> testTables = new ArrayList<DBTable>(); + testTables.add(new DefaultDBTable("adqlTable", "dbTable")); + + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + parser.setQueryChecker(new DBChecker(testTables)); + + /* CASES: NON-CASE SENSITIVE TABLE NAME. + * (see testDBTables()) */ + try { + assertNotNull(parser.parseQuery("WITH aTable AS (SELECT * FROM adqltable) SELECT * FROM atable")); + assertNotNull(parser.parseQuery("WITH aTable AS (SELECT * FROM adqltable) SELECT * FROM ATABLE")); + assertNotNull(parser.parseQuery("WITH aTable AS (SELECT * FROM adqltable) SELECT * FROM aTable")); + assertNotNull(parser.parseQuery("WITH aTable AS (SELECT * FROM adqltable) SELECT * FROM \"atable\"")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("This non-case sensitive table name should have matched! (see console for more details)"); + } + try { + parser.parseQuery("WITH aTable AS (SELECT * FROM adqltable) SELECT * FROM \"aTable\""); + fail("The table name is NOT case sensitive. This test should have failed if the table name is not fully in lowercase."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: aTable [l.1 c.56 - l.1 c.64]!\n - Unknown table \"\"aTable\"\" !", ex.getMessage()); + } + + /* CASES: CASE SENSITIVE TABLE NAME. + * (see testDBTables()) */ + try { + assertNotNull(parser.parseQuery("WITH \"aTable\" AS (SELECT * FROM adqltable) SELECT * FROM atable")); + assertNotNull(parser.parseQuery("WITH \"aTable\" AS (SELECT * FROM adqltable) SELECT * FROM ATABLE")); + assertNotNull(parser.parseQuery("WITH \"aTable\" AS (SELECT * FROM adqltable) SELECT * FROM aTable")); + assertNotNull(parser.parseQuery("WITH \"aTable\" AS (SELECT * FROM adqltable) SELECT * FROM \"aTable\"")); + } catch(Exception ex) { + ex.printStackTrace(); + fail("This case sensitive table should have matched! (see console for more details)"); + } + try { + parser.parseQuery("WITH \"aTable\" AS (SELECT * FROM adqltable) SELECT * FROM \"atable\""); + fail("The table name is case sensitive. This test should have failed if the table name is not written with the exact case."); + } catch(Exception ex) { + assertEquals(UnresolvedIdentifiersException.class, ex.getClass()); + assertEquals("1 unresolved identifiers: atable [l.1 c.58 - l.1 c.66]!\n - Unknown table \"\"atable\"\" !", ex.getMessage()); + } + } + + @Test + public void testTranslateDBTables() { + List<DBTable> testTables = new ArrayList<DBTable>(4); + testTables.add(new DefaultDBTable("Table1", "dbTable1")); + testTables.add(new DefaultDBTable("\"Table2\"", "dbTable2")); + testTables.add(new DefaultDBTable("Table3")); + testTables.add(new DefaultDBTable("\"Table4\"")); + + for(ADQLVersion version : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(version); + parser.setQueryChecker(new DBChecker(testTables)); + PostgreSQLTranslator trCS = new PostgreSQLTranslator(true); + PostgreSQLTranslator trCI = new PostgreSQLTranslator(false); + + // CASE: Non-case-sensitive ADQL name, Specified DB name: + try { + ADQLQuery query = parser.parseQuery("SELECT * FROM table1"); + assertEquals("SELECT *\nFROM table1", query.toADQL()); + assertEquals("SELECT *\nFROM \"dbTable1\"", trCS.translate(query)); + assertEquals("SELECT *\nFROM dbTable1", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Case-sensitive ADQL name, Specified DB name: + try { + ADQLQuery query = parser.parseQuery("SELECT * FROM \"Table2\""); + assertEquals("SELECT *\nFROM \"Table2\"", query.toADQL()); + assertEquals("SELECT *\nFROM \"dbTable2\"", trCS.translate(query)); + assertEquals("SELECT *\nFROM dbTable2", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Non-case-sensitive ADQL name, UNspecified DB name: + try { + ADQLQuery query = parser.parseQuery("SELECT * FROM table3"); + assertEquals("SELECT *\nFROM table3", query.toADQL()); + assertEquals("SELECT *\nFROM \"Table3\"", trCS.translate(query)); + assertEquals("SELECT *\nFROM Table3", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Case-sensitive ADQL name, UNspecified DB name: + try { + ADQLQuery query = parser.parseQuery("SELECT * FROM table4"); + assertEquals("SELECT *\nFROM table4", query.toADQL()); + assertEquals("SELECT *\nFROM \"Table4\"", trCS.translate(query)); + assertEquals("SELECT *\nFROM Table4", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + } + } + + @Test + public void testTranslateFROMNames() { + List<DBTable> testTables = new ArrayList<DBTable>(1); + DefaultDBTable t = new DefaultDBTable("Table1", "dbTable1"); + t.addColumn(new DefaultDBColumn("col1", t)); + testTables.add(t); + + for(ADQLVersion version : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(version); + parser.setQueryChecker(new DBChecker(testTables)); + PostgreSQLTranslator trCS = new PostgreSQLTranslator(true); + PostgreSQLTranslator trCI = new PostgreSQLTranslator(false); + + // CASE: Non-case-sensitive lower-case name: + try { + ADQLQuery query = parser.parseQuery("SELECT T1.*, t1.col1 FROM table1 AS t1"); + assertEquals("SELECT T1.* , t1.col1\nFROM table1 AS t1", query.toADQL()); + assertEquals("SELECT \"t1\".\"col1\" AS \"col1\" , \"t1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\" AS \"t1\"", trCS.translate(query)); + assertEquals("SELECT t1.col1 AS \"col1\" , t1.col1 AS \"col1\"\nFROM dbTable1 AS \"t1\"", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Non-case-sensitive mixed-case name: + try { + ADQLQuery query = parser.parseQuery("SELECT T1.*, t1.col1 FROM table1 AS T1"); + assertEquals("SELECT T1.* , t1.col1\nFROM table1 AS T1", query.toADQL()); + assertEquals("SELECT \"t1\".\"col1\" AS \"col1\" , \"t1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\" AS \"t1\"", trCS.translate(query)); + assertEquals("SELECT t1.col1 AS \"col1\" , t1.col1 AS \"col1\"\nFROM dbTable1 AS \"t1\"", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Case-sensitive ADQL name: + try { + ADQLQuery query = parser.parseQuery("SELECT T1.*, t1.col1 FROM table1 AS \"T1\""); + assertEquals("SELECT T1.* , t1.col1\nFROM table1 AS \"T1\"", query.toADQL()); + assertEquals("SELECT \"T1\".\"col1\" AS \"col1\" , \"T1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\" AS \"T1\"", trCS.translate(query)); + assertEquals("SELECT T1.col1 AS \"col1\" , T1.col1 AS \"col1\"\nFROM dbTable1 AS \"T1\"", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + } + } + + @Test + public void testTranslateCTENames() { + List<DBTable> testTables = new ArrayList<DBTable>(1); + DefaultDBTable t = new DefaultDBTable("Table1", "dbTable1"); + t.addColumn(new DefaultDBColumn("col1", t)); + testTables.add(t); + + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + parser.setQueryChecker(new DBChecker(testTables)); + PostgreSQLTranslator trCS = new PostgreSQLTranslator(true); + PostgreSQLTranslator trCI = new PostgreSQLTranslator(false); + + // CASE: Non-case-sensitive lower-case name: + try { + ADQLQuery query = parser.parseQuery("WITH t1 AS (SELECT * FROM table1) SELECT * FROM t1"); + assertEquals("WITH t1 AS (\nSELECT *\nFROM table1\n)\nSELECT *\nFROM t1", query.toADQL()); + assertEquals("WITH \"t1\"(\"col1\") AS (\nSELECT \"dbTable1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\"\n)\nSELECT \"t1\".\"col1\" AS \"col1\"\nFROM \"t1\"", trCS.translate(query)); + assertEquals("WITH \"t1\"(\"col1\") AS (\nSELECT dbTable1.col1 AS \"col1\"\nFROM dbTable1\n)\nSELECT t1.col1 AS \"col1\"\nFROM t1", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Non-case-sensitive mixed-case name: + try { + ADQLQuery query = parser.parseQuery("WITH T1 AS (SELECT * FROM table1) SELECT * FROM t1"); + assertEquals("WITH T1 AS (\nSELECT *\nFROM table1\n)\nSELECT *\nFROM t1", query.toADQL()); + assertEquals("WITH \"t1\"(\"col1\") AS (\nSELECT \"dbTable1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\"\n)\nSELECT \"t1\".\"col1\" AS \"col1\"\nFROM \"t1\"", trCS.translate(query)); + assertEquals("WITH \"t1\"(\"col1\") AS (\nSELECT dbTable1.col1 AS \"col1\"\nFROM dbTable1\n)\nSELECT t1.col1 AS \"col1\"\nFROM t1", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Case-sensitive ADQL name: + try { + ADQLQuery query = parser.parseQuery("WITH \"T1\" AS (SELECT * FROM table1) SELECT * FROM t1"); + assertEquals("WITH \"T1\" AS (\nSELECT *\nFROM table1\n)\nSELECT *\nFROM t1", query.toADQL()); + assertEquals("WITH \"T1\"(\"col1\") AS (\nSELECT \"dbTable1\".\"col1\" AS \"col1\"\nFROM \"dbTable1\"\n)\nSELECT \"T1\".\"col1\" AS \"col1\"\nFROM \"T1\"", trCS.translate(query)); + assertEquals("WITH \"T1\"(\"col1\") AS (\nSELECT dbTable1.col1 AS \"col1\"\nFROM dbTable1\n)\nSELECT T1.col1 AS \"col1\"\nFROM T1", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + } + + @Test + public void testTranslateDBColumns() { + List<DBTable> testTables = new ArrayList<DBTable>(1); + DefaultDBTable table = new DefaultDBTable("Table1", "dbTable1"); + table.addColumn(new DefaultDBColumn("Col1", table)); + table.addColumn(new DefaultDBColumn("\"Col2\"", table)); + testTables.add(table); + + PostgreSQLTranslator trCS = new PostgreSQLTranslator(true); + PostgreSQLTranslator trCI = new PostgreSQLTranslator(false); + + for(ADQLVersion version : ADQLVersion.values()) { + ADQLParser parser = new ADQLParser(version); + parser.setQueryChecker(new DBChecker(testTables)); + + // CASE: Columns from database: + try { + ADQLQuery query = parser.parseQuery("SELECT COL1, col2, col1 AS \"SuperCol\" FROM table1"); + assertEquals("SELECT COL1 , col2 , col1 AS \"SuperCol\"\nFROM table1", query.toADQL()); + assertEquals("SELECT \"dbTable1\".\"Col1\" AS \"col1\" , \"dbTable1\".\"Col2\" AS \"Col2\" , \"dbTable1\".\"Col1\" AS \"SuperCol\"\nFROM \"dbTable1\"", trCS.translate(query)); + assertEquals("SELECT dbTable1.Col1 AS \"col1\" , dbTable1.Col2 AS \"Col2\" , dbTable1.Col1 AS \"SuperCol\"\nFROM dbTable1", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Columns from subquery + try { + ADQLQuery query = parser.parseQuery("SELECT * FROM (SELECT col1, col2, col1 AS Col3, col2 AS \"COL4\" FROM table1) AS table2"); + assertEquals("SELECT *\nFROM (SELECT col1 , col2 , col1 AS Col3 , col2 AS \"COL4\"\nFROM table1) AS table2", query.toADQL()); + assertEquals("SELECT \"table2\".\"Col1\" AS \"col1\" , \"table2\".\"Col2\" AS \"Col2\" , \"table2\".\"col3\" AS \"col3\" , \"table2\".\"COL4\" AS \"COL4\"\nFROM (SELECT \"dbTable1\".\"Col1\" AS \"col1\" , \"dbTable1\".\"Col2\" AS \"Col2\" , \"dbTable1\".\"Col1\" AS \"col3\" , \"dbTable1\".\"Col2\" AS \"COL4\"\nFROM \"dbTable1\") AS \"table2\"", trCS.translate(query)); + assertEquals("SELECT table2.Col1 AS \"col1\" , table2.Col2 AS \"Col2\" , table2.col3 AS \"col3\" , table2.COL4 AS \"COL4\"\nFROM (SELECT dbTable1.Col1 AS \"col1\" , dbTable1.Col2 AS \"Col2\" , dbTable1.Col1 AS \"col3\" , dbTable1.Col2 AS \"COL4\"\nFROM dbTable1) AS \"table2\"", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + + // CASE: Columns from CTE + if (parser.getADQLVersion() != ADQLVersion.V2_0) { + try { + ADQLQuery query = parser.parseQuery("WITH table3 AS (SELECT COL1, col2, col1 AS \"SuperCol\" FROM table1) SELECT * FROM table3"); + assertEquals("WITH table3 AS (\nSELECT COL1 , col2 , col1 AS \"SuperCol\"\nFROM table1\n)\nSELECT *\nFROM table3", query.toADQL()); + assertEquals("WITH \"table3\"(\"col1\",\"Col2\",\"SuperCol\") AS (\nSELECT \"dbTable1\".\"Col1\" AS \"col1\" , \"dbTable1\".\"Col2\" AS \"Col2\" , \"dbTable1\".\"Col1\" AS \"SuperCol\"\nFROM \"dbTable1\"\n)\nSELECT \"table3\".\"col1\" AS \"col1\" , \"table3\".\"Col2\" AS \"Col2\" , \"table3\".\"SuperCol\" AS \"SuperCol\"\nFROM \"table3\"", trCS.translate(query)); + assertEquals("WITH \"table3\"(\"col1\",\"Col2\",\"SuperCol\") AS (\nSELECT dbTable1.Col1 AS \"col1\" , dbTable1.Col2 AS \"Col2\" , dbTable1.Col1 AS \"SuperCol\"\nFROM dbTable1\n)\nSELECT table3.col1 AS \"col1\" , table3.Col2 AS \"Col2\" , table3.SuperCol AS \"SuperCol\"\nFROM table3", trCI.translate(query)); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception! (see console for more details)"); + } + } + } + } + +} diff --git a/test/adql/db/TestSubQueries.java b/test/adql/db/TestSubQueries.java index ff9e8283eb96aa48832e1ba7993b14319ef68b28..17b63a3e954000e5612988948b858a732c2ad329 100644 --- a/test/adql/db/TestSubQueries.java +++ b/test/adql/db/TestSubQueries.java @@ -58,6 +58,7 @@ public class TestSubQueries { adqlParser.setQueryChecker(new DBChecker(esaTables)); ADQLQuery query = adqlParser.parseQuery("SELECT oid FROM table1 as MyAlias WHERE oid IN (SELECT oid2 FROM table2 WHERE oid2 = myAlias.oid)"); + assertEquals("SELECT oid\nFROM table1 AS MyAlias\nWHERE oid IN (SELECT oid2\nFROM table2\nWHERE oid2 = myAlias.oid)", query.toADQL()); assertEquals("SELECT \"myalias\".\"oid\" AS \"oid\"\nFROM \"public\".\"table1\" AS \"myalias\"\nWHERE \"myalias\".\"oid\" IN (SELECT \"public\".\"table2\".\"oid2\" AS \"oid2\"\nFROM \"public\".\"table2\"\nWHERE \"public\".\"table2\".\"oid2\" = \"myalias\".\"oid\")", (new PostgreSQLTranslator()).translate(query)); } catch(Exception ex) { ex.printStackTrace(System.err); @@ -79,7 +80,8 @@ public class TestSubQueries { adqlParser.setQueryChecker(new DBChecker(esaTables)); ADQLQuery query = adqlParser.parseQuery("SELECT t.* FROM (SELECT (ra+ra_error) AS x, (dec+dec_error) AS Y, pmra AS \"ProperMotion\" FROM table2) AS t"); - assertEquals("SELECT \"t\".\"x\" AS \"x\",\"t\".\"y\" AS \"y\",\"t\".\"ProperMotion\" AS \"ProperMotion\"\nFROM (SELECT ((\"public\".\"table2\".\"ra\"+\"public\".\"table2\".\"ra_error\")) AS \"x\" , ((\"public\".\"table2\".\"dec\"+\"public\".\"table2\".\"dec_error\")) AS \"y\" , \"public\".\"table2\".\"pmra\" AS \"ProperMotion\"\nFROM \"public\".\"table2\") AS \"t\"", (new PostgreSQLTranslator()).translate(query)); + assertEquals("SELECT t.*\nFROM (SELECT (ra+ra_error) AS x , (dec+dec_error) AS Y , pmra AS \"ProperMotion\"\nFROM table2) AS t", query.toADQL()); + assertEquals("SELECT \"t\".\"x\" AS \"x\" , \"t\".\"y\" AS \"y\" , \"t\".\"ProperMotion\" AS \"ProperMotion\"\nFROM (SELECT ((\"public\".\"table2\".\"ra\"+\"public\".\"table2\".\"ra_error\")) AS \"x\" , ((\"public\".\"table2\".\"dec\"+\"public\".\"table2\".\"dec_error\")) AS \"y\" , \"public\".\"table2\".\"pmra\" AS \"ProperMotion\"\nFROM \"public\".\"table2\") AS \"t\"", (new PostgreSQLTranslator()).translate(query)); } catch(Exception ex) { ex.printStackTrace(System.err); fail("No error expected! (see console for more details)"); diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java index 6743b8ec66d6eb2d2323e680d7c46f1eeb7d3813..c7c2002feca498e4f504069c64fd7b64ce0a856c 100644 --- a/test/adql/parser/TestADQLParser.java +++ b/test/adql/parser/TestADQLParser.java @@ -1,7 +1,9 @@ package adql.parser; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -24,6 +26,7 @@ import adql.parser.grammar.ADQLGrammar200Constants; import adql.parser.grammar.ParseException; import adql.parser.grammar.Token; import adql.query.ADQLQuery; +import adql.query.WithItem; import adql.query.from.ADQLJoin; import adql.query.from.ADQLTable; import adql.query.operand.ADQLOperand; @@ -55,6 +58,83 @@ public class TestADQLParser { public void tearDown() throws Exception { } + @Test + public void testWithClause() { + + // CASE: ADQL-2.0 => ERROR + ADQLParser parser = new ADQLParser(ADQLVersion.V2_0); + try { + parser.parseQuery("WITH foo AS (SELECT * FROM bar) SELECT * FROM foo"); + fail("In ADQL-2.0, the WITH should not be allowed....it does not exist!"); + } catch(Exception ex) { + assertEquals(ParseException.class, ex.getClass()); + assertEquals(" Encountered \"WITH\". Was expecting: \"SELECT\" \n" + "(HINT: \"WITH\" is not supported in ADQL v2.0, but is however a reserved word. To use it as a column/table/schema name/alias, write it between double quotes.)", ex.getMessage()); + } + + parser = new ADQLParser(ADQLVersion.V2_1); + try { + + // CASE: Same with ADQL-2.1 => OK + ADQLQuery query = parser.parseQuery("WITH foo AS (SELECT * FROM bar) SELECT * FROM foo"); + assertNotNull(query.getWith()); + assertEquals(1, query.getWith().size()); + WithItem item = query.getWith().get(0); + assertEquals("foo", item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNull(item.getColumnLabels()); + assertEquals("SELECT *\nFROM bar", item.getQuery().toADQL()); + + // CASE: WITH clause with column labels => OK + query = parser.parseQuery("WITH foo(id, ra, dec) AS (SELECT col1, col2, col3 FROM bar) SELECT * FROM foo"); + assertNotNull(query.getWith()); + assertEquals(1, query.getWith().size()); + item = query.getWith().get(0); + assertEquals("foo", item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNotNull(item.getColumnLabels()); + assertEquals(3, item.getColumnLabels().size()); + assertEquals("SELECT col1 , col2 , col3\nFROM bar", item.getQuery().toADQL()); + + // CASE: more than 1 WITH clause + CTE's label case sensitivity + query = parser.parseQuery("WITH foo(id, ra, dec) AS (SELECT col1, col2, col3 FROM bar), \"Foo2\" AS (SELECT * FROM bar2) SELECT * FROM foo NATURAL JOIN \"Foo2\""); + assertNotNull(query.getWith()); + assertEquals(2, query.getWith().size()); + item = query.getWith().get(0); + assertEquals("foo", item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNotNull(item.getColumnLabels()); + assertEquals(3, item.getColumnLabels().size()); + assertEquals("SELECT col1 , col2 , col3\nFROM bar", item.getQuery().toADQL()); + item = query.getWith().get(1); + assertEquals("Foo2", item.getLabel()); + assertTrue(item.isLabelCaseSensitive()); + assertNull(item.getColumnLabels()); + assertEquals("SELECT *\nFROM bar2", item.getQuery().toADQL()); + + // CASE: WITH clause inside a WITH clause => OK + query = parser.parseQuery("WITH foo(id, ra, dec) AS (WITH innerFoo AS (SELECT col1, col2, col3 FROM bar) SELECT * FROM stars NATURAL JOIN innerFoo) SELECT * FROM foo"); + assertNotNull(query.getWith()); + assertEquals(1, query.getWith().size()); + item = query.getWith().get(0); + assertEquals("foo", item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNotNull(item.getColumnLabels()); + assertEquals(3, item.getColumnLabels().size()); + assertEquals("WITH innerFoo AS (\nSELECT col1 , col2 , col3\nFROM bar\n)\nSELECT *\nFROM stars NATURAL INNER JOIN innerFoo", item.getQuery().toADQL()); + assertNotNull(query.getWith().get(0).getQuery().getWith()); + assertEquals(1, query.getWith().get(0).getQuery().getWith().size()); + item = query.getWith().get(0).getQuery().getWith().get(0); + assertEquals("innerFoo", item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNull(item.getColumnLabels()); + assertEquals("SELECT col1 , col2 , col3\nFROM bar", item.getQuery().toADQL()); + + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected error while parsing a valid query with a WITH clause! (see console for more details)"); + } + } + @Test public void testConstraintList() { for(ADQLVersion version : ADQLVersion.values()) { diff --git a/test/adql/query/TestADQLObjectPosition.java b/test/adql/query/TestADQLObjectPosition.java index 772e4072c83f6176ab1abf91fa453d53ad5d039b..26fb5fbd274944fccec25a2f2d9f41acbf0d7a28 100644 --- a/test/adql/query/TestADQLObjectPosition.java +++ b/test/adql/query/TestADQLObjectPosition.java @@ -31,12 +31,15 @@ public class TestADQLObjectPosition { public void testPositionInAllClauses() { for(ADQLVersion version : ADQLVersion.values()) { try { - ADQLQuery query = new ADQLParser(version).parseQuery("SELECT truc, bidule.machin, toto(truc, chose) AS \"super\" FROM foo JOIN bidule USING(id) WHERE truc > 12.5 AND bidule.machin < 5 GROUP BY chose HAVING try > 0 ORDER BY chouetteAlors, 2 DESC"); + ADQLQuery query = new ADQLParser(version).parseQuery((version != ADQLVersion.V2_0 ? "WITH bar AS (SELECT * FROM superbar) " : "") + "SELECT truc, bidule.machin, toto(truc, chose) AS \"super\" FROM foo JOIN bidule USING(id) WHERE truc > 12.5 AND bidule.machin < 5 GROUP BY chose HAVING try > 0 ORDER BY chouetteAlors, 2 DESC"); Iterator<ADQLObject> results = query.search(new SimpleSearchHandler(true) { @Override protected boolean match(ADQLObject obj) { - return obj.getPosition() == null; + if (obj instanceof ADQLList<?> && ((ADQLList<?>)obj).isEmpty()) + return false; + else + return obj.getPosition() == null; } }); if (results.hasNext()) { diff --git a/test/adql/query/TestWithItem.java b/test/adql/query/TestWithItem.java new file mode 100644 index 0000000000000000000000000000000000000000..57ca9ed00969c77313e9806cf2585320be126fb8 --- /dev/null +++ b/test/adql/query/TestWithItem.java @@ -0,0 +1,154 @@ +package adql.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; + +import org.junit.Test; + +import adql.db.DBColumn; +import adql.parser.ADQLParser; +import adql.parser.ADQLParser.ADQLVersion; +import adql.parser.grammar.ParseException; +import adql.query.operand.ADQLColumn; + +public class TestWithItem { + + @Test + public void testWithItemStringADQLQuery() { + // CASE: No label => ERROR! + String[] toTest = new String[]{ null, "", " " }; + for(String label : toTest) { + try { + new WithItem(label, null); + fail("It should be impossible to create a WithItem without a label!"); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing label of the WITH item!", ex.getMessage()); + } + } + + // CASE: No query => ERROR! + try { + new WithItem("query", null); + fail("It should be impossible to create a WithItem without a query!"); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing query of the WITH item!", ex.getMessage()); + } + + // CASE: label + query => OK! + final String WITH_LABEL = "aNamedQuery"; + WithItem item = new WithItem(WITH_LABEL, new ADQLQuery()); + assertEquals(WITH_LABEL, item.getLabel()); + assertFalse(item.isLabelCaseSensitive()); + assertNotNull(item.getQuery()); + assertNull(item.getColumnLabels()); + + // CASE: label + query => OK! + item = new WithItem("\"" + WITH_LABEL + "\"", new ADQLQuery()); + assertEquals(WITH_LABEL, item.getLabel()); + assertTrue(item.isLabelCaseSensitive()); + assertNotNull(item.getQuery()); + assertNull(item.getColumnLabels()); + } + + @Test + public void testWithItemStringADQLQueryCollectionOfIdentifierItem() { + + // CASE: No label => ERROR! + String[] toTest = new String[]{ null, "", " " }; + for(String label : toTest) { + try { + new WithItem(label, null, null); + fail("It should be impossible to create a WithItem without a label!"); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing label of the WITH item!", ex.getMessage()); + } + } + + // CASE: No query => ERROR! + try { + new WithItem("query", null, null); + fail("It should be impossible to create a WithItem without a query!"); + } catch(Exception ex) { + assertEquals(NullPointerException.class, ex.getClass()); + assertEquals("Missing query of the WITH item!", ex.getMessage()); + } + + // CASE: label + query but no col. label => OK! + final String WITH_LABEL = "aNamedQuery"; + WithItem item = new WithItem(WITH_LABEL, new ADQLQuery(), null); + assertEquals(WITH_LABEL, item.getLabel()); + assertNotNull(item.getQuery()); + assertNull(item.getColumnLabels()); + + // CASE: label + query + col. labels => OK! + item = new WithItem(WITH_LABEL, new ADQLQuery(), Arrays.asList(new ADQLColumn[]{ new ADQLColumn("aColumn") })); + assertEquals(WITH_LABEL, item.getLabel()); + assertNotNull(item.getQuery()); + assertNotNull(item.getColumnLabels()); + assertEquals(1, item.getColumnLabels().size()); + } + + @Test + public void testToADQL() { + try { + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + + // CASE: NO column labels + WithItem item = new WithItem("myQuery", parser.parseQuery("SELECT foo FROM bar")); + assertEquals("myQuery AS (\n" + "SELECT foo\n" + "FROM bar\n" + ")", item.toADQL()); + + // CASE: WITH column labels + item = new WithItem("myQuery", parser.parseQuery("SELECT foo, stuff FROM bar"), Arrays.asList(new ADQLColumn[]{ new ADQLColumn("aColumn"), new ADQLColumn("\"Another\"Column\"") })); + assertEquals("myQuery(aColumn,\"Another\"\"Column\") AS (\n" + "SELECT foo , stuff\n" + "FROM bar\n" + ")", item.toADQL()); + + // CASE: after an integral parsing + ADQLQuery query = parser.parseQuery("WITH myQuery(aColumn, \"Another\"\"Column\") AS (SELECT foo, stuff FROM bar) SELECT * FROM myQuery"); + assertEquals("WITH myQuery(aColumn,\"Another\"\"Column\") AS (\n" + "SELECT foo , stuff\n" + "FROM bar\n" + ")\nSELECT *\nFROM myQuery", query.toADQL()); + } catch(ParseException ex) { + ex.printStackTrace(); + fail("Unexpected parsing error!"); + } + } + + @Test + public void testGetResultingColumns() { + try { + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + + // CASE: NO column labels + WithItem item = new WithItem("myQuery", parser.parseQuery("SELECT foo FROM bar")); + DBColumn[] lstCol = item.getResultingColumns(); + assertNotNull(lstCol); + assertEquals(1, lstCol.length); + assertEquals("foo", lstCol[0].getADQLName()); + assertEquals(lstCol[0].getADQLName(), lstCol[0].getDBName()); + assertFalse(lstCol[0].isCaseSensitive()); + + // CASE: WITH column labels + item = new WithItem("myQuery", parser.parseQuery("SELECT foo, stuff FROM bar"), Arrays.asList(new ADQLColumn[]{ new ADQLColumn("aColumn"), new ADQLColumn("\"Another\"Column\"") })); + assertEquals("myQuery(aColumn,\"Another\"\"Column\") AS (\n" + "SELECT foo , stuff\n" + "FROM bar\n" + ")", item.toADQL()); + lstCol = item.getResultingColumns(); + assertNotNull(lstCol); + assertEquals(2, lstCol.length); + assertEquals("acolumn", lstCol[0].getADQLName()); + assertEquals(lstCol[0].getADQLName(), lstCol[0].getDBName()); + assertFalse(lstCol[0].isCaseSensitive()); + assertEquals("Another\"Column", lstCol[1].getADQLName()); + assertEquals(lstCol[1].getADQLName(), lstCol[1].getDBName()); + assertTrue(lstCol[1].isCaseSensitive()); + } catch(ParseException ex) { + ex.printStackTrace(); + fail("Unexpected parsing error!"); + } + } + +} diff --git a/test/adql/translator/TestJDBCTranslator.java b/test/adql/translator/TestJDBCTranslator.java index dbd3cbfc2b2269ffd1977762e61945fdf1948c0c..2f8a5a010d92917b9bc5cfc8e0e5595d8aea12fa 100644 --- a/test/adql/translator/TestJDBCTranslator.java +++ b/test/adql/translator/TestJDBCTranslator.java @@ -1,12 +1,21 @@ package adql.translator; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.Arrays; + import org.junit.Before; import org.junit.Test; +import adql.db.DBChecker; +import adql.db.DBTable; import adql.db.DBType; +import adql.db.DefaultDBColumn; +import adql.db.DefaultDBTable; import adql.db.FunctionDef; import adql.db.STCS.Region; import adql.parser.ADQLParser; @@ -15,7 +24,9 @@ import adql.parser.feature.FeatureSet; import adql.parser.feature.LanguageFeature; import adql.parser.grammar.ParseException; import adql.query.ADQLQuery; +import adql.query.ClauseADQL; import adql.query.IdentifierField; +import adql.query.WithItem; import adql.query.operand.ADQLColumn; import adql.query.operand.ADQLOperand; import adql.query.operand.NumericConstant; @@ -42,6 +53,70 @@ public class TestJDBCTranslator { public void setUp() throws Exception { } + @Test + public void testTranslateWithClause() { + JDBCTranslator tr = new AJDBCTranslator(); + ADQLParser parser = new ADQLParser(ADQLVersion.V2_1); + + try { + // CASE: No WITH clause + ADQLQuery query = parser.parseQuery("SELECT * FROM foo"); + ClauseADQL<WithItem> withClause = query.getWith(); + assertTrue(withClause.isEmpty()); + assertEquals("WITH ", tr.translate(withClause)); + assertEquals("SELECT *\nFROM foo", tr.translate(query)); + + // CASE: A single WITH item + query = parser.parseQuery("WITH foo AS (SELECT * FROM bar) SELECT * FROM foo"); + withClause = query.getWith(); + assertEquals(1, withClause.size()); + assertEquals("WITH \"foo\" AS (\nSELECT *\nFROM bar\n)", tr.translate(withClause)); + assertEquals("WITH \"foo\" AS (\nSELECT *\nFROM bar\n)\nSELECT *\nFROM foo", tr.translate(query)); + + // CASE: Several WITH items + query = parser.parseQuery("WITH foo AS (SELECT * FROM bar), Foo2(myCol) AS (SELECT aCol FROM myTable) SELECT * FROM foo JOIN foo2 ON foo.id = foo2.myCol"); + withClause = query.getWith(); + assertEquals(2, withClause.size()); + assertEquals("WITH \"foo\" AS (\nSELECT *\nFROM bar\n) , \"foo2\"(\"mycol\") AS (\nSELECT aCol AS \"aCol\"\nFROM myTable\n)", tr.translate(withClause)); + assertEquals("WITH \"foo\" AS (\nSELECT *\nFROM bar\n) , \"foo2\"(\"mycol\") AS (\nSELECT aCol AS \"aCol\"\nFROM myTable\n)\nSELECT *\nFROM foo INNER JOIN foo2 ON foo.id = foo2.myCol", tr.translate(query)); + + } catch(ParseException pe) { + pe.printStackTrace(); + fail("Unexpected parsing failure! (see console for more details)"); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected error while translating a correct WITH item! (see console for more details)"); + } + } + + @Test + public void testTranslateWithItem() { + JDBCTranslator tr = new AJDBCTranslator(); + + try { + // CASE: Simple WITH item (no case sensitivity, no column labels) + WithItem item = new WithItem("Foo", (new ADQLParser(ADQLVersion.V2_1)).parseQuery("SELECT * FROM bar")); + item.setLabelCaseSensitive(false); + assertEquals("\"foo\" AS (\nSELECT *\nFROM bar\n)", tr.translate(item)); + + // CASE: WITH item with case sensitivity and column labels + item = new WithItem("Foo", (new ADQLParser(ADQLVersion.V2_1)).parseQuery("SELECT col1, col2 FROM bar"), Arrays.asList(new ADQLColumn[]{ new ADQLColumn("FirstColumn"), new ADQLColumn("\"SecondColumn\"") })); + item.setLabelCaseSensitive(true); + assertEquals("\"Foo\"(\"firstcolumn\",\"SecondColumn\") AS (\nSELECT col1 AS \"col1\" , col2 AS \"col2\"\nFROM bar\n)", tr.translate(item)); + + // CASE: query with an inner WITH + item = new WithItem("Foo", (new ADQLParser(ADQLVersion.V2_1)).parseQuery("WITH bar(col1, col2) AS (SELECT aCol, anotherCol FROM stuff) SELECT * FROM bar")); + assertEquals("\"foo\" AS (\nWITH \"bar\"(\"col1\",\"col2\") AS (\nSELECT aCol AS \"aCol\" , anotherCol AS \"anotherCol\"\nFROM stuff\n)\nSELECT *\nFROM bar\n)", tr.translate(item)); + + } catch(ParseException pe) { + pe.printStackTrace(); + fail("Unexpected parsing failure! (see console for more details)"); + } catch(Exception ex) { + ex.printStackTrace(); + fail("Unexpected error while translating a correct WITH item! (see console for more details)"); + } + } + public final static int countFeatures(final FeatureSet features) { int cnt = 0; for(LanguageFeature feat : features) @@ -181,6 +256,80 @@ public class TestJDBCTranslator { } } + @Test + public void testNaturalJoin() { + 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); + + final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;"; + + try { + ADQLParser parser = new ADQLParser(); + parser.setQueryChecker(new DBChecker(tables)); + ADQLQuery query = parser.parseQuery(adqlquery); + JDBCTranslator translator = new AJDBCTranslator(); + + // Test the FROM part: + assertEquals("aTable AS \"a\" NATURAL INNER JOIN anotherTable AS \"b\" ", 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 id AS \"id\" , 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() { + 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); + + final String adqlquery = "SELECT B.id, name, aColumn, anotherColumn FROM aTable A JOIN anotherTable B USING(name);"; + + try { + ADQLParser parser = new ADQLParser(); + parser.setQueryChecker(new DBChecker(tables)); + ADQLQuery query = parser.parseQuery(adqlquery); + JDBCTranslator translator = new AJDBCTranslator(); + + // Test the FROM part: + assertEquals("aTable AS \"a\" INNER JOIN anotherTable AS \"b\" USING (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\" , 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)"); + } + } + public final static class AJDBCTranslator extends JDBCTranslator { @Override diff --git a/test/adql/translator/TestSQLServerTranslator.java b/test/adql/translator/TestSQLServerTranslator.java index 1629efb0df5dbee4fda337fefc543defd849f4da..89d34a11e00764416354e3c9939c7b1e88de92ce 100644 --- a/test/adql/translator/TestSQLServerTranslator.java +++ b/test/adql/translator/TestSQLServerTranslator.java @@ -88,7 +88,7 @@ public class TestSQLServerTranslator { assertEquals("\"aTable\" AS \"a\" INNER JOIN \"anotherTable\" AS \"b\" ON \"a\".\"id\"=\"b\".\"id\" AND \"a\".\"name\"=\"b\".\"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())); + 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(); @@ -114,7 +114,7 @@ public class TestSQLServerTranslator { assertEquals("\"aTable\" AS \"a\" INNER JOIN \"anotherTable\" AS \"b\" ON \"a\".\"name\"=\"b\".\"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())); + 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(); diff --git a/test/tap/config/TestTAPConfiguration.java b/test/tap/config/TestTAPConfiguration.java index 30d8092d5e4decb7498b6312326ad5c03f5a7a8b..602e754d0288603b2849a6f70e04b3f6ff34d3bd 100644 --- a/test/tap/config/TestTAPConfiguration.java +++ b/test/tap/config/TestTAPConfiguration.java @@ -26,28 +26,29 @@ import java.util.Properties; import org.junit.Before; import org.junit.Test; +import adql.query.ColumnReference; import tap.ServiceConnection; import tap.ServiceConnection.LimitUnit; import tap.TAPException; import tap.TAPFactory; import tap.metadata.TAPMetadata; import tap.metadata.TAPSchema; -import adql.query.ColumnReference; public class TestTAPConfiguration { @Before - public void setUp() throws Exception{} + public void setUp() throws Exception { + } /** * TEST isClassName(String): * - null, "", "{}", "an incorrect syntax" => FALSE must be returned * - "{ }", "{ }", "{class.path}", "{ class.path }" => TRUE must be returned - * + * * @see ConfigurableServiceConnection#isClassName(String) */ @Test - public void testIsClassPath(){ + public void testIsClassPath() { // NULL and EMPTY: assertFalse(isClassName(null)); assertFalse(isClassName("")); @@ -76,79 +77,79 @@ public class TestTAPConfiguration { * - "{mypackage.foo}", "{java.util.ArrayList}" (while a String is expected) => a TAPException must be thrown */ @Test - public void testGetClassStringStringClass(){ + public void testGetClassStringStringClass() { // NULL and EMPTY: - try{ + try { assertNull(fetchClass(null, KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If a NULL value is provided as class name: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } - try{ + try { assertNull(fetchClass("", KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If an EMPTY value is provided as class name: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } // EMPTY CLASS NAME: - try{ + try { assertNull(fetchClass("{}", KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } // INCORRECT SYNTAX: - try{ + try { assertNull(fetchClass("incorrect class name ; missing {}", KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If an incorrect class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } // VALID CLASS NAME: - try{ + try { Class<? extends String> classObject = fetchClass("{java.lang.String}", KEY_FILE_MANAGER, String.class); assertNotNull(classObject); assertEquals(classObject.getName(), "java.lang.String"); - }catch(TAPException e){ + } catch(TAPException e) { fail("If a VALID class name is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); } // INCORRECT CLASS NAME: - try{ + try { fetchClass("{mypackage.foo}", KEY_FILE_MANAGER, String.class); fail("This MUST have failed because an incorrect class name is provided!"); - }catch(TAPException e){ + } catch(TAPException e) { assertEquals(e.getClass(), TAPException.class); assertEquals(e.getMessage(), "The class specified by the property \"" + KEY_FILE_MANAGER + "\" ({mypackage.foo}) can not be found."); } // INCOMPATIBLE TYPES: - try{ + try { @SuppressWarnings("unused") Class<? extends String> classObject = fetchClass("{java.util.ArrayList}", KEY_FILE_MANAGER, String.class); fail("This MUST have failed because a class of a different type has been asked!"); - }catch(TAPException e){ + } catch(TAPException e) { assertEquals(e.getClass(), TAPException.class); assertEquals(e.getMessage(), "The class specified by the property \"" + KEY_FILE_MANAGER + "\" ({java.util.ArrayList}) is not implementing " + String.class.getName() + "."); } // CLASS NAME VALID ONLY IN THE SYNTAX: - try{ + try { assertNull(fetchClass("{ }", KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } - try{ + try { assertNull(fetchClass("{ }", KEY_FILE_MANAGER, String.class)); - }catch(TAPException e){ + } catch(TAPException e) { fail("If an EMPTY class name is provided: getClass(...) MUST return null!\nCaught exception: " + getPertinentMessage(e)); } // NOT TRIM CLASS NAME: - try{ + try { Class<?> classObject = fetchClass("{ java.lang.String }", KEY_FILE_MANAGER, String.class); assertNotNull(classObject); assertEquals(classObject.getName(), "java.lang.String"); - }catch(TAPException e){ + } catch(TAPException e) { fail("If a VALID class name is provided: getClass(...) MUST return a Class object of the wanted type!\nCaught exception: " + getPertinentMessage(e)); } } @@ -163,82 +164,82 @@ public class TestTAPConfiguration { * - if the specified constructor exists return <code>true</code>, else <code>false</code> must be returned. */ @Test - public void testHasConstructor(){ + public void testHasConstructor() { /* hasConstructor(...) must throw an exception if the specification of the class (1st and 3rd parameters) * is wrong. But that is performed by fetchClass(...) which is called at the beginning of the function * and is not surrounded by a try-catch. So all these tests are already done by testGetClassStringStringClass(). */ // With a missing list of parameters: - try{ + try { assertTrue(hasConstructor("{java.lang.String}", "STRING", String.class, null)); - }catch(TAPException te){ + } catch(TAPException te) { te.printStackTrace(); fail("\"No list of parameters\" MUST be interpreted as the specification of a constructor with no parameter! This test has failed."); } // With an empty list of parameters - try{ + try { assertTrue(hasConstructor("{java.lang.String}", "STRING", String.class, new Class[0])); - }catch(TAPException te){ + } catch(TAPException te) { te.printStackTrace(); fail("\"An empty list of parameters\" MUST be interpreted as the specification of a constructor with no parameter! This test has failed."); } // With a wrong list of parameters - 1 - try{ + try { assertFalse(hasConstructor("{tap.config.ConfigurableTAPFactory}", KEY_TAP_FACTORY, TAPFactory.class, new Class[]{})); - }catch(TAPException te){ + } catch(TAPException te) { te.printStackTrace(); fail("ConfigurableTAPFactory does not have an empty constructor ; this test should have failed!"); } // With a wrong list of parameters - 2 - try{ - assertFalse(hasConstructor("{tap.config.ConfigurableTAPFactory}", KEY_TAP_FACTORY, TAPFactory.class, new Class[]{String.class,String.class})); - }catch(TAPException te){ + try { + assertFalse(hasConstructor("{tap.config.ConfigurableTAPFactory}", KEY_TAP_FACTORY, TAPFactory.class, new Class[]{ String.class, String.class })); + } catch(TAPException te) { te.printStackTrace(); fail("ConfigurableTAPFactory does not have a constructor with 2 Strings as parameter ; this test should have failed!"); } // With a good list of parameters - 1 - try{ - assertTrue(hasConstructor("{tap.config.ConfigurableTAPFactory}", KEY_TAP_FACTORY, TAPFactory.class, new Class[]{ServiceConnection.class,Properties.class})); - }catch(TAPException te){ + try { + assertTrue(hasConstructor("{tap.config.ConfigurableTAPFactory}", KEY_TAP_FACTORY, TAPFactory.class, new Class[]{ ServiceConnection.class, Properties.class })); + } catch(TAPException te) { te.printStackTrace(); fail("ConfigurableTAPFactory has a constructor with a ServiceConnection and a Properties in parameters ; this test should have failed!"); } // With a good list of parameters - 2 - try{ - assertTrue(hasConstructor("{java.lang.String}", "STRING", String.class, new Class[]{String.class})); - }catch(TAPException te){ + try { + assertTrue(hasConstructor("{java.lang.String}", "STRING", String.class, new Class[]{ String.class })); + } catch(TAPException te) { te.printStackTrace(); fail("String has a constructor with a String as parameter ; this test should have failed!"); } } @Test - public void testNewInstance(){ + public void testNewInstance() { // VALID CONSTRUCTOR with no parameters: - try{ + try { TAPMetadata metadata = newInstance("{tap.metadata.TAPMetadata}", "metadata", TAPMetadata.class); assertNotNull(metadata); assertEquals("tap.metadata.TAPMetadata", metadata.getClass().getName()); - }catch(Exception ex){ + } catch(Exception ex) { ex.printStackTrace(); fail("This test should have succeeded: the parameters of newInstance(...) are all valid."); } // VALID CONSTRUCTOR with some parameters: - try{ + try { final String schemaName = "MySuperSchema", description = "And its less super description.", utype = "UTYPE"; - TAPSchema schema = newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{String.class,String.class,String.class}, new String[]{schemaName,description,utype}); + TAPSchema schema = newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{ String.class, String.class, String.class }, new String[]{ schemaName, description, utype }); assertNotNull(schema); assertEquals("tap.metadata.TAPSchema", schema.getClass().getName()); assertEquals(schemaName, schema.getADQLName()); assertEquals(description, schema.getDescription()); assertEquals(utype, schema.getUtype()); - }catch(Exception ex){ + } catch(Exception ex) { ex.printStackTrace(); fail("This test should have succeeded: the constructor TAPSchema(String,String,String) exists."); } @@ -246,79 +247,80 @@ public class TestTAPConfiguration { // VALID CONSTRUCTOR with some parameters whose the type is an extension (not the exact type): OutputStream output = null; File tmp = new File("tmp.empty"); - try{ - output = newInstance("{java.io.BufferedOutputStream}", "stream", OutputStream.class, new Class<?>[]{OutputStream.class}, new OutputStream[]{new FileOutputStream(tmp)}); + try { + output = newInstance("{java.io.BufferedOutputStream}", "stream", OutputStream.class, new Class<?>[]{ OutputStream.class }, new OutputStream[]{ new FileOutputStream(tmp) }); assertNotNull(output); assertEquals(BufferedOutputStream.class, output.getClass()); - }catch(Exception ex){ + } catch(Exception ex) { ex.printStackTrace(); fail("This test should have succeeded: the constructor TAPSchema(String,String,String) exists."); - }finally{ - try{ + } finally { + try { tmp.delete(); if (output != null) output.close(); - }catch(IOException ioe){} + } catch(IOException ioe) { + } } // NOT A CLASS NAME: - try{ + try { newInstance("tap.metadata.TAPMetadata", "metadata", TAPMetadata.class); fail("This MUST have failed because the property value is not a class name!"); - }catch(Exception ex){ + } catch(Exception ex) { assertEquals(TAPException.class, ex.getClass()); assertEquals("Class name expected for the property \"metadata\" instead of: \"tap.metadata.TAPMetadata\"! The specified class must extend/implement tap.metadata.TAPMetadata.", ex.getMessage()); } // NO MATCHING CONSTRUCTOR: - try{ - newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{Integer.class}, new Object[]{new Integer(123)}); + try { + newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{ Integer.class }, new Object[]{ new Integer(123) }); fail("This MUST have failed because the specified class does not have any expected constructor!"); - }catch(Exception ex){ + } catch(Exception ex) { assertEquals(TAPException.class, ex.getClass()); assertEquals("Missing constructor tap.metadata.TAPSchema(java.lang.Integer)! See the value \"{tap.metadata.TAPSchema}\" of the property \"schema\".", ex.getMessage()); } // VALID CONSTRUCTOR with primitive type: - try{ - ColumnReference colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{int.class}, new Object[]{123}); + try { + ColumnReference colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{ int.class }, new Object[]{ 123 }); assertNotNull(colRef); assertEquals(ColumnReference.class, colRef.getClass()); assertEquals(123, colRef.getColumnIndex()); - colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{int.class}, new Object[]{new Integer(123)}); + colRef = newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{ int.class }, new Object[]{ new Integer(123) }); assertNotNull(colRef); assertEquals(ColumnReference.class, colRef.getClass()); assertEquals(123, colRef.getColumnIndex()); - }catch(Exception ex){ + } catch(Exception ex) { ex.printStackTrace(); fail("This test should have succeeded: the constructor ColumnReference(int) exists."); } // WRONG CONSTRUCTOR with primitive type: - try{ - newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{Integer.class}, new Object[]{new Integer(123)}); + try { + newInstance("{adql.query.ColumnReference}", "colRef", ColumnReference.class, new Class<?>[]{ Integer.class }, new Object[]{ new Integer(123) }); fail("This MUST have failed because the constructor of the specified class expects an int, not an java.lang.Integer!"); - }catch(Exception ex){ + } catch(Exception ex) { assertEquals(TAPException.class, ex.getClass()); assertEquals("Missing constructor adql.query.ColumnReference(java.lang.Integer)! See the value \"{adql.query.ColumnReference}\" of the property \"colRef\".", ex.getMessage()); } // THE CONSTRUCTOR THROWS AN EXCEPTION: - try{ - newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{String.class}, new Object[]{null}); + try { + newInstance("{tap.metadata.TAPSchema}", "schema", TAPSchema.class, new Class<?>[]{ String.class }, new Object[]{ null }); fail("This MUST have failed because the constructor of the specified class throws an exception!"); - }catch(Exception ex){ + } catch(Exception ex) { assertEquals(TAPException.class, ex.getClass()); assertNotNull(ex.getCause()); assertEquals(NullPointerException.class, ex.getCause().getClass()); - assertEquals("Missing schema name!", ex.getCause().getMessage()); + assertEquals("Missing ADQL name!", ex.getCause().getMessage()); } // THE CONSTRUCTOR THROWS A TAPEXCEPTION: - try{ + try { newInstance("{tap.config.TestTAPConfiguration$ClassAlwaysThrowTAPError}", "tapError", ClassAlwaysThrowTAPError.class); fail("This MUST have failed because the constructor of the specified class throws a TAPException!"); - }catch(Exception ex){ + } catch(Exception ex) { assertEquals(TAPException.class, ex.getClass()); assertEquals("This error is always thrown by ClassAlwaysThrowTAPError ^^", ex.getMessage()); } @@ -337,13 +339,13 @@ public class TestTAPConfiguration { * - foo, 100b, 100TB, 1foo => an exception must occur */ @Test - public void testParseLimitStringString(){ + public void testParseLimitStringString() { final String propertyName = KEY_DEFAULT_OUTPUT_LIMIT + " or " + KEY_MAX_OUTPUT_LIMIT; // Test empty or negative or null values => OK! - try{ - String[] testValues = new String[]{null,""," ","-123"}; + try { + String[] testValues = new String[]{ null, "", " ", "-123" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, false); assertEquals(limit[0], -1); assertEquals(limit[1], LimitUnit.rows); @@ -352,132 +354,132 @@ public class TestTAPConfiguration { limit = parseLimit("0", propertyName, false); assertEquals(limit[0], 0); assertEquals(limit[1], LimitUnit.rows); - }catch(TAPException te){ + } catch(TAPException te) { fail("All these empty limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test all accepted rows values: - try{ - String[] testValues = new String[]{"20","20r","20 R"}; + try { + String[] testValues = new String[]{ "20", "20r", "20 R" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, false); assertEquals(limit[0], 20); assertEquals(limit[1], LimitUnit.rows); } - }catch(TAPException te){ + } catch(TAPException te) { fail("All these rows limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test all accepted bytes values: - try{ - String[] testValues = new String[]{"100B","100 B"}; + try { + String[] testValues = new String[]{ "100B", "100 B" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, true); assertEquals(limit[0], 100); assertEquals(limit[1], LimitUnit.bytes); } - }catch(TAPException te){ + } catch(TAPException te) { fail("All these bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test all accepted kilo-bytes values: - try{ - String[] testValues = new String[]{"100kB","100 k B"}; + try { + String[] testValues = new String[]{ "100kB", "100 k B" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, true); assertEquals(limit[0], 100); assertEquals(limit[1], LimitUnit.kilobytes); } - }catch(TAPException te){ + } catch(TAPException te) { fail("All these kilo-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test all accepted mega-bytes values: - try{ - String[] testValues = new String[]{"100MB","1 0 0MB"}; + try { + String[] testValues = new String[]{ "100MB", "1 0 0MB" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, true); assertEquals(limit[0], 100); assertEquals(limit[1], LimitUnit.megabytes); } - }catch(TAPException te){ + } catch(TAPException te) { fail("All these mega-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test all accepted giga-bytes values: - try{ - String[] testValues = new String[]{"100GB","1 0 0 G B"}; + try { + String[] testValues = new String[]{ "100GB", "1 0 0 G B" }; Object[] limit; - for(String v : testValues){ + for(String v : testValues) { limit = parseLimit(v, propertyName, true); assertEquals(limit[0], 100); assertEquals(limit[1], LimitUnit.gigabytes); } - }catch(TAPException te){ + } catch(TAPException te) { fail("All these giga-bytes limit values are valid, so these tests should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test with only the ROWS unit provided: - try{ + try { Object[] limit = parseLimit("r", propertyName, false); assertEquals(limit[0], -1); assertEquals(limit[1], LimitUnit.rows); - }catch(TAPException te){ + } catch(TAPException te) { fail("Providing only the ROWS unit is valid, so this test should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test with only the BYTES unit provided: - try{ + try { Object[] limit = parseLimit("kB", propertyName, true); assertEquals(limit[0], -1); assertEquals(limit[1], LimitUnit.kilobytes); - }catch(TAPException te){ + } catch(TAPException te) { fail("Providing only the BYTES unit is valid, so this test should have succeeded!\nCaught exception: " + getPertinentMessage(te)); } // Test with incorrect limit formats: - String[] values = new String[]{"","100","100","1"}; - String[] unitPart = new String[]{"foo","b","TB","foo"}; - for(int i = 0; i < values.length; i++){ - try{ + String[] values = new String[]{ "", "100", "100", "1" }; + String[] unitPart = new String[]{ "foo", "b", "TB", "foo" }; + for(int i = 0; i < values.length; i++) { + try { parseLimit(values[i] + unitPart[i], propertyName, true); fail("This test should have failed because an incorrect limit is provided: \"" + values[i] + unitPart[i] + "\"!"); - }catch(TAPException te){ + } catch(TAPException te) { assertEquals(te.getClass(), TAPException.class); assertEquals(te.getMessage(), "Unknown limit unit (" + unitPart[i] + ") for the property " + propertyName + ": \"" + values[i] + unitPart[i] + "\"!"); } } // Test with an incorrect numeric limit value: - try{ + try { parseLimit("abc100b", propertyName, true); fail("This test should have failed because an incorrect limit is provided: \"abc100b\"!"); - }catch(TAPException te){ + } catch(TAPException te) { assertEquals(te.getClass(), TAPException.class); assertEquals(te.getMessage(), "Integer expected for the property " + propertyName + " for the substring \"abc100\" of the whole value: \"abc100b\"!"); } // Test with a BYTES unit whereas the BYTES unit is forbidden: - try{ + try { parseLimit("100B", propertyName, false); fail("This test should have failed because an incorrect limit is provided: \"100B\"!"); - }catch(TAPException te){ + } catch(TAPException te) { assertEquals(te.getClass(), TAPException.class); assertEquals(te.getMessage(), "BYTES unit is not allowed for the property " + propertyName + " (100B)!"); } } - public static final String getPertinentMessage(final Exception ex){ + public static final String getPertinentMessage(final Exception ex) { return (ex.getCause() == null || ex.getMessage().equals(ex.getCause().getMessage())) ? ex.getMessage() : ex.getCause().getMessage(); } private static class ClassAlwaysThrowTAPError { @SuppressWarnings("unused") - public ClassAlwaysThrowTAPError() throws TAPException{ + public ClassAlwaysThrowTAPError() throws TAPException { throw new TAPException("This error is always thrown by ClassAlwaysThrowTAPError ^^"); } } diff --git a/test/tap/metadata/TestMetadataNames.java b/test/tap/metadata/TestMetadataNames.java index 4fdb9c9e98555cba22cee1f9c0b8f94b9d10f7cc..1480b222eae66fc96ef94a805b4b3a1cb9df9b82 100644 --- a/test/tap/metadata/TestMetadataNames.java +++ b/test/tap/metadata/TestMetadataNames.java @@ -27,7 +27,7 @@ public class TestMetadataNames { new TAPSchema(null); fail("It should be impossible to create a TAPSchema with a NULL name."); } catch(NullPointerException npe) { - assertEquals("Missing schema name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty string (not a single character): @@ -35,7 +35,7 @@ public class TestMetadataNames { new TAPSchema(""); fail("It should be impossible to create a TAPSchema with an empty name."); } catch(NullPointerException npe) { - assertEquals("Missing schema name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // String with only space characters: @@ -43,7 +43,7 @@ public class TestMetadataNames { new TAPSchema(" "); fail("It should be impossible to create a TAPSchema with a name just composed of space characters."); } catch(NullPointerException npe) { - assertEquals("Missing schema name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string I: @@ -51,7 +51,7 @@ public class TestMetadataNames { new TAPSchema("\"\""); fail("It should be impossible to create a TAPSchema with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing schema name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string II: @@ -59,7 +59,7 @@ public class TestMetadataNames { new TAPSchema("\" \""); fail("It should be impossible to create a TAPSchema with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing schema name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Non quoted names => ADQL_NAME = RAW_NAME = TRIMMED(GIVEN_NAME) @@ -125,7 +125,7 @@ public class TestMetadataNames { new TAPTable(null); fail("It should be impossible to create a TAPTable with a NULL name."); } catch(NullPointerException npe) { - assertEquals("Missing table name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty string (not a single character): @@ -133,7 +133,7 @@ public class TestMetadataNames { new TAPTable(""); fail("It should be impossible to create a TAPTable with an empty name."); } catch(NullPointerException npe) { - assertEquals("Missing table name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // String with only space characters: @@ -141,7 +141,7 @@ public class TestMetadataNames { new TAPTable(" "); fail("It should be impossible to create a TAPTable with a name just composed of space characters."); } catch(NullPointerException npe) { - assertEquals("Missing table name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string I: @@ -149,7 +149,7 @@ public class TestMetadataNames { new TAPTable("\"\""); fail("It should be impossible to create a TAPTable with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing table name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string II: @@ -157,7 +157,7 @@ public class TestMetadataNames { new TAPTable("\" \""); fail("It should be impossible to create a TAPTable with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing table name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Non quoted names => ADQL_NAME = RAW_NAME = TRIMMED(GIVEN_NAME) @@ -263,7 +263,7 @@ public class TestMetadataNames { new TAPColumn(null); fail("It should be impossible to create a TAPColumn with a NULL name."); } catch(NullPointerException npe) { - assertEquals("Missing column name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty string (not a single character): @@ -271,7 +271,7 @@ public class TestMetadataNames { new TAPColumn(""); fail("It should be impossible to create a TAPColumn with an empty name."); } catch(NullPointerException npe) { - assertEquals("Missing column name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // String with only space characters: @@ -279,7 +279,7 @@ public class TestMetadataNames { new TAPColumn(" "); fail("It should be impossible to create a TAPColumn with a name just composed of space characters."); } catch(NullPointerException npe) { - assertEquals("Missing column name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string I: @@ -287,7 +287,7 @@ public class TestMetadataNames { new TAPColumn("\"\""); fail("It should be impossible to create a TAPColumn with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing column name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Empty quoted string II: @@ -295,7 +295,7 @@ public class TestMetadataNames { new TAPColumn("\" \""); fail("It should be impossible to create a TAPColumn with a empty name even if quoted."); } catch(NullPointerException npe) { - assertEquals("Missing column name!", npe.getMessage()); + assertEquals("Missing ADQL name!", npe.getMessage()); } // Non quoted names => ADQL_NAME = RAW_NAME = TRIMMED(GIVEN_NAME)