From f0b3c43c4108e9551a09b8f1e0975154a7c11898 Mon Sep 17 00:00:00 2001 From: Enrique Ruiz Ruiz Date: Tue, 24 Feb 2026 12:46:11 +0100 Subject: [PATCH 1/4] GAIAPCR-1325 fix float formatting in __cone_search & __query_object --- astroquery/gaia/core.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/astroquery/gaia/core.py b/astroquery/gaia/core.py index d58d46eeb5..c403e11d2b 100644 --- a/astroquery/gaia/core.py +++ b/astroquery/gaia/core.py @@ -518,7 +518,7 @@ def __query_object(self, coordinate, *, radius=None, width=None, height=None, {row_limit} DISTANCE( POINT('ICRS', {ra_column}, {dec_column}), - POINT('ICRS', {ra}, {dec}) + POINT('ICRS', {ra:.14f}, {dec:.14f}) ) as dist, {columns} FROM @@ -528,10 +528,10 @@ def __query_object(self, coordinate, *, radius=None, width=None, height=None, POINT('ICRS', {ra_column}, {dec_column}), BOX( 'ICRS', - {ra}, - {dec}, - {width}, - {height} + {ra:.14f}, + {dec:.14f}, + {width:.14f}, + {height:.14f} ) ) ORDER BY @@ -664,14 +664,14 @@ def __cone_search(self, coordinate, radius, *, table_name=None, {columns}, DISTANCE( POINT('ICRS', {ra_column}, {dec_column}), - POINT('ICRS', {ra}, {dec}) + POINT('ICRS', {ra:.14f}, {dec:.14f}) ) AS dist FROM {table_name} WHERE 1 = CONTAINS( POINT('ICRS', {ra_column}, {dec_column}), - CIRCLE('ICRS', {ra}, {dec}, {radius}) + CIRCLE('ICRS', {ra:.14f}, {dec:.14f}, {radius:.14f}) ) ORDER BY dist ASC From b6a9c95a901720c463f6ecd7e1ab863e4236aee1 Mon Sep 17 00:00:00 2001 From: Enrique Ruiz Ruiz Date: Tue, 24 Feb 2026 16:41:54 +0100 Subject: [PATCH 2/4] GAIAPCR-1325 Added precision tests for query_object and cone_search --- astroquery/gaia/tests/test_gaiatap.py | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/astroquery/gaia/tests/test_gaiatap.py b/astroquery/gaia/tests/test_gaiatap.py index 05aea651ef..439ec41bda 100644 --- a/astroquery/gaia/tests/test_gaiatap.py +++ b/astroquery/gaia/tests/test_gaiatap.py @@ -23,6 +23,7 @@ import astropy.units as u import numpy as np import pytest +import re from astropy.coordinates.sky_coordinate import SkyCoord from astropy.table import Column, Table from astropy.units import Quantity @@ -516,6 +517,110 @@ def test_query_object_async(column_attrs, mock_querier_async, kwargs): for colname, attrs in column_attrs.items(): assert table[colname].attrs_equal(attrs) +def test_query_object_precision_debug(monkeypatch, mock_querier): + captured_query = {} + + # Intercept call to launch_job so we can capture the generated query + def fake_launch_job(self, query, verbose=False): + captured_query["query"] = query + + class FakeJob: + async_ = False + failed = False + def get_phase(self): + return "COMPLETED" + def get_results(self): + return Table({"x": [1]}) + return FakeJob() + + monkeypatch.setattr(GaiaClass, "launch_job", fake_launch_job) + + # Use coordinates that produce predictable float values + coord = SkyCoord(ra=19 * u.deg, dec=20 * u.deg) + width = 12 * u.deg + height = 10 * u.deg + + # Execute the query + mock_querier.query_object(coord, width=width, height=height) + + query = captured_query.get("query") + assert query is not None + + # Regex to detect floats with exactly 14 decimal digits + float14 = r"[0-9]+\.[0-9]{14}" + matches = re.findall(float14, query) + + # ----------------------------- + # Debug: print out everything + # ----------------------------- + print("\n==== DEBUG: Generated query ====") + print(query) + print("==== DEBUG: Detected 14-decimal floats ====") + for m in matches: + print(m) + print("================================\n") + + # At least RA, DEC, width and height should be formatted with 14 decimals + assert len(matches) == 6, ( + f"Expected 6 float values with 14 decimals, " + f"found {len(matches)}. Query:\n{query}" + ) + +def test_cone_search_precision_debug(monkeypatch, mock_querier): + """ + Test that cone_search() builds a query containing RA, DEC and radius + formatted with 14 decimal places. This test also prints the captured + numbers for debugging purposes. + """ + + captured_query = {} + + # Intercept launch_job to capture the generated SQL query + def fake_launch_job(self, query, output_file=None, output_format="votable_gzip", + verbose=False, dump_to_file=False): + captured_query["query"] = query + + class FakeJob: + async_ = False + failed = False + def get_phase(self): + return "COMPLETED" + def get_results(self): + return Table({"x": [1]}) + return FakeJob() + + monkeypatch.setattr(GaiaClass, "launch_job", fake_launch_job) + + # Coordinates chosen to generate predictable floats + coord = SkyCoord(ra=19 * u.deg, dec=20 * u.deg) + radius = 1 * u.deg + + # Run the cone search + job = mock_querier.cone_search(coord, radius=radius) + job.get_results() + + query = captured_query.get("query") + assert query is not None + + # Regex to detect floats with exactly 14 decimal digits + float14 = r"[0-9]+\.[0-9]{14}" + matches = re.findall(float14, query) + + # ----------------------------- + # Debug: output for inspection + # ----------------------------- + print("\n==== DEBUG: Generated cone-search query ====") + print(query) + print("==== DEBUG: Detected 14-decimal floats ====") + for m in matches: + print(m) + print("============================================\n") + + # We expect at least RA, DEC and radius in .14f format + assert len(matches) == 5, ( + f"Expected 5 float values with 14 decimals, " + f"but found {len(matches)}. Query:\n{query}" + ) def test_cone_search_sync(column_attrs, mock_querier): assert mock_querier.USE_NAMES_OVER_IDS is True @@ -1608,3 +1713,4 @@ def test_logout(mock_logout): mock_logout.side_effect = HTTPError("Login error") tap.logout() assert (mock_logout.call_count == 3) + From f1489ea9c35a24f38c8f55508455364a2b394de2 Mon Sep 17 00:00:00 2001 From: Enrique Ruiz Ruiz Date: Wed, 25 Feb 2026 10:29:40 +0100 Subject: [PATCH 3/4] GAIAPCR-1325 Fix PR checks --- CHANGES.rst | 1 + astroquery/gaia/tests/test_gaiatap.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index eabaaef26d..9ede3a8beb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -57,6 +57,7 @@ gaia - New datalink DR4 retrieval type RESIDUAL_IMAGE. [#3489] - The method ``load_data`` parses ecsv files [#3500]. +- Fixed decimal precision for query_object and cone_search to use 14 decimal places [#3539]. esa.hubble ^^^^^^^^^^ diff --git a/astroquery/gaia/tests/test_gaiatap.py b/astroquery/gaia/tests/test_gaiatap.py index 439ec41bda..8825dff025 100644 --- a/astroquery/gaia/tests/test_gaiatap.py +++ b/astroquery/gaia/tests/test_gaiatap.py @@ -517,6 +517,7 @@ def test_query_object_async(column_attrs, mock_querier_async, kwargs): for colname, attrs in column_attrs.items(): assert table[colname].attrs_equal(attrs) + def test_query_object_precision_debug(monkeypatch, mock_querier): captured_query = {} @@ -527,8 +528,10 @@ def fake_launch_job(self, query, verbose=False): class FakeJob: async_ = False failed = False + def get_phase(self): return "COMPLETED" + def get_results(self): return Table({"x": [1]}) return FakeJob() @@ -566,6 +569,7 @@ def get_results(self): f"found {len(matches)}. Query:\n{query}" ) + def test_cone_search_precision_debug(monkeypatch, mock_querier): """ Test that cone_search() builds a query containing RA, DEC and radius @@ -583,8 +587,10 @@ def fake_launch_job(self, query, output_file=None, output_format="votable_gzip", class FakeJob: async_ = False failed = False + def get_phase(self): return "COMPLETED" + def get_results(self): return Table({"x": [1]}) return FakeJob() @@ -622,6 +628,7 @@ def get_results(self): f"but found {len(matches)}. Query:\n{query}" ) + def test_cone_search_sync(column_attrs, mock_querier): assert mock_querier.USE_NAMES_OVER_IDS is True @@ -1713,4 +1720,3 @@ def test_logout(mock_logout): mock_logout.side_effect = HTTPError("Login error") tap.logout() assert (mock_logout.call_count == 3) - From 791084fae262fa2ab1876d71681b6ea121dacae9 Mon Sep 17 00:00:00 2001 From: Enrique Ruiz Ruiz Date: Mon, 2 Mar 2026 12:23:53 +0100 Subject: [PATCH 4/4] GAIAPCR-1325 Applying PR comments and corrections --- astroquery/gaia/core.py | 35 ++++++-- astroquery/gaia/tests/test_gaiatap.py | 111 ++++++-------------------- 2 files changed, 52 insertions(+), 94 deletions(-) diff --git a/astroquery/gaia/core.py b/astroquery/gaia/core.py index c403e11d2b..cc028d8d00 100644 --- a/astroquery/gaia/core.py +++ b/astroquery/gaia/core.py @@ -470,7 +470,7 @@ def get_datalinks(self, ids, *, linking_parameter='SOURCE_ID', verbose=False): return self.__gaiadata.get_datalinks(ids=ids, linking_parameter=final_linking_parameter, verbose=verbose) def __query_object(self, coordinate, *, radius=None, width=None, height=None, - async_job=False, verbose=False, columns=()): + async_job=False, verbose=False, columns=(), get_query_payload=False): """Launches a job TAP & TAP+ @@ -491,6 +491,9 @@ def __query_object(self, coordinate, *, radius=None, width=None, height=None, flag to display information about the process columns: list, optional, default () if empty, all columns will be selected + get_query_payload : bool, optional + If True, return the SQL query string that would be sent to the server, + instead of executing the query. This is useful for debugging and testing. Returns ------- @@ -541,13 +544,18 @@ def __query_object(self, coordinate, *, radius=None, width=None, height=None, 'columns': columns, 'table_name': self.MAIN_GAIA_TABLE or conf.MAIN_GAIA_TABLE, 'ra': ra, 'dec': dec, 'width': widthDeg.value, 'height': heightDeg.value}) + + if get_query_payload: + return query + if async_job: job = self.launch_job_async(query, verbose=verbose) else: job = self.launch_job(query, verbose=verbose) return job.get_results() - def query_object(self, coordinate, *, radius=None, width=None, height=None, verbose=False, columns=()): + def query_object(self, coordinate, *, radius=None, width=None, height=None, verbose=False, columns=(), + get_query_payload=False): """Launches a synchronous cone search for the input search radius or the box on the sky, sorted by angular separation TAP & TAP+ @@ -566,13 +574,16 @@ def query_object(self, coordinate, *, radius=None, width=None, height=None, verb flag to display information about the process columns: list, optional, default () if empty, all columns will be selected + get_query_payload : bool, optional + If True, return the SQL query string that would be sent to the server, + instead of executing the query. This is useful for debugging and testing. Returns ------- The job results (astropy.table). """ return self.__query_object(coordinate, radius=radius, width=width, height=height, async_job=False, - verbose=verbose, columns=columns) + verbose=verbose, columns=columns, get_query_payload=get_query_payload) def query_object_async(self, coordinate, *, radius=None, width=None, height=None, verbose=False, columns=()): """Launches an asynchronous cone search for the input search radius or the box on the sky, sorted by angular @@ -608,7 +619,8 @@ def __cone_search(self, coordinate, radius, *, table_name=None, background=False, output_file=None, output_format="votable_gzip", verbose=False, dump_to_file=False, - columns=()): + columns=(), + get_query_payload=False): """Cone search sorted by distance TAP & TAP+ @@ -641,6 +653,9 @@ def __cone_search(self, coordinate, radius, *, table_name=None, if True, the results are saved in a file instead of using memory columns: list, optional, default () if empty, all columns will be selected + get_query_payload : bool, optional + If True, return the SQL query string that would be sent to the server, + instead of executing it. This is useful for debugging and testing. Returns ------- @@ -681,6 +696,9 @@ def __cone_search(self, coordinate, radius, *, table_name=None, 'radius': radiusDeg, 'table_name': table_name or self.MAIN_GAIA_TABLE or conf.MAIN_GAIA_TABLE}) + if get_query_payload: + return query + if async_job: return self.launch_job_async(query=query, output_file=output_file, output_format=output_format, verbose=verbose, dump_to_file=dump_to_file, background=background) @@ -695,7 +713,8 @@ def cone_search(self, coordinate, *, radius=None, output_file=None, output_format="votable_gzip", verbose=False, dump_to_file=False, - columns=()): + columns=(), + get_query_payload=False): """Cone search sorted by distance (sync.) TAP & TAP+ @@ -722,6 +741,9 @@ def cone_search(self, coordinate, *, radius=None, if True, the results are saved in a file instead of using memory columns: list, optional, default () if empty, all columns will be selected + get_query_payload : bool, optional + If True, return the SQL query string that would be sent to the server, + instead of executing the query. This is useful for debugging and testing. Returns ------- @@ -737,7 +759,8 @@ def cone_search(self, coordinate, *, radius=None, output_file=output_file, output_format=output_format, verbose=verbose, - dump_to_file=dump_to_file, columns=columns) + dump_to_file=dump_to_file, columns=columns, + get_query_payload=get_query_payload) def cone_search_async(self, coordinate, *, radius=None, table_name=None, diff --git a/astroquery/gaia/tests/test_gaiatap.py b/astroquery/gaia/tests/test_gaiatap.py index 8825dff025..2fc5b48d72 100644 --- a/astroquery/gaia/tests/test_gaiatap.py +++ b/astroquery/gaia/tests/test_gaiatap.py @@ -518,114 +518,49 @@ def test_query_object_async(column_attrs, mock_querier_async, kwargs): assert table[colname].attrs_equal(attrs) -def test_query_object_precision_debug(monkeypatch, mock_querier): - captured_query = {} - - # Intercept call to launch_job so we can capture the generated query - def fake_launch_job(self, query, verbose=False): - captured_query["query"] = query - - class FakeJob: - async_ = False - failed = False - - def get_phase(self): - return "COMPLETED" - - def get_results(self): - return Table({"x": [1]}) - return FakeJob() - - monkeypatch.setattr(GaiaClass, "launch_job", fake_launch_job) - - # Use coordinates that produce predictable float values +def test_query_object_precision(mock_querier): + """ + Verifies that query_object() produces a query where RA, DEC, width and height + are formatted with exactly 14 decimal places when using get_query_payload=True. + """ coord = SkyCoord(ra=19 * u.deg, dec=20 * u.deg) width = 12 * u.deg height = 10 * u.deg - # Execute the query - mock_querier.query_object(coord, width=width, height=height) - - query = captured_query.get("query") - assert query is not None + query = mock_querier.query_object( + coord, + width=width, + height=height, + get_query_payload=True + ) - # Regex to detect floats with exactly 14 decimal digits float14 = r"[0-9]+\.[0-9]{14}" matches = re.findall(float14, query) - # ----------------------------- - # Debug: print out everything - # ----------------------------- - print("\n==== DEBUG: Generated query ====") - print(query) - print("==== DEBUG: Detected 14-decimal floats ====") - for m in matches: - print(m) - print("================================\n") - - # At least RA, DEC, width and height should be formatted with 14 decimals assert len(matches) == 6, ( - f"Expected 6 float values with 14 decimals, " - f"found {len(matches)}. Query:\n{query}" + f"Expected 6 float values with 14 decimals, found {len(matches)}.\n{query}" ) -def test_cone_search_precision_debug(monkeypatch, mock_querier): +def test_cone_search_precision(mock_querier): """ - Test that cone_search() builds a query containing RA, DEC and radius - formatted with 14 decimal places. This test also prints the captured - numbers for debugging purposes. + Verifies that cone_search() produces a query where RA, DEC and radius + appear formatted with exactly 14 decimal places when using get_query_payload=True. """ + coord = SkyCoord(ra=19*u.deg, dec=20*u.deg) + radius = 1*u.deg - captured_query = {} - - # Intercept launch_job to capture the generated SQL query - def fake_launch_job(self, query, output_file=None, output_format="votable_gzip", - verbose=False, dump_to_file=False): - captured_query["query"] = query - - class FakeJob: - async_ = False - failed = False - - def get_phase(self): - return "COMPLETED" - - def get_results(self): - return Table({"x": [1]}) - return FakeJob() - - monkeypatch.setattr(GaiaClass, "launch_job", fake_launch_job) - - # Coordinates chosen to generate predictable floats - coord = SkyCoord(ra=19 * u.deg, dec=20 * u.deg) - radius = 1 * u.deg - - # Run the cone search - job = mock_querier.cone_search(coord, radius=radius) - job.get_results() - - query = captured_query.get("query") - assert query is not None + query = mock_querier.cone_search( + coord, + radius=radius, + get_query_payload=True, + ) - # Regex to detect floats with exactly 14 decimal digits float14 = r"[0-9]+\.[0-9]{14}" matches = re.findall(float14, query) - # ----------------------------- - # Debug: output for inspection - # ----------------------------- - print("\n==== DEBUG: Generated cone-search query ====") - print(query) - print("==== DEBUG: Detected 14-decimal floats ====") - for m in matches: - print(m) - print("============================================\n") - - # We expect at least RA, DEC and radius in .14f format assert len(matches) == 5, ( - f"Expected 5 float values with 14 decimals, " - f"but found {len(matches)}. Query:\n{query}" + f"Expected 5 float values with 14 decimals, found {len(matches)}.\n{query}" )