Skip to content
Snippets Groups Projects
Commit 8a2ce121 authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[ADQL] Support OFFSET.

parent 9f845f0b
Branches
No related tags found
No related merge requests found
......@@ -301,6 +301,7 @@ TOKEN : {
| < ORDER: "ORDER" > { matchedToken.adqlReserved = true; }
| < ASC: "ASC" > { matchedToken.adqlReserved = true; }
| < DESC: "DESC" > { matchedToken.adqlReserved = true; }
| < OFFSET: "OFFSET" > { matchedToken.adqlReserved = true; }
}
/* ************* */
......@@ -439,6 +440,7 @@ ADQLQuery QueryExpression(): {TextPosition endPos = null;} {
[GroupBy() {endPos = query.getGroupBy().getPosition();}]
[Having() {endPos = query.getHaving().getPosition();}]
[OrderBy() {endPos = query.getOrderBy().getPosition();}]
[Offset() {endPos = new TextPosition(token);}]
{
// set the position of the query:
query.setPosition(new TextPosition(query.getSelect().getPosition(), endPos));
......@@ -577,6 +579,17 @@ void OrderBy(): {ClauseADQL<ADQLOrder> orderBy = query.getOrderBy(); ADQLOrder o
{ orderBy.setPosition(new TextPosition(start, token)); }
}
void Offset(): { Token t; } {
<OFFSET> t=<UNSIGNED_INTEGER>
{
try{
query.setOffset(Integer.parseInt(t.image));
}catch(NumberFormatException nfe){
throw new ParseException("The OFFSET limit (\""+t.image+"\") isn't a regular unsigned integer!", new TextPosition(t));
}
}
}
/* *************************** */
/* COLUMN AND TABLE REFERENCES */
/* *************************** */
......
......@@ -52,7 +52,7 @@ import adql.search.ISearchHandler;
* </p>
*
* @author Gr&eacute;gory Mantelet (CDS;ARI)
* @version 2.0 (07/2019)
* @version 2.0 (08/2019)
*/
public class ADQLQuery implements ADQLObject {
......@@ -82,6 +82,10 @@ public class ADQLQuery implements ADQLObject {
/** The ADQL clause ORDER BY. */
private ClauseADQL<ADQLOrder> orderBy;
/** The ADQL clause OFFSET.
* @since 2.0 */
private int offset;
/** Position of this Query (or sub-query) inside the whole given ADQL query
* string.
* @since 1.4 */
......@@ -112,6 +116,7 @@ public class ADQLQuery implements ADQLObject {
groupBy = new ClauseADQL<ADQLColumn>("GROUP BY");
having = new ClauseConstraints("HAVING");
orderBy = new ClauseADQL<ADQLOrder>("ORDER BY");
offset = -1;
}
/**
......@@ -130,6 +135,7 @@ public class ADQLQuery implements ADQLObject {
groupBy = (ClauseADQL<ADQLColumn>)toCopy.groupBy.getCopy();
having = (ClauseConstraints)toCopy.having.getCopy();
orderBy = (ClauseADQL<ADQLOrder>)toCopy.orderBy.getCopy();
offset = toCopy.offset;
position = (toCopy.position == null) ? null : new TextPosition(toCopy.position);
}
......@@ -160,6 +166,7 @@ public class ADQLQuery implements ADQLObject {
groupBy.clear();
having.clear();
orderBy.clear();
offset = -1;
position = null;
}
......@@ -331,6 +338,62 @@ public class ADQLQuery implements ADQLObject {
position = null;
}
/**
* Gets the OFFSET value of this query.
*
* @return Its OFFSET value,
* or a negative value if no OFFSET is set.
*
* @since 2.0
*/
public final int getOffset() {
return offset;
}
/**
* Tell whether an OFFSET is set in this query.
*
* @return <code>true</code> if an OFFSET is set,
* <code>false</code> otherwise.
*
* @since 2.0
*/
public final boolean hasOffset() {
return (offset > -1);
}
/**
* Remove the OFFSET value of this query.
*
* <p><i><b>Note:</b>
* The position of the query is erased.
* </i></p>.
*
* @since 2.0
*/
public void setNoOffset() {
offset = -1;
position = null;
}
/**
* Replaces its OFFSET value by the given one.
*
* <p><i><b>Note:</b>
* The position of the query is erased.
* </i></p>
*
* @param newOffset The new OFFSET value.
* <i><b>Note:</b> a negative value removes the OFFSET from
* this query.</i>
*
* @since 2.0
*/
public void setOffset(final int newOffset) {
offset = newOffset;
position = null;
}
@Override
public final TextPosition getPosition() {
return position;
......@@ -580,6 +643,9 @@ public class ADQLQuery implements ADQLObject {
if (!orderBy.isEmpty())
adql.append('\n').append(orderBy.toADQL());
if (hasOffset())
adql.append("\nOFFSET ").append(offset);
return adql.toString();
}
......
......@@ -375,7 +375,10 @@ public abstract class JDBCTranslator implements ADQLTranslator {
sql.append('\n').append(translate(query.getOrderBy()));
if (query.getSelect().hasLimit())
sql.append("\nLimit ").append(query.getSelect().getLimit());
sql.append("\nLIMIT ").append(query.getSelect().getLimit());
if (query.hasOffset())
sql.append("\nOFFSET ").append(query.getOffset());
return sql.toString();
}
......
......@@ -134,10 +134,13 @@ public class SQLServerTranslator extends JDBCTranslator {
/**
* For SQL Server, {@link #translate(ClauseSelect)} must be overridden for
* TOP/LIMIT handling. We must not add the LIMIT at the end of the query, it
* must go in the SELECT.
* LIMIT and OFFSET handling.
*
* @see #translate(ClauseSelect)
* <p><i><b>Implementation note:</b>
* LIMIT is replaced by FETCH NEXT instead of TOP because of the addition
* of OFFSET support in ADQL-2.1 grammar. With MS-SQLServer, TOP can not be
* used with OFFSET...it must be OFFSET...LIMIT....
* </i></p>
*/
@Override
public String translate(ADQLQuery query) throws TranslationException {
......@@ -157,6 +160,16 @@ public class SQLServerTranslator extends JDBCTranslator {
if (!query.getOrderBy().isEmpty())
sql.append('\n').append(translate(query.getOrderBy()));
if (query.getSelect().hasLimit()) {
if (query.hasOffset())
sql.append('\n').append("OFFSET ").append(query.getOffset()).append(" ROWS");
else
sql.append('\n').append("OFFSET 0 ROWS");
sql.append(" FETCH NEXT ").append(query.getSelect().getLimit()).append(" ROWS ONLY");
} else if (query.hasOffset()) {
sql.append('\n').append("OFFSET ").append(query.getOffset()).append(" ROWS");
}
return sql.toString();
}
......@@ -165,9 +178,9 @@ public class SQLServerTranslator extends JDBCTranslator {
String sql = null;
for(int i = 0; i < clause.size(); i++) {
if (i == 0) {
sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "") + (clause.hasLimit() ? " TOP " + clause.getLimit() + " " : "");
} else
if (i == 0)
sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "");
else
sql += " " + clause.getSeparator(i);
sql += " " + translate(clause.get(i));
......
......@@ -51,6 +51,45 @@ public class TestADQLParser {
public void tearDown() throws Exception {
}
@Test
public void testOffset() {
// CASE: No OFFSET in ADQL-2.0
ADQLParser parser = new ADQLParser(ADQLVersion.V2_0);
try {
parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 10");
fail("OFFSET should not be allowed with ADQL-2.0!");
} catch(Exception ex) {
assertEquals(ParseException.class, ex.getClass());
assertEquals(" Encountered \"OFFSET\". Was expecting one of: <EOF> \",\" \";\" \"ASC\" \"DESC\" ", ex.getMessage());
}
// CASE: OFFSET allowed in ADQL-2.1
parser = new ADQLParser(ADQLVersion.V2_1);
try {
assertEquals("SELECT *\nFROM foo\nOFFSET 10", parser.parseQuery("SELECT * FROM foo OFFSET 10").toADQL());
assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 10", parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 10").toADQL());
assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 0", parser.parseQuery("SELECT * FROM foo ORDER BY id OFFSET 0").toADQL());
assertEquals("SELECT TOP 5 *\nFROM foo\nORDER BY id ASC\nOFFSET 10", parser.parseQuery("SELECT TOP 5 * FROM foo ORDER BY id OFFSET 10").toADQL());
} catch(Exception ex) {
ex.printStackTrace();
fail("Unexpected error with a valid OFFSET syntax! (see console for more details)");
}
// CASE: Only an unsigned integer constant is allowed
String[] offsets = new String[]{ "-1", "colOffset", "2*5" };
String[] expectedErrors = new String[]{ " Encountered \"-\". Was expecting: <UNSIGNED_INTEGER> ", " Encountered \"colOffset\". Was expecting: <UNSIGNED_INTEGER> ", " Encountered \"*\". Was expecting one of: <EOF> \";\" " };
for(int i = 0; i < offsets.length; i++) {
try {
parser.parseQuery("SELECT * FROM foo OFFSET " + offsets[i]);
fail("Incorrect offset expression (\"" + offsets[i] + "\"). This test should have failed.");
} catch(Exception ex) {
assertEquals(ParseException.class, ex.getClass());
assertEquals(expectedErrors[i], ex.getMessage());
}
}
}
@Test
public void testColumnReference() {
ADQLParser parser = new ADQLParser();
......
......@@ -13,7 +13,6 @@ import adql.query.ADQLObject;
import adql.query.ADQLOrder;
import adql.query.ADQLQuery;
import adql.query.ClauseSelect;
import adql.query.constraint.In;
import adql.query.from.ADQLTable;
import adql.query.operand.ADQLColumn;
import adql.query.operand.ADQLOperand;
......@@ -64,7 +63,7 @@ public class TestIN {
assertEquals("typeObj NOT IN (SELECT DISTINCT TOP 10 typeObj\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL());
// check the SQL translation:
try {
assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLimit 10)", translator.translate(myIn));
assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLIMIT 10)", translator.translate(myIn));
} catch(Exception ex) {
ex.printStackTrace();
fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!");
......
......@@ -9,6 +9,8 @@ import org.junit.Test;
import adql.db.DBType;
import adql.db.FunctionDef;
import adql.db.STCS.Region;
import adql.parser.ADQLParser;
import adql.parser.ADQLParser.ADQLVersion;
import adql.parser.grammar.ParseException;
import adql.query.IdentifierField;
import adql.query.operand.ADQLColumn;
......@@ -34,6 +36,34 @@ public class TestJDBCTranslator {
public void setUp() throws Exception {
}
@Test
public void testTranslateOffset() {
JDBCTranslator tr = new AJDBCTranslator();
ADQLParser parser = new ADQLParser(ADQLVersion.V2_1);
try {
// CASE: Only OFFSET
assertEquals("SELECT *\nFROM foo\nOFFSET 10", tr.translate(parser.parseQuery("Select * From foo OffSet 10")));
// CASE: Only OFFSET = 0
assertEquals("SELECT *\nFROM foo\nOFFSET 0", tr.translate(parser.parseQuery("Select * From foo OffSet 0")));
// CASE: TOP + OFFSET
assertEquals("SELECT *\nFROM foo\nLIMIT 5\nOFFSET 10", tr.translate(parser.parseQuery("Select Top 5 * From foo OffSet 10")));
// CASE: TOP + ORDER BY + OFFSET
assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nLIMIT 5\nOFFSET 10", tr.translate(parser.parseQuery("Select Top 5 * From foo Order By id Asc OffSet 10")));
} catch(ParseException pe) {
pe.printStackTrace(System.err);
fail("Unexpected failed query parsing! (see console for more details)");
} catch(Exception e) {
e.printStackTrace(System.err);
fail("There should have been no problem to translate a query with offset into SQL.");
}
}
@Test
public void testTranslateStringConstant() {
JDBCTranslator tr = new AJDBCTranslator();
......
......@@ -14,6 +14,7 @@ import adql.db.DBTable;
import adql.db.DefaultDBColumn;
import adql.db.DefaultDBTable;
import adql.parser.ADQLParser;
import adql.parser.ADQLParser.ADQLVersion;
import adql.parser.SQLServer_ADQLQueryFactory;
import adql.parser.grammar.ParseException;
import adql.query.ADQLQuery;
......@@ -37,6 +38,34 @@ public class TestSQLServerTranslator {
tables.add(t);
}
@Test
public void testTranslateOffset() {
SQLServerTranslator tr = new SQLServerTranslator();
ADQLParser parser = new ADQLParser(ADQLVersion.V2_1);
try {
// CASE: Only OFFSET
assertEquals("SELECT *\nFROM foo\nOFFSET 10 ROWS", tr.translate(parser.parseQuery("Select * From foo OffSet 10")));
// CASE: Only OFFSET = 0
assertEquals("SELECT *\nFROM foo\nOFFSET 0 ROWS", tr.translate(parser.parseQuery("Select * From foo OffSet 0")));
// CASE: TOP + OFFSET
assertEquals("SELECT *\nFROM foo\nOFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY", tr.translate(parser.parseQuery("Select Top 5 * From foo OffSet 10")));
// CASE: TOP + ORDER BY + OFFSET
assertEquals("SELECT *\nFROM foo\nORDER BY id ASC\nOFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY", tr.translate(parser.parseQuery("Select Top 5 * From foo Order By id Asc OffSet 10")));
} catch(ParseException pe) {
pe.printStackTrace(System.err);
fail("Unexpected failed query parsing! (see console for more details)");
} catch(Exception e) {
e.printStackTrace(System.err);
fail("There should have been no problem to translate a query with offset into SQL.");
}
}
@Test
public void testNaturalJoin() {
final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;";
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment