From 046ac6911b9a3a5d420cb99ebdb0c6ed284d6a8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9gory=20Mantelet?=
 <gregory.mantelet@astro.unistra.fr>
Date: Wed, 21 Aug 2019 10:28:10 +0200
Subject: [PATCH] [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.
---
 src/adql/translator/PgSphereTranslator.java   | 190 ++++++++++++------
 .../translator/TestPgSphereTranslator.java    | 106 ++++++----
 2 files changed, 205 insertions(+), 91 deletions(-)

diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java
index 7c34e1a..ef8cfdb 100644
--- a/src/adql/translator/PgSphereTranslator.java
+++ b/src/adql/translator/PgSphereTranslator.java
@@ -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 {
 
@@ -68,7 +77,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	 *
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator()
 	 */
-	public PgSphereTranslator(){
+	public PgSphereTranslator() {
 		super();
 	}
 
@@ -80,7 +89,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	 *
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean)
 	 */
-	public PgSphereTranslator(boolean allCaseSensitive){
+	public PgSphereTranslator(boolean allCaseSensitive) {
 		super(allCaseSensitive);
 	}
 
@@ -94,12 +103,12 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	 *
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean, boolean, boolean, boolean)
 	 */
-	public PgSphereTranslator(boolean catalog, boolean schema, boolean table, boolean column){
+	public PgSphereTranslator(boolean catalog, boolean schema, boolean table, boolean column) {
 		super(catalog, schema, table, column);
 	}
 
 	@Override
-	public String translate(PointFunction point) throws TranslationException{
+	public String translate(PointFunction point) throws TranslationException {
 		StringBuffer str = new StringBuffer("spoint(");
 		str.append("radians(").append(translate(point.getCoord1())).append("),");
 		str.append("radians(").append(translate(point.getCoord2())).append("))");
@@ -107,7 +116,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public String translate(CircleFunction circle) throws TranslationException{
+	public String translate(CircleFunction circle) throws TranslationException {
 		StringBuffer str = new StringBuffer("scircle(");
 		str.append("spoint(radians(").append(translate(circle.getCoord1())).append("),");
 		str.append("radians(").append(translate(circle.getCoord2())).append(")),");
@@ -116,7 +125,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public String translate(BoxFunction box) throws TranslationException{
+	public String translate(BoxFunction box) throws TranslationException {
 		StringBuffer str = new StringBuffer("sbox(");
 
 		str.append("spoint(").append("radians(").append(translate(box.getCoord1())).append("-(").append(translate(box.getWidth())).append("/2.0)),");
@@ -129,15 +138,15 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public String translate(PolygonFunction polygon) throws TranslationException{
-		try{
+	public String translate(PolygonFunction polygon) throws TranslationException {
+		try {
 			StringBuffer str = new StringBuffer("spoly('{'");
 
-			if (polygon.getNbParameters() > 2){
+			if (polygon.getNbParameters() > 2) {
 				PointFunction point = new PointFunction(polygon.getCoordinateSystem(), polygon.getParameter(1), polygon.getParameter(2));
 				str.append(" || ").append(translate(point));
 
-				for(int i = 3; i < polygon.getNbParameters() && i + 1 < polygon.getNbParameters(); i += 2){
+				for(int i = 3; i < polygon.getNbParameters() && i + 1 < polygon.getNbParameters(); i += 2) {
 					point.setCoord1(polygon.getParameter(i));
 					point.setCoord2(polygon.getParameter(i + 1));
 					str.append(" || ',' || ").append(translate(point));
@@ -147,14 +156,14 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 			str.append(" || '}')");
 
 			return str.toString();
-		}catch(Exception e){
+		} catch(Exception e) {
 			e.printStackTrace();
 			throw new TranslationException(e);
 		}
 	}
 
 	@Override
-	public String translate(ExtractCoord extractCoord) throws TranslationException{
+	public String translate(ExtractCoord extractCoord) throws TranslationException {
 		StringBuffer str = new StringBuffer("degrees(");
 		if (extractCoord.getName().equalsIgnoreCase("COORD1"))
 			str.append("long(");
@@ -165,52 +174,118 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public String translate(DistanceFunction fct) throws TranslationException{
+	public String translate(DistanceFunction fct) throws TranslationException {
 		StringBuffer str = new StringBuffer("degrees(");
 		str.append(translate(fct.getP1())).append(" <-> ").append(translate(fct.getP2())).append(")");
 		return str.toString();
 	}
 
 	@Override
-	public String translate(AreaFunction areaFunction) throws TranslationException{
+	public String translate(AreaFunction areaFunction) throws TranslationException {
 		StringBuffer str = new StringBuffer("degrees(degrees(area(");
 		str.append(translate(areaFunction.getParameter())).append(")))");
 		return str.toString();
 	}
 
 	@Override
-	public String translate(CentroidFunction centroidFunction) throws TranslationException{
+	public String translate(CentroidFunction centroidFunction) throws TranslationException {
 		StringBuffer str = new StringBuffer("center(");
 		str.append(translate(centroidFunction.getParameter(0))).append(")");
 		return str.toString();
 	}
 
 	@Override
-	public String translate(ContainsFunction fct) throws TranslationException{
+	public String translate(ContainsFunction fct) throws TranslationException {
 		StringBuffer str = new StringBuffer("(");
 		str.append(translate(fct.getLeftParam())).append(" @ ").append(translate(fct.getRightParam())).append(")");
 		return str.toString();
 	}
 
 	@Override
-	public String translate(IntersectsFunction fct) throws TranslationException{
+	public String translate(IntersectsFunction fct) throws TranslationException {
 		StringBuffer str = new StringBuffer("(");
 		str.append(translate(fct.getLeftParam())).append(" && ").append(translate(fct.getRightParam())).append(")");
 		return str.toString();
 	}
 
 	@Override
-	public String translate(Comparison comp) throws TranslationException{
+	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){
+	public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params) {
 		// If no type is provided return VARCHAR:
 		if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0)
 			return null;
@@ -227,8 +302,8 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public String convertTypeToDB(final DBType type){
-		if (type != null){
+	public String convertTypeToDB(final DBType type) {
+		if (type != null) {
 			if (type.type == DBDatatype.POINT)
 				return "spoint";
 			else if (type.type == DBDatatype.REGION)
@@ -238,7 +313,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{
+	public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException {
 		// A NULL value stays NULL:
 		if (jdbcColValue == null)
 			return null;
@@ -271,17 +346,17 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	@Override
-	public Object translateGeometryToDB(final Region region) throws ParseException{
+	public Object translateGeometryToDB(final Region region) throws ParseException {
 		// A NULL value stays NULL:
 		if (region == null)
 			return null;
 
-		try{
+		try {
 			PGobject dbRegion = new PGobject();
 			StringBuffer buf;
 
 			// Build the PgSphere expression from the given geometry in function of its type:
-			switch(region.type){
+			switch(region.type) {
 
 				case POSITION:
 					dbRegion.setType("spoint");
@@ -291,7 +366,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 				case POLYGON:
 					dbRegion.setType("spoly");
 					buf = new StringBuffer("{");
-					for(int i = 0; i < region.coordinates.length; i++){
+					for(int i = 0; i < region.coordinates.length; i++) {
 						if (i > 0)
 							buf.append(',');
 						buf.append('(').append(region.coordinates[i][0]).append("d,").append(region.coordinates[i][1]).append("d)");
@@ -324,7 +399,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 					throw new ParseException("Unsupported geometrical region: \"" + region.type + "\"!");
 			}
 			return dbRegion;
-		}catch(SQLException e){
+		} catch(SQLException e) {
 			/* This error could never happen! */
 			return null;
 		}
@@ -345,10 +420,10 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	 *
 	 * @since 1.3
 	 */
-	protected String circleToPolygon(final double[] center, final double radius){
+	protected String circleToPolygon(final double[] center, final double radius) {
 		double angle = 0, x, y;
 		StringBuffer buf = new StringBuffer();
-		while(angle < 2 * Math.PI){
+		while(angle < 2 * Math.PI) {
 			x = center[0] + radius * Math.cos(angle);
 			y = center[1] + radius * Math.sin(angle);
 			if (buf.length() > 0)
@@ -419,7 +494,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 			private static final long serialVersionUID = 1L;
 
 			/** Build a simple EOEException. */
-			public EOEException(){
+			public EOEException() {
 				super("Unexpected End Of PgSphere Expression!");
 			}
 		}
@@ -427,14 +502,15 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		/**
 		 * Build the PgSphere parser.
 		 */
-		public PgSphereGeometryParser(){}
+		public PgSphereGeometryParser() {
+		}
 
 		/**
 		 * Prepare the parser in order to read the given PgSphere expression.
 		 *
 		 * @param newStcs	New PgSphere expression to parse from now.
 		 */
-		private void init(final String newExpr){
+		private void init(final String newExpr) {
 			expr = (newExpr == null) ? "" : newExpr;
 			token = null;
 			buffer = new StringBuffer();
@@ -447,7 +523,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If other non-space characters remains.
 		 */
-		private void end() throws ParseException{
+		private void end() throws ParseException {
 			// Skip all spaces:
 			skipSpaces();
 
@@ -464,7 +540,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		/**
 		 * Tool function which skips all next space characters until the next meaningful characters.
 		 */
-		private void skipSpaces(){
+		private void skipSpaces() {
 			while(pos < expr.length() && Character.isWhitespace(expr.charAt(pos)))
 				pos++;
 		}
@@ -480,7 +556,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @return	The full read word/token, or NULL if the end has been reached.
 		 */
-		private String nextToken() throws EOEException{
+		private String nextToken() throws EOEException {
 			// Skip all spaces:
 			skipSpaces();
 
@@ -489,8 +565,8 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 
 			// Fetch all characters until word separator (a space or a open/close parenthesis):
 			buffer.append(expr.charAt(pos++));
-			if (!isSyntaxSeparator(buffer.charAt(0))){
-				while(pos < expr.length() && !isSyntaxSeparator(expr.charAt(pos))){
+			if (!isSyntaxSeparator(buffer.charAt(0))) {
+				while(pos < expr.length() && !isSyntaxSeparator(expr.charAt(pos))) {
 					// skip eventual white-spaces:
 					if (!Character.isWhitespace(expr.charAt(pos)))
 						buffer.append(expr.charAt(pos));
@@ -515,7 +591,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @return	<i>true</i> if the given character must be considered as a separator, <i>false</i> otherwise.
 		 */
-		private static boolean isSyntaxSeparator(final char c){
+		private static boolean isSyntaxSeparator(final char c) {
 			return (c == COMMA || c == DEGREE || c == HOUR || c == MINUTE || c == SECOND || c == OPEN_PAR || c == CLOSE_PAR || c == LESS_THAN || c == GREATER_THAN || c == OPEN_BRACE || c == CLOSE_BRACE);
 		}
 
@@ -527,7 +603,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the next character is not matching the given one.
 		 */
-		private void nextToken(final char expected) throws ParseException{
+		private void nextToken(final char expected) throws ParseException {
 			// Skip all spaces:
 			skipSpaces();
 
@@ -537,7 +613,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 
 			// Fetch the next character:
 			char t = expr.charAt(pos++);
-			token = new String(new char[]{t});
+			token = new String(new char[]{ t });
 
 			/* Test the the fetched character with the expected one
 			 * and throw an error if they don't match: */
@@ -554,7 +630,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the PgSphere syntax of the given expression is wrong or does not correspond to a point.
 		 */
-		public Region parsePoint(final String pgsphereExpr) throws ParseException{
+		public Region parsePoint(final String pgsphereExpr) throws ParseException {
 			// Init the parser:
 			init(pgsphereExpr);
 			// Parse the expression:
@@ -575,13 +651,13 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 * @see #parseAngle()
 		 * @see #parsePoint(String)
 		 */
-		private double[] parsePoint() throws ParseException{
+		private double[] parsePoint() throws ParseException {
 			nextToken(OPEN_PAR);
 			double x = parseAngle();
 			nextToken(COMMA);
 			double y = parseAngle();
 			nextToken(CLOSE_PAR);
-			return new double[]{x,y};
+			return new double[]{ x, y };
 		}
 
 		/**
@@ -593,7 +669,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the PgSphere syntax of the given expression is wrong or does not correspond to a circle.
 		 */
-		public Region parseCircle(final String pgsphereExpr) throws ParseException{
+		public Region parseCircle(final String pgsphereExpr) throws ParseException {
 			// Init the parser:
 			init(pgsphereExpr);
 
@@ -620,7 +696,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the PgSphere syntax of the given expression is wrong or does not correspond to a box.
 		 */
-		public Region parseBox(final String pgsphereExpr) throws ParseException{
+		public Region parseBox(final String pgsphereExpr) throws ParseException {
 			// Init the parser:
 			init(pgsphereExpr);
 
@@ -637,7 +713,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 			// Build the STC Box region:
 			double width = Math.abs(northeast[0] - southwest[0]),
 					height = Math.abs(northeast[1] - southwest[1]);
-			double[] center = new double[]{northeast[0] - width / 2,northeast[1] - height / 2};
+			double[] center = new double[]{ northeast[0] - width / 2, northeast[1] - height / 2 };
 			return new Region(null, center, width, height);
 		}
 
@@ -650,7 +726,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the PgSphere syntax of the given expression is wrong or does not correspond to a point.
 		 */
-		public Region parsePolygon(final String pgsphereExpr) throws ParseException{
+		public Region parsePolygon(final String pgsphereExpr) throws ParseException {
 			// Init the parser:
 			init(pgsphereExpr);
 
@@ -692,19 +768,19 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 		 *
 		 * @throws ParseException	If the angle syntax is wrong or not supported.
 		 */
-		private double parseAngle() throws ParseException{
+		private double parseAngle() throws ParseException {
 			int oldPos = pos;
 			String number = nextToken();
-			try{
+			try {
 				double degrees = Double.parseDouble(number);
 				int sign = (degrees < 0) ? -1 : 1;
 				degrees = Math.abs(degrees);
 
 				oldPos = pos;
-				try{
+				try {
 					if (nextToken().length() == 1 && token.charAt(0) == HOUR)
 						sign *= 15;
-					else if (token.length() != 1 || token.charAt(0) != DEGREE){
+					else if (token.length() != 1 || token.charAt(0) != DEGREE) {
 						degrees = degrees * 180 / Math.PI;
 						pos -= token.length();
 						return degrees * sign;
@@ -714,10 +790,10 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 					number = nextToken();
 					if (nextToken().length() == 1 && token.charAt(0) == MINUTE)
 						degrees += Double.parseDouble(number) / 60;
-					else if (token.length() == 1 && token.charAt(0) == SECOND){
+					else if (token.length() == 1 && token.charAt(0) == SECOND) {
 						degrees += Double.parseDouble(number) / 3600;
 						return degrees * sign;
-					}else{
+					} else {
 						pos = oldPos;
 						return degrees * sign;
 					}
@@ -728,13 +804,13 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 						degrees += Double.parseDouble(number) / 3600;
 					else
 						pos = oldPos;
-				}catch(EOEException ex){
+				} catch(EOEException ex) {
 					pos = oldPos;
 				}
 
 				return degrees * sign;
 
-			}catch(NumberFormatException nfe){
+			} catch(NumberFormatException nfe) {
 				throw new ParseException("Incorrect numeric syntax: \"" + number + "\"!", new TextPosition(1, pos - token.length(), 1, pos));
 			}
 		}
diff --git a/test/adql/translator/TestPgSphereTranslator.java b/test/adql/translator/TestPgSphereTranslator.java
index 825f21a..0f48b09 100644
--- a/test/adql/translator/TestPgSphereTranslator.java
+++ b/test/adql/translator/TestPgSphereTranslator.java
@@ -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,31 +31,35 @@ 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(){
-		try{
+	public void testTranslateCentroidFunction() {
+		try {
 			PgSphereTranslator translator = new PgSphereTranslator();
 			CentroidFunction centfc = new CentroidFunction(new GeometryValue<GeometryFunction>(new CircleFunction(new StringConstant("ICRS"), new NumericConstant(128.23), new NumericConstant(0.53), new NumericConstant(2))));
 			assertEquals("center(scircle(spoint(radians(128.23),radians(0.53)),radians(2)))", translator.translate(centfc));
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace(System.err);
 			fail("An error occured while building a simple CentroidFunction! (see the console for more details)");
 		}
 	}
 
 	@Test
-	public void testConvertTypeFromDB(){
+	public void testConvertTypeFromDB() {
 		PgSphereTranslator translator = new PgSphereTranslator();
 
 		// POINT
@@ -83,7 +88,7 @@ public class TestPgSphereTranslator {
 	}
 
 	@Test
-	public void testConvertTypeToDB(){
+	public void testConvertTypeToDB() {
 		PgSphereTranslator translator = new PgSphereTranslator();
 
 		// NULL
@@ -97,20 +102,20 @@ public class TestPgSphereTranslator {
 	}
 
 	@Test
-	public void testTranslateGeometryFromDB(){
+	public void testTranslateGeometryFromDB() {
 		PgSphereTranslator translator = new PgSphereTranslator();
 		PGobject pgo = new PGobject();
 
 		// NULL
-		try{
+		try {
 			assertNull(translator.translateGeometryFromDB(null));
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 
 		// SPOINT
-		try{
+		try {
 			pgo.setType("spoint");
 			pgo.setValue("(0.1 , 0.2)");
 			Region r = translator.translateGeometryFromDB(pgo);
@@ -131,13 +136,13 @@ public class TestPgSphereTranslator {
 			r = translator.translateGeometryFromDB(pgo);
 			assertEquals(5.72957, r.coordinates[0][0], 1e-5);
 			assertEquals(11.45915, r.coordinates[0][1], 1e-5);
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 
 		// SCIRCLE
-		try{
+		try {
 			pgo.setType("scircle");
 			pgo.setValue("<(0.1,-0.2),1>");
 			Region r = translator.translateGeometryFromDB(pgo);
@@ -162,13 +167,13 @@ public class TestPgSphereTranslator {
 			assertEquals(5.72957, r.coordinates[0][0], 1e-5);
 			assertEquals(-11.45915, r.coordinates[0][1], 1e-5);
 			assertEquals(57.29577, r.radius, 1e-5);
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 
 		// SBOX
-		try{
+		try {
 			pgo.setType("sbox");
 			pgo.setValue("((0.1,0.2),(0.5,0.5))");
 			Region r = translator.translateGeometryFromDB(pgo);
@@ -197,13 +202,13 @@ public class TestPgSphereTranslator {
 			assertEquals(20.05352, r.coordinates[0][1], 1e-5);
 			assertEquals(22.91831, r.width, 1e-5);
 			assertEquals(17.18873, r.height, 1e-5);
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 
 		// SPOLY
-		try{
+		try {
 			pgo.setType("spoly");
 			pgo.setValue("{(0.789761486527434 , 0.00436332312998582),(0.789761486527434 , 0.00872664625997165),(0.785398163397448 , 0.00872664625997165),(0.785398163397448 , 0.00436332312998582),(0.781034840267463 , 0.00436332312998582),(0.781034840267463 , 0),(0.785398163397448 , 0)}");
 			Region r = translator.translateGeometryFromDB(pgo);
@@ -272,47 +277,47 @@ public class TestPgSphereTranslator {
 			assertEquals(0, r.coordinates[5][1], 1e-2);
 			assertEquals(45, r.coordinates[6][0], 1e-2);
 			assertEquals(0, r.coordinates[6][1], 1e-2);
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 
 		// OTHER
-		try{
+		try {
 			translator.translateGeometryFromDB(new Double(12.3));
 			fail("The translation of a Double as a geometry is not supported!");
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			assertTrue(t instanceof ParseException);
 			assertEquals("Incompatible type! The column value \"12.3\" was supposed to be a geometrical object.", t.getMessage());
 		}
-		try{
+		try {
 			pgo.setType("sline");
 			pgo.setValue("( -90d, -20d, 200d, XYZ ), 30d ");
 			translator.translateGeometryFromDB(pgo);
 			fail("The translation of a sline is not supported!");
-		}catch(Throwable t){
+		} catch(Throwable t) {
 			assertTrue(t instanceof ParseException);
 			assertEquals("Unsupported PgSphere type: \"sline\"! Impossible to convert the column value \"( -90d, -20d, 200d, XYZ ), 30d \" into a Region.", t.getMessage());
 		}
 	}
 
 	@Test
-	public void testTranslateGeometryToDB(){
+	public void testTranslateGeometryToDB() {
 		PgSphereTranslator translator = new PgSphereTranslator();
 
-		try{
+		try {
 			// NULL
 			assertNull(translator.translateGeometryToDB(null));
 
 			// POSITION
-			Region r = new Region(null, new double[]{45,0});
+			Region r = new Region(null, new double[]{ 45, 0 });
 			PGobject pgo = (PGobject)translator.translateGeometryToDB(r);
 			assertNotNull(pgo);
 			assertEquals("spoint", pgo.getType());
 			assertEquals("(45.0d,0.0d)", pgo.getValue());
 
 			// CIRCLE
-			r = new Region(null, new double[]{45,0}, 1.2);
+			r = new Region(null, new double[]{ 45, 0 }, 1.2);
 			pgo = (PGobject)translator.translateGeometryToDB(r);
 			assertNotNull(pgo);
 			assertEquals("spoly", pgo.getType());
@@ -320,33 +325,66 @@ public class TestPgSphereTranslator {
 			assertEquals("{(46.2d,0.0d),(46.17694233d,0.23410838d),(46.10865543d,0.45922011d),(45.99776353d,0.66668427d),(45.84852813d,0.84852813d),(45.66668427d,0.99776353d),(45.45922011d,1.10865543d),(45.23410838d,1.17694233d),(45.0d,1.2d),(44.76589161d,1.17694233d),(44.54077988d,1.10865543d),(44.33331572d,0.99776353d),(44.15147186d,0.84852813d),(44.00223646d,0.66668427d),(43.89134456d,0.45922011d),(43.82305766d,0.23410838d),(43.8d,-9.188564877424678E-16d),(43.82305766d,-0.23410838d),(43.89134456d,-0.45922011d),(44.00223646d,-0.66668427d),(44.15147186d,-0.84852813d),(44.33331572d,-0.99776353d),(44.54077988d,-1.10865543d),(44.76589161d,-1.17694233d),(45.0d,-1.2d),(45.23410838d,-1.17694233d),(45.45922011d,-1.10865543d),(45.66668427d,-0.99776353d),(45.84852813d,-0.84852813d),(45.99776353d,-0.66668427d),(46.10865543d,-0.45922011d),(46.17694233d,-0.23410838d)}", fp8.matcher(pgo.getValue()).replaceAll("$1d"));
 
 			// BOX
-			r = new Region(null, new double[]{45,0}, 1.2, 5);
+			r = new Region(null, new double[]{ 45, 0 }, 1.2, 5);
 			pgo = (PGobject)translator.translateGeometryToDB(r);
 			assertNotNull(pgo);
 			assertEquals("spoly", pgo.getType());
 			assertEquals("{(44.4d,-2.5d),(44.4d,2.5d),(45.6d,2.5d),(45.6d,-2.5d)}", pgo.getValue());
 
 			// POLYGON
-			r = new Region(null, new double[][]{new double[]{45.25,0.25},new double[]{45.25,0.5},new double[]{45,0.5},new double[]{45,0.25},new double[]{44.75,0.25},new double[]{44.75,0},new double[]{45,0}});
+			r = new Region(null, new double[][]{ new double[]{ 45.25, 0.25 }, new double[]{ 45.25, 0.5 }, new double[]{ 45, 0.5 }, new double[]{ 45, 0.25 }, new double[]{ 44.75, 0.25 }, new double[]{ 44.75, 0 }, new double[]{ 45, 0 } });
 			pgo = (PGobject)translator.translateGeometryToDB(r);
 			assertNotNull(pgo);
 			assertEquals("spoly", pgo.getType());
 			assertEquals("{(45.25d,0.25d),(45.25d,0.5d),(45.0d,0.5d),(45.0d,0.25d),(44.75d,0.25d),(44.75d,0.0d),(45.0d,0.0d)}", pgo.getValue());
 
 			// OTHER
-			try{
-				r = new Region(new Region(null, new double[]{45,0}));
+			try {
+				r = new Region(new Region(null, new double[]{ 45, 0 }));
 				translator.translateGeometryToDB(r);
 				fail("The translation of a STC Not region is not supported!");
-			}catch(Throwable ex){
+			} catch(Throwable ex) {
 				assertTrue(ex instanceof ParseException);
 				assertEquals("Unsupported geometrical region: \"" + r.type + "\"!", ex.getMessage());
 			}
 
-		}catch(ParseException t){
+		} catch(ParseException t) {
 			t.printStackTrace();
 			fail(t.getMessage());
 		}
 	}
 
+	@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)");
+		}
+	}
+
 }
-- 
GitLab