Commit 046ac691 authored by Grégory Mantelet's avatar Grégory Mantelet
Browse files

[ADQL] Efficient PgSphere translation of the Preferred X-Match Syntax described

in the ADQL-2.1 standard (in section 4.2.7).

Currently, geometries are not translated in MySQL and MS-SQL Server translators.
So, the preferred xmatch syntax has not been implemented in these translators.
parent 5cc544e4
Loading
Loading
Loading
Loading
+133 −57
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ package adql.translator;
 * 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)
 */

@@ -32,6 +32,8 @@ import adql.parser.grammar.ParseException;
import adql.query.TextPosition;
import adql.query.constraint.Comparison;
import adql.query.constraint.ComparisonOperator;
import adql.query.operand.ADQLOperand;
import adql.query.operand.StringConstant;
import adql.query.operand.function.geometry.AreaFunction;
import adql.query.operand.function.geometry.BoxFunction;
import adql.query.operand.function.geometry.CentroidFunction;
@@ -51,8 +53,15 @@ import adql.query.operand.function.geometry.PolygonFunction;
 * 	class. The other functions are managed by {@link PostgreSQLTranslator}.
 * </p>
 *
 * <p><i><b>Implementation note:</b>
 * 	The preferred xmatch syntax described in the section 4.2.7 of the ADQL
 * 	standard (here ADQL-2.1) is implemented here so that such query is as
 * 	efficient as a <code>CONTAINS(POINT(...), CIRCLE(...)) = 1</code>.
 * 	See {@link #translate(adql.query.constraint.Comparison)} for more details.
 * </i></p>
 *
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 1.4 (07/2017)
 * @version 2.0 (08/2019)
 */
public class PgSphereTranslator extends PostgreSQLTranslator {

@@ -201,14 +210,80 @@ public class PgSphereTranslator extends PostgreSQLTranslator {

	@Override
	public String translate(Comparison comp) throws TranslationException {

		// CONTAINS or INTERSECTS(...) on left:
		if ((comp.getLeftOperand() instanceof ContainsFunction || comp.getLeftOperand() instanceof IntersectsFunction) && (comp.getOperator() == ComparisonOperator.EQUAL || comp.getOperator() == ComparisonOperator.NOT_EQUAL) && comp.getRightOperand().isNumeric())
			return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " '" + translate(comp.getRightOperand()) + "'";

		// CONTAINS or INTERSECTS(...) on right:
		else if ((comp.getRightOperand() instanceof ContainsFunction || comp.getRightOperand() instanceof IntersectsFunction) && (comp.getOperator() == ComparisonOperator.EQUAL || comp.getOperator() == ComparisonOperator.NOT_EQUAL) && comp.getLeftOperand().isNumeric())
			return "'" + translate(comp.getLeftOperand()) + "' " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand());

		// Preferred xmatch syntax described in the ADQL standard:
		else if (isPreferredXmatchSyntax(comp)) {
			// extract the DISTANCE and its compared value:
			DistanceFunction distFct;
			ADQLOperand numericOperand;
			if (comp.getLeftOperand() instanceof DistanceFunction) {
				distFct = (DistanceFunction)comp.getLeftOperand();
				numericOperand = comp.getRightOperand();
			} else {
				distFct = (DistanceFunction)comp.getRightOperand();
				numericOperand = comp.getLeftOperand();
			}
			try {
				// build the CIRCLE to use in the artificial CONTAINS:
				CircleFunction circleFct = new CircleFunction(new StringConstant(""), ((PointFunction)distFct.getParameter(1)).getCoord1(), ((PointFunction)distFct.getParameter(1)).getCoord2(), numericOperand);
				// adapt the translation in function of the comp. operator:
				switch(comp.getOperator()) {
					case LESS_THAN:
						return "((" + translate(distFct.getParameter(0)) + " @ " + translate(circleFct) + ") = '1' AND " + super.translate(comp) + ")";
					case LESS_OR_EQUAL:
						return "((" + translate(distFct.getParameter(0)) + " @ " + translate(circleFct) + ") = '1'" + ")";
					case GREATER_THAN:
						return "((" + translate(distFct.getParameter(0)) + " @ " + translate(circleFct) + ") = '0' AND " + super.translate(comp) + ")";
					case GREATER_OR_EQUAL:
						return "((" + translate(distFct.getParameter(0)) + " @ " + translate(circleFct) + ") = '0'" + ")";
					default: // theoretically, this case never happens!
						return super.translate(comp);
				}
			} catch(Exception ex) {
				throw new TranslationException("Impossible to translate the following xmatch syntax: \"" + comp.toADQL() + "\"! Cause: " + ex.getMessage(), ex);
			}
		}
		// Any other comparison:
		else
			return super.translate(comp);
	}

	/**
	 * Test whether the given comparison corresponds to the preferred xmatch
	 * syntax described in the ADQL standard.
	 *
	 * <p>
	 * 	In other words, this function returns <code>true</code> if the following
	 * 	conditions are met:
	 * </p>
	 * <ul>
	 * 	<li>the left operand is DISTANCE and the right one is a numeric,
	 * 		or vice-versa,</li>
	 * 	<li>and the comparison operator is &lt;, &le;, &gt; or &ge;.</li>
	 * </ul>
	 *
	 * @param comp	The comparison to test.
	 *
	 * @return	<code>true</code> if it corresponds to a valid xmatch syntax,
	 *        	<code>false</code> otherwise.
	 *
	 * @since 2.0
	 */
	protected boolean isPreferredXmatchSyntax(final Comparison comp) {
		if ((comp.getLeftOperand() instanceof DistanceFunction && comp.getRightOperand().isNumeric()) || (comp.getLeftOperand().isNumeric() && comp.getRightOperand() instanceof DistanceFunction))
			return (comp.getOperator() == ComparisonOperator.LESS_THAN || comp.getOperator() == ComparisonOperator.LESS_OR_EQUAL || comp.getOperator() == ComparisonOperator.GREATER_THAN || comp.getOperator() == ComparisonOperator.GREATER_OR_EQUAL);
		else
			return false;
	}

	@Override
	public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params) {
		// If no type is provided return VARCHAR:
@@ -427,7 +502,8 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
		/**
		 * Build the PgSphere parser.
		 */
		public PgSphereGeometryParser(){}
		public PgSphereGeometryParser() {
		}

		/**
		 * Prepare the parser in order to read the given PgSphere expression.
+72 −34
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ import org.postgresql.util.PGobject;
import adql.db.DBType;
import adql.db.DBType.DBDatatype;
import adql.db.STCS.Region;
import adql.parser.ADQLParser;
import adql.parser.grammar.ParseException;
import adql.query.operand.NumericConstant;
import adql.query.operand.StringConstant;
@@ -30,16 +31,20 @@ import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
public class TestPgSphereTranslator {

	@BeforeClass
	public static void setUpBeforeClass() throws Exception{}
	public static void setUpBeforeClass() throws Exception {
	}

	@AfterClass
	public static void tearDownAfterClass() throws Exception{}
	public static void tearDownAfterClass() throws Exception {
	}

	@Before
	public void setUp() throws Exception{}
	public void setUp() throws Exception {
	}

	@After
	public void tearDown() throws Exception{}
	public void tearDown() throws Exception {
	}

	@Test
	public void testTranslateCentroidFunction() {
@@ -349,4 +354,37 @@ public class TestPgSphereTranslator {
		}
	}

	@Test
	public void testTranslateXMatch() {
		PgSphereTranslator translator = new PgSphereTranslator();
		ADQLParser parser = new ADQLParser();

		try {
			// CASE: CONTAINS(POINT, CIRCLE) = 1
			assertEquals("(spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.))) = '1'", translator.translate(parser.parseWhere("WHERE CONTAINS(POINT('', ra, dec), CIRCLE('', 0, 0, 1.)) = 1").get(0)));

			// CASE: 1 = CONTAINS(POINT, CIRCLE)
			assertEquals("'1' = (spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.)))", translator.translate(parser.parseWhere("WHERE 1=CONTAINS(POINT('', ra, dec), CIRCLE('', 0, 0, 1.))").get(0)));

			// CASE: DISTANCE(...) <= 1
			assertEquals("((spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.))) = '1')", translator.translate(parser.parseWhere("WHERE DISTANCE(POINT('', ra, dec), POINT('', 0, 0)) <= 1.").get(0)));

			// CASE: DISTANCE(...) >= 1
			assertEquals("((spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.))) = '0')", translator.translate(parser.parseWhere("WHERE DISTANCE(POINT('', ra, dec), POINT('', 0, 0)) >= 1.").get(0)));

			// CASE: DISTANCE(...) < 1
			assertEquals("((spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.))) = '1' AND degrees(spoint(radians(ra),radians(dec)) <-> spoint(radians(0),radians(0))) < 1.)", translator.translate(parser.parseWhere("WHERE DISTANCE(POINT('', ra, dec), POINT('', 0, 0)) < 1.").get(0)));

			// CASE: DISTANCE(...) > 1
			assertEquals("((spoint(radians(ra),radians(dec)) @ scircle(spoint(radians(0),radians(0)),radians(1.))) = '0' AND degrees(spoint(radians(ra),radians(dec)) <-> spoint(radians(0),radians(0))) > 1.)", translator.translate(parser.parseWhere("WHERE DISTANCE(POINT('', ra, dec), POINT('', 0, 0)) > 1.").get(0)));

		} catch(ParseException pe) {
			pe.printStackTrace();
			fail("Failed parsing before translation!");
		} catch(Exception ex) {
			ex.printStackTrace();
			fail("Unexpected failure of xmatch translation! (see console for more details)");
		}
	}

}