astro query:docs

- Index
- Modules

### Navigation

- astroquery v0.4.11 »
- Module code »
- astroquery.simbad »

# Source code for astroquery.simbad.core

    # Licensed under a 3-clause BSD style license - see LICENSE.rst
    """SIMBAD query class for accessing the SIMBAD Service"""

    import copy
    from dataclasses import dataclass, field
    from difflib import get_close_matches
    from functools import lru_cache
    import gc
    import re
    from typing import Any
    import warnings

    import astropy.coordinates as coord
    from astropy.table import Table, Column, vstack
    import astropy.units as u
    from astropy.utils import deprecated
    from astropy.utils.decorators import deprecated_renamed_argument
    import numpy as np

    from astroquery.query import BaseVOQuery
    from astroquery.utils import commons
    from astroquery.exceptions import NoResultsWarning
    from astroquery.simbad.utils import (_catch_deprecated_fields_with_arguments,
                                         _wildcard_to_regexp, CriteriaTranslator,
                                         query_criteria_fields)

    from pyvo.dal import TAPService, TAPQuery
    from . import conf

    __all__ = ['Simbad', 'SimbadClass']

    def _adql_parameter(entry: str):
        """Replace single quotes by two single quotes.

        This should be applied to parameters used in ADQL queries.
        It is not a SQL injection protection: it just allows to search, for example,
        for authors with quotes in their names or titles/descriptions with apostrophes.

        Parameters
        ----------
        entry : str

        Returns
        -------
        str
        """
        return entry.replace("'", "''")

    @lru_cache(256)
    def _cached_query_tap(tap, query: str, *, maxrec=10000, async_job=False, timeout=None):
        """Cache version of query TAP.

        This private function is called when query_tap is executed without an
        ``uploads`` extra keyword argument. This is a work around because
        `~astropy.table.Table` objects are not hashable and thus cannot
        be used as arguments for a function decorated with lru_cache.

        Parameters
        ----------
        tap : `~pyvo.dal.TAPService`
            The TAP service to query SIMBAD.
        query : str
            A string containing the query written in the
            Astronomical Data Query Language (ADQL).
        maxrec : int, optional
            The number of records to be returned. Its maximum value is 2000000.
        async_job: bool, optional
            When set to `True`, the query will be executed in asynchronous mode. This is
            better for very long queries, as it prevents transient failures to abort the
            query execution.
            Defaults to `False`.
        timeout: int, optional
            The execution duration for the asynchronous query. If 'async_job' is true, then
            this has to be provided.

        Returns
        -------
        `~astropy.table.Table`
            The response returned by SIMBAD.
        """
        if async_job:
            return tap.run_async(query, maxrec=maxrec, execution_duration=timeout).to_table()
        return tap.search(query, maxrec=maxrec).to_table()

    @dataclass(frozen=True)
    class _Column:
        """A class to define a column in a SIMBAD query."""
        table: str
        name: str
        alias: str = field(default=None)

    @dataclass(frozen=True)
    class _Join:
        """A class to define a join between two tables."""
        table: str
        column_left: Any
        column_right: Any
        join_type: str = field(default="JOIN")
        alias: str = field(default=None)

    class SimbadClass(BaseVOQuery):
        """The class for querying the SIMBAD web service.

        Note that SIMBAD suggests submitting no more than 6 queries per second; if
        you submit more than that, your IP may be temporarily blacklisted
        (https://simbad.cds.unistra.fr/guide/sim-url.htx)
        """
        SIMBAD_URL = 'https://' + conf.server + '/simbad/sim-script'

        def __init__(self, ROW_LIMIT=None, *, timeout=None):
            super().__init__()
            # to create the TAPService
            self._server = conf.server
            self._tap = None
            self._hardlimit = None
            self._uploadlimit = None
            # attributes to construct ADQL queries
            self._columns_in_output = None  # a list of _Column
            self.joins = []  # a list of _Join
            self.criteria = []  # a list of strings
            self.ROW_LIMIT = ROW_LIMIT
            self.timeout = timeout

        @property
        def ROW_LIMIT(self):
            return self._ROW_LIMIT

        @ROW_LIMIT.setter
        def ROW_LIMIT(self, ROW_LIMIT):
            if ROW_LIMIT is None:
                self._ROW_LIMIT = conf.row_limit
            elif isinstance(ROW_LIMIT, int) and ROW_LIMIT >= -1:
                self._ROW_LIMIT = ROW_LIMIT
            else:
                raise ValueError("ROW_LIMIT can be either -1 to set the limit to SIMBAD's "
                                 "maximum capability, 0 to retrieve an empty table, "
                                 "or a positive integer.")

        @property
        def timeout(self):
            """The execution time for asynchronous queries.

            Returns
            -------
            int
                The execution time before the query times out, in seconds.
            """
            return self._timeout

        @timeout.setter
        def timeout(self, timeout):
            if timeout is None:
                self._timeout = conf.timeout
            elif timeout <= self.tap.capabilities[0].executionduration.hard:
                self._timeout = timeout
            else:
                raise ValueError(
                    "'timeout' cannot exceed the maximum time duration set by this mirror: "
                    f"{self.tap.capabilities[0].executionduration.hard} seconds.")

        @property
        def server(self):
            """The SIMBAD mirror to use."""
            return self._server

        @server.setter
        def server(self, server: str):
            """Allows to switch server between SIMBAD mirrors.

            Parameters
            ----------
            server : str
                It should be one of `~astroquery.simbad.conf.servers_list`.
            """
            if server in conf.servers_list:
                self._server = server
            else:
                raise ValueError(f"'{server}' does not correspond to a SIMBAD server, "
                                 f"the two existing ones are {conf.servers_list}.")

        @property
        def tap(self):
            """A `~pyvo.dal.TAPService` service for SIMBAD."""
            tap_url = f"https://{self.server}/simbad/sim-tap"
            # only creates a new tap instance if there are no existing one
            # or if the server property changed since the last getter call.
            if (not self._tap) or (self._tap.baseurl != tap_url):
                self._tap = TAPService(baseurl=tap_url, session=self._session)
            return self._tap

        @property
        def hardlimit(self):
            """The maximum number of lines for SIMBAD's output."""
            if self._hardlimit is None:
                self._hardlimit = self.tap.hardlimit
            return self._hardlimit

        @property
        def uploadlimit(self):
            if self._uploadlimit is None:
                self._uploadlimit = self.tap.get_tap_capability().uploadlimit.hard.content
            return self._uploadlimit

        @property
        def columns_in_output(self):
            """A list of _Column.

            They will be included in the output of the following methods:

            - `query_object`,
            - `query_objects`,
            - `query_region`,
            - `query_catalog`,
            - `query_hierarchy`,
            - `query_bibobj`,
            - `query_criteria`.

            """
            if self._columns_in_output is None:
                self._columns_in_output = [_Column("basic", item)
                                           for item in conf.default_columns]
            return self._columns_in_output

        @columns_in_output.setter
        def columns_in_output(self, list_columns):
            self._columns_in_output = list_columns

        @staticmethod
        def list_wildcards():
            """
            Displays the available wildcards that may be used in SIMBAD queries and
            their usage.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> Simbad.list_wildcards()
            *: Any string of characters (including an empty one)
            ?: Any character (exactly one character)
            [abc]: Exactly one character taken in the list. Can also be defined by a range of characters: [A-Z]
            [^0-9]: Any (one) character not in the list.
            """
            WILDCARDS = {'*': 'Any string of characters (including an empty one)',
                         '?': 'Any character (exactly one character)',
                         '[abc]': ('Exactly one character taken in the list. '
                                   'Can also be defined by a range of characters: [A-Z]'),
                         '[^0-9]': 'Any (one) character not in the list.'}
            print("\n".join(f"{k}: {v}" for k, v in WILDCARDS.items()))

        # ---------------------------------
        # Methods to define SIMBAD's output
        # ---------------------------------

        def list_votable_fields(self):
            """List all options to add columns to SIMBAD's output.

            They are of four types:

            - "column of basic": a column of the basic table. There fields can also be explored with
              `~astroquery.simbad.SimbadClass.list_columns`.
            - "table": a table other than basic that has a declared direct join
            - "bundle of basic columns": a pre-selected bundle of columns of basic. Ex: "parallax" will add all
              columns relevant to parallax
            - "filter name": an optical filter name

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> options = Simbad.list_votable_fields() # doctest: +REMOTE_DATA
            >>> # to print only the available bundles of columns
            >>> options[options["type"] == "bundle of basic columns"][["name", "description"]] # doctest: +REMOTE_DATA
            <Table length=8>
                 name                         description
                object                           object
            ------------- ----------------------------------------------------
              coordinates                  all fields related with coordinates
                      dim          major and minor axis, angle and inclination
               dimensions              all fields related to object dimensions
                morphtype         all fields related to the morphological type
                 parallax                     all fields related to parallaxes
            propermotions           all fields related with the proper motions
                       sp            all fields related with the spectral type
                 velocity all fields related with radial velocity and redshift
            """
            # get the tables with a simple link to basic
            query_tables = """SELECT DISTINCT table_name AS name, tables.description
            FROM TAP_SCHEMA.keys JOIN  TAP_SCHEMA.key_columns USING (key_id)
            JOIN TAP_SCHEMA.tables ON TAP_SCHEMA.keys.from_table = TAP_SCHEMA.tables.table_name
            OR TAP_SCHEMA.keys.target_table = TAP_SCHEMA.tables.table_name
            WHERE TAP_SCHEMA.tables.schema_name = 'public'
            AND (from_table = 'basic' OR target_table = 'basic')
            AND from_table != 'h_link'
            """
            tables = self.query_tap(query_tables)
            tables["type"] = Column(["table"] * len(tables), dtype="object")
            # the columns of basic are also valid options
            basic_columns = self.list_columns("basic")[["column_name", "description"]]
            basic_columns["column_name"].info.name = "name"
            basic_columns["type"] = Column(["column of basic"] * len(basic_columns), dtype="object")
            # get the bundles of columns from file
            bundle_entries = {key: value for key, value in query_criteria_fields.items()
                              if value["type"] == "bundle"}
            bundles = Table({"name": list(bundle_entries.keys()),
                             "description": [value["description"] for _, value in bundle_entries.items()],
                             "type": ["bundle of basic columns"] * len(bundle_entries)},
                            dtype=["object", "object", "object"])
            # get the filter names
            filters = self.query_tap("SELECT filtername AS name, description FROM filter")
            filters["type"] = Column(["filter name"] * len(filters), dtype="object")
            # vstack the four types of options
            return vstack([tables, basic_columns, bundles, filters], metadata_conflicts="silent")

        def _get_bundle_columns(self, bundle_name):
            """Get the list of columns in the preselected bundles.

            Parameters
            ----------
            bundle_name : str
                The possible values can be listed with `~astroquery.simbad.SimbadClass.list_votable_fields`

            Returns
            -------
            list[simbad._Column]
                The list of columns corresponding to the selected bundle.
            """
            basic_columns = set(map(str.casefold, set(self.list_columns("basic")["column_name"])))

            bundle_entries = {key: value for key, value in query_criteria_fields.items()
                              if value["type"] == "bundle"}

            if bundle_name in bundle_entries:
                bundle = bundle_entries[bundle_name]
                columns = [_Column("basic", column) for column in basic_columns
                           if column.startswith(bundle["tap_startswith"])]
                if "tap_column" in bundle:
                    columns = [_Column("basic", column) for column in bundle["tap_column"]] + columns
            return columns

        def _add_table_to_output(self, table):
            """Add all fields of a 'table' to the output of queries.

            This handles the join from the table and the naming of the columns.
            It only takes in account tables with an explicit link to basic. Other
            cases should be added manually in an ADQL query string.

            Parameters
            ----------
            table : str
                name of the table to add
            """
            table = table.casefold()

            if table == "basic":
                self.columns_in_output.append(_Column(table, "*"))
                return

            linked_to_basic = self.list_linked_tables("basic")
            # list of accepted tables
            linked_to_basic["from_table"] = [table.casefold() for table in linked_to_basic["from_table"]]
            # the actual link to construct the join
            link = linked_to_basic[linked_to_basic["from_table"] == table][0]

            if table not in linked_to_basic["from_table"] or table == "h_link":
                raise ValueError(f"'{table}' has no explicit link to 'basic'. These cases require a custom ADQL "
                                 "query to be written and called with 'SimbadClass.query_tap'.")

            columns = list(self.list_columns(table)["column_name"])

            # allfluxes is the only case-dependent table
            if table == "allfluxes":
                columns = [column for column in columns if column not in {"oidref", "oidbibref"}]
                alias = [column.replace("_", "") if "_" in column else None
                         for column in columns]
            else:
                columns = [column.casefold() for column in columns if column not in {"oidref", "oidbibref"}]
                # the alias is mandatory to be able to distinguish between duplicates like
                # mesDistance.bibcode and mesDiameter.bibcode.
                alias = [f"{table}.{column}" if not column.startswith(table) else None for column in columns]

            # modify the attributes here
            self.columns_in_output += [_Column(table, column, alias)
                                       for column, alias in zip(columns, alias)]
            self.joins += [_Join(table, _Column("basic", link["target_column"]),
                                 _Column(table, link["from_column"]), "LEFT JOIN")]

        def add_votable_fields(self, *args):
            """Add columns to the output of a SIMBAD query.

            The list of possible arguments and their description for this method
            can be printed with `~astroquery.simbad.SimbadClass.list_votable_fields`.

            The methods affected by this:

            - `query_object`,
            - `query_objects`,
            - `query_region`,
            - `query_catalog`,
            - `query_hierarchy`,
            - `query_bibobj`,
            - `query_criteria`.

            Parameters
            ----------
            args : str
                The arguments to be added.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> simbad = Simbad()
            >>> simbad.add_votable_fields('sp_type', 'sp_qual', 'sp_bibcode') # doctest: +REMOTE_DATA
            >>> simbad.get_votable_fields() # doctest: +REMOTE_DATA
            ['basic.main_id', 'basic.ra', 'basic.dec', 'basic.coo_err_maj', 'basic.coo_err_min', ...
            """

            # the legacy way of adding fluxes
            args = set(args)
            fluxes_to_add = []
            args_copy = args.copy()
            for arg in args_copy:
                if arg.startswith("flux_"):
                    raise ValueError("The votable fields 'flux_***(filtername)' are removed and replaced "
                                     "by 'flux' that will add all information for every filters. "
                                     "See section on filters in "
                                     "https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html"
                                     " to see the new ways to interact with SIMBAD's fluxes.")
                if re.match(r"^flux.*\(.+\)$", arg):
                    filter_name = re.findall(r"\((\w+)\)", arg)[0]
                    warnings.warn(f"The notation 'flux({filter_name})' is deprecated since 0.4.8 in favor of "
                                  f"'{filter_name}'. You will see the column appearing with its new name "
                                  "in the output. See section on filters in "
                                  "https://astroquery.readthedocs.io/en/latest/simbad/simbad_evolution.html "
                                  "to see the new ways to interact with SIMBAD's fluxes.", DeprecationWarning, stacklevel=2)
                    fluxes_to_add.append(filter_name)
                    args.remove(arg)

            # output options
            output_options = self.list_votable_fields()
            # fluxes are case-dependant
            fluxes = set(output_options[output_options["type"] == "filter name"]["name"])
            # add fluxes
            fluxes_from_names = set(flux for flux in args if flux in fluxes)
            fluxes_to_add += fluxes_from_names
            if fluxes_to_add:
                self.joins.append(_Join("allfluxes", _Column("basic", "oid"),
                                  _Column("allfluxes", "oidref")))
                for flux in fluxes_to_add:
                    if len(flux) == 1 and flux.islower():
                        # the name in the allfluxes view has a trailing underscore. This is not
                        # the case in the list of filter names, so we homogenize with an alias
                        self.columns_in_output.append(_Column("allfluxes", flux + "_", flux))
                    else:
                        self.columns_in_output.append(_Column("allfluxes", flux))
            # remove the arguments already added
            args -= fluxes_from_names
            # remove filters from output options
            output_options = output_options[output_options["type"] != "filter name"]

            # casefold args because we allow case difference for every other argument (legacy behavior)
            args = set(map(str.casefold, args))
            output_options["name"] = list(map(str.casefold, list(output_options["name"])))
            basic_columns = output_options[output_options["type"] == "column of basic"]["name"]
            all_tables = output_options[output_options["type"] == "table"]["name"]
            bundles = output_options[output_options["type"] == "bundle of basic columns"]["name"]

            # Add columns from basic
            self.columns_in_output += [_Column("basic", column) for column in args if column in basic_columns]

            # Add tables
            tables_to_add = [table for table in args if table in all_tables]
            for table in tables_to_add:
                self._add_table_to_output(table)

            # Add bundles
            bundles_to_add = [bundle for bundle in args if bundle in bundles]
            for bundle in bundles_to_add:
                self.columns_in_output += self._get_bundle_columns(bundle)

            if args.issubset(set(output_options["name"])):
                return

            # The other possible values are from the deprecated votable fields
            remaining_arguments = args - set(output_options["name"])
            for votable_field in remaining_arguments:
                if votable_field in query_criteria_fields:
                    field_data = query_criteria_fields[votable_field]
                    field_type = field_data["type"]
                    # some columns are still there but under a new name
                    if field_type == "alias":
                        tap_column = field_data["tap_column"]
                        self.columns_in_output.append(_Column("basic", tap_column))
                        warning_message = (f"'{votable_field}' has been renamed '{tap_column}'. You'll see it "
                                           "appearing with its new name in the output table")
                        warnings.warn(warning_message, DeprecationWarning, stacklevel=2)
                    # some tables are still there but under a new name
                    elif field_type == "alias table":
                        # add all columns of the desired measurement table
                        tap_table = field_data["tap_table"]
                        self._add_table_to_output(tap_table)
                        warning_message = (f"'{votable_field}' has been renamed '{tap_table}'. You will see "
                                           "this new name in the result.")
                        warnings.warn(warning_message, DeprecationWarning, stacklevel=2)
                    # some tables have been moved to VizieR. This is broken since years
                    # but they were still appearing in list_votable_fields.
                    elif field_type == "historical measurement":
                        raise ValueError(f"'{votable_field}' is no longer a part of SIMBAD. It was moved "
                                         "into a separate VizieR catalog. It is possible to query "
                                         "it with the `astroquery.vizier` module.")
                else:
                    # raise a ValueError on fields with arguments
                    _catch_deprecated_fields_with_arguments(votable_field)
                    # or a typo
                    close_match = get_close_matches(votable_field, set(output_options["name"]))
                    error_message = (f"'{votable_field}' is not one of the accepted options "
                                     "which can be listed with 'list_votable_fields'.")
                    if close_match != []:
                        close_matches = "' or '".join(close_match)
                        error_message += f" Did you mean '{close_matches}'?"
                    raise ValueError(error_message)

        def get_votable_fields(self):
            """Display votable fields."""
            return [f"{column.table}.{column.name}" for column in self.columns_in_output]

        def reset_votable_fields(self):
            """Reset the output of the query_*** methods to default.

            They will be included in the output of the following methods:

            - `query_object`,
            - `query_objects`,
            - `query_region`,
            - `query_catalog`,
            - `query_hierarchy`,
            - `query_bibobj`,
            - `query_criteria`.

            """
            self.columns_in_output = [_Column("basic", item)
                                      for item in conf.default_columns]
            self.joins = []
            self.criteria = []

        def get_field_description(self, field_name):
            """Displays a description of the VOTable field.

            This can be replaced by the output of `~astroquery.simbad.SimbadClass.list_votable_fields`.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> options = Simbad.list_votable_fields() # doctest: +REMOTE_DATA
            >>> description_dimensions = options[options["name"] == "dimensions"]["description"] # doctest: +REMOTE_DATA
            >>> description_dimensions.data.data[0] # doctest: +REMOTE_DATA
            'all fields related to object dimensions'

            """
            options = self.list_votable_fields()
            description = options[options["name"] == field_name]["description"]
            return description.data.data[0]

        # -------------
        # Query methods
        # -------------

        @deprecated_renamed_argument(["verbose"], new_name=[None],
                                     since=['0.4.8'], relax=True)
        def query_object(self, object_name, *, wildcard=False, criteria=None,
                         get_query_payload=False, async_job=False, verbose=False):
            """Query SIMBAD for the given object.

            Object names may also be specified with wildcards. See examples below.

            Parameters
            ----------
            object_name : str
                name of object to be queried.
            wildcard : boolean, optional
                When it is set to `True` it implies that the object is specified
                with wildcards. This parameter will render the query case-sensitive.
                Defaults to `False`.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string. See example.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                The result of the query to SIMBAD.

            Examples
            --------

            Get the dimensions of a specific galaxy

            >>> from astroquery.simbad import Simbad
            >>> simbad = Simbad()
            >>> simbad.add_votable_fields("dim") # doctest: +REMOTE_DATA
            >>> result = simbad.query_object("m101") # doctest: +REMOTE_DATA
            >>> result["main_id", "ra", "dec", "galdim_majaxis", "galdim_minaxis", "galdim_bibcode"] # doctest: +REMOTE_DATA
            <Table length=1>
            main_id         ra           dec    ... galdim_minaxis    galdim_bibcode
                           deg           deg    ...     arcmin
             object      float64       float64  ...    float32            object
            ------- ------------------ -------- ... -------------- -------------------
              M 101 210.80242916666668 54.34875 ...          20.89 2003A&A...412...45P

            Get 5 NGC objects that are clusters of stars

            >>> from astroquery.simbad import Simbad
            >>> simbad = Simbad()
            >>> simbad.ROW_LIMIT = 5
            >>> ngc_clusters = simbad.query_object("NGC*", wildcard=True, criteria="otype='Cl*..'") # doctest: +REMOTE_DATA
            >>> ngc_clusters  # doctest: +REMOTE_DATA +IGNORE_OUTPUT
            <Table length=5>
             main_id          ra        ...     coo_bibcode     matched_id
                             deg        ...
              object       float64      ...        object         object
            --------- ----------------- ... ------------------- ----------
            NGC  2024 85.42916666666667 ... 2003A&A...397..177B  NGC  2024
            NGC  1826 76.39174999999999 ... 2011AJ....142...48W  NGC  1826
            NGC  2121 87.05495833333332 ... 2011AJ....142...48W  NGC  2121
            NGC  2019 82.98533333333333 ... 1999AcA....49..521P  NGC  2019
            NGC  1777             73.95 ... 2008MNRAS.389..678B  NGC  1777
            """
            top, columns, joins, instance_criteria = self._get_query_parameters()

            columns.append(_Column("ident", "id", "matched_id"))

            joins.append(_Join("ident", _Column("basic", "oid"), _Column("ident", "oidref")))

            if wildcard:
                instance_criteria.append(rf" regexp(id, '{_wildcard_to_regexp(object_name)}') = 1")
            else:
                instance_criteria.append(rf"id = '{_adql_parameter(object_name)}'")

            if criteria:
                instance_criteria.append(f"({criteria})")

            return self._query(top, columns, joins, instance_criteria,
                               get_query_payload=get_query_payload, async_job=async_job)

        @deprecated_renamed_argument(["verbose", "cache"], new_name=[None, None],
                                     since=['0.4.8', '0.4.8'], relax=True)
        def query_objects(self, object_names, *, wildcard=False, criteria=None,
                          get_query_payload=False, async_job=False, verbose=False,
                          cache=False):
            """Query SIMBAD for the specified list of objects.

            Object names may be specified with wildcards.
            If one of the ``object_names`` is not found in SIMBAD, the corresponding line is
            returned empty in the output (see ``Giga Cluster`` in the example).
            In the output, the column ``user_specified_id`` is the input object name.

            Parameters
            ----------
            object_names : sequence of strs
                names of objects to be queried
            wildcard : boolean, optional
                When `True`, the names may have wildcards in them. Defaults to
                `False`.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string. See example.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                The result of the query to SIMBAD.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> clusters = Simbad.query_objects(["Boss Great Wall", "Great Attractor",
            ...                                  "Giga Cluster", "Coma Supercluster"]) # doctest: +REMOTE_DATA
            >>> clusters[["main_id", "ra", "dec", "user_specified_id"]] # doctest: +REMOTE_DATA
            <Table length=4>
                   main_id            ra     dec   user_specified_id
                                     deg     deg
                    object         float64 float64       object
            ---------------------- ------- ------- -----------------
              NAME Boss Great Wall   163.0    52.0   Boss Great Wall
              NAME Great Attractor   158.0   -46.0   Great Attractor
                                        --      --      Giga Cluster
            NAME Coma Supercluster  170.75    23.9 Coma Supercluster
            """
            top, columns, joins, instance_criteria = self._get_query_parameters()

            if criteria:
                instance_criteria.append(f"({criteria})")

            if wildcard:
                columns.append(_Column("ident", "id", "matched_id"))
                joins += [_Join("ident", _Column("basic", "oid"), _Column("ident", "oidref"))]
                list_criteria = [f"regexp(id, '{_wildcard_to_regexp(object_name)}') = 1"
                                 for object_name in object_names]
                instance_criteria += [f'({" OR ".join(list_criteria)})']

                return self._query(top, columns, joins, instance_criteria,
                                   get_query_payload=get_query_payload, async_job=async_job)

            # There is a faster way to do the query if there is no wildcard: the first table
            # can be the uploaded one and we use a LEFT JOIN for the other ones.
            upload = Table({"user_specified_id": object_names,
                            "object_number_id": list(range(1, len(object_names) + 1))})
            upload_name = "TAP_UPLOAD.script_infos"
            columns.append(_Column(upload_name, "*"))

            # join on ident needs an alias in case the users want to add the votable field ident
            left_joins = [_Join("ident", _Column(upload_name, "user_specified_id"),
                                _Column("ident", "id"), "LEFT JOIN", "ident_upload"),
                          _Join("basic", _Column("basic", "oid"),
                                _Column("ident_upload", "oidref"), "LEFT JOIN")]
            for join in joins:
                left_joins.append(_Join(join.table, join.column_left,
                                        join.column_right, "LEFT JOIN"))
            return self._query(top, columns, left_joins, instance_criteria,
                               from_table=upload_name,
                               get_query_payload=get_query_payload,
                               script_infos=upload)

        @deprecated_renamed_argument(["equinox", "epoch", "cache"],
                                     new_name=[None]*3,
                                     since=['0.4.8']*3, relax=True)
        def query_region(self, coordinates, radius=2*u.arcmin, *,
                         criteria=None, get_query_payload=False, async_job=False,
                         equinox=None, epoch=None, cache=None):
            """Query SIMBAD in a cone around the specified coordinates.

            Parameters
            ----------
            coordinates : str or `astropy.coordinates` object
                The identifier or coordinates around which to query.
            radius : str or `~astropy.units.Quantity`
                the radius of the region.  Defaults to 2 arcmin.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                The result of the query to SIMBAD.

            Examples
            --------

            Look for largest galaxies in two cones

            >>> from astroquery.simbad import Simbad
            >>> from astropy.coordinates import SkyCoord
            >>> simbad = Simbad()
            >>> simbad.ROW_LIMIT = 5
            >>> simbad.add_votable_fields("otype", "dim") # doctest: +REMOTE_DATA
            >>> coordinates = SkyCoord([SkyCoord(186.6, 12.7, unit=("deg", "deg")),
            ...                         SkyCoord(170.75, 23.9, unit=("deg", "deg"))])
            >>> result = simbad.query_region(coordinates, radius="2d5m",
            ...                              criteria="otype = 'Galaxy..' AND galdim_majaxis>8.5") # doctest: +REMOTE_DATA
            >>> result.sort("galdim_majaxis", reverse=True) # doctest: +REMOTE_DATA
            >>> result["main_id", "otype", "galdim_majaxis"] # doctest: +REMOTE_DATA
            <Table length=5>
              main_id    otype  galdim_majaxis
                                    arcmin
               object    object    float32
            ------------ ------ --------------
            LEDA   41362    GiC           11.0
                   M  86    GiG          10.47
            LEDA   40917    AG?           10.3
                   M  87    AGN           9.12
               NGC  4438    LIN           8.91

            Notes
            -----
            It is very inefficient to call this within a loop. Creating an `~astropy.coordinates.SkyCoord`
            object with a list of coordinates will be way faster.

            """
            if radius is None:
                # this message is specifically for deprecated use of 'None' to mean 'Default'
                raise TypeError("The cone radius must be specified as an angle-equivalent quantity")

            center = commons.parse_coordinates(coordinates)
            center = center.transform_to("icrs")

            top, columns, joins, instance_criteria = self._get_query_parameters()
            if criteria:
                instance_criteria.append(f"({criteria})")

            if center.isscalar:
                radius = coord.Angle(radius)
                instance_criteria.append(f"CONTAINS(POINT('ICRS', basic.ra, basic.dec), "
                                         f"CIRCLE('ICRS', {center.ra.deg}, {center.dec.deg}, "
                                         f"{radius.to(u.deg).value})) = 1")
                return self._query(top, columns, joins, instance_criteria,
                                   get_query_payload=get_query_payload)

            # from uploadLimit in SIMBAD's capabilities
            # http://simbad.cds.unistra.fr/simbad/sim-tap/capabilities
            if len(center) > self.uploadlimit:
                raise ValueError(f"'query_region' can process up to {self.uploadlimit} "
                                 "centers. For larger queries, split your centers list.")

            # `radius` as `str` is iterable, but contains only one value.
            if np.iterable(radius) and not isinstance(radius, str):
                if len(radius) != len(center):
                    raise ValueError(f"Mismatch between radii of length {len(radius)}"
                                     f" and center coordinates of length {len(center)}.")
                radius = [coord.Angle(item).to(u.deg).value for item in radius]
            else:
                radius = [coord.Angle(radius).to(u.deg).value] * len(center)

            # for small number of centers, not using the upload method is faster
            if len(center) <= 300:
                cone_criteria = [(f"CONTAINS(POINT('ICRS', basic.ra, basic.dec), CIRCLE('ICRS', "
                                 f"{center.ra.deg}, {center.dec.deg}, {radius})) = 1 ")
                                 for center, radius in zip(center, radius)]

                cone_criteria = f" ({'OR '.join(cone_criteria)})"
                instance_criteria.append(cone_criteria)

                return self._query(top, columns, joins, instance_criteria,
                                   get_query_payload=get_query_payload)

            # for longer centers list, we use a TAP upload
            upload_centers = Table({"ra": center.ra.deg, "dec": center.dec.deg,
                                    "radius": radius})
            sub_query = "(SELECT ra, dec, radius FROM TAP_UPLOAD.centers) AS centers"
            instance_criteria.append("CONTAINS(POINT('ICRS', basic.ra, basic.dec), CIRCLE"
                                     "('ICRS', centers.ra, centers.dec, centers.radius)) = 1 ")

            return self._query(top, columns, joins, instance_criteria,
                               from_table=f"{sub_query}, basic", async_job=async_job,
                               get_query_payload=get_query_payload, centers=upload_centers)

        @deprecated_renamed_argument(["verbose", "cache"], new_name=[None, None],
                                     since=['0.4.8', '0.4.8'], relax=True)
        def query_catalog(self, catalog, *, criteria=None, get_query_payload=False,
                          async_job=False, verbose=False, cache=True):
            """Query a whole catalog.

            Parameters
            ----------
            catalog : str
                The name of the catalog. This is case-dependant.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string. See example.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                The result of the query to SIMBAD.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> simbad = Simbad()
            >>> simbad.ROW_LIMIT = 5
            >>> simbad.query_catalog("GSC", criteria="pmra > 50 and pmra < 100") # doctest: +REMOTE_DATA
            <Table length=5>
                main_id            ra       ...     coo_bibcode        catalog_id
                                  deg       ...
                 object         float64     ...        object            object
            --------------- --------------- ... ------------------- ---------------
                  HD  26053  61.84326890626 ... 2020yCat.1350....0G GSC 04725-00973
            TYC 8454-1081-1 345.11163189562 ... 2020yCat.1350....0G GSC 08454-01081
                  HD  10158  24.86286094434 ... 2020yCat.1350....0G GSC 00624-00340
                CD-22  1862  73.17988827324 ... 2020yCat.1350....0G GSC 05911-00222
                BD+02  4434 327.90220788982 ... 2020yCat.1350....0G GSC 00548-00194

            Notes
            -----
            Catalogs can be very large. Configuring ``SimbadClass.ROW_LIMIT`` allows to
            restrict the output.

            """
            top, columns, joins, instance_criteria = self._get_query_parameters()

            columns.append(_Column("ident", "id", "catalog_id"))

            joins += [_Join("ident", _Column("basic", "oid"), _Column("ident", "oidref"))]

            instance_criteria.append(fr"id LIKE '{catalog} %'")
            if criteria:
                instance_criteria.append(f"({criteria})")

            return self._query(top, columns, joins, instance_criteria,
                               get_query_payload=get_query_payload, async_job=async_job)

        def query_hierarchy(self, name, hierarchy, *,
                            detailed_hierarchy=True,
                            criteria=None, get_query_payload=False, async_job=False):
            """Query either the parents or the children of the object.

            Parameters
            ----------
            name : str
                name of the object
            hierarchy : str
                Can take the values "parents" to return the parents of the object (ex: a
                galaxy cluster is a parent of a galaxy), the value "children" to return
                the children of an object (ex: stars can be children of a globular cluster),
                or the value "siblings" to return the object that share a parent with the
                given one (ex: the stars of an open cluster are all siblings).
            detailed_hierarchy : bool
                Whether to add the two extra columns 'hierarchy_bibcode' that gives the
                article in which the hierarchy link is mentioned, and
                'membership_certainty'. membership_certainty is an integer that reflects the
                certainty of the hierarchy link according to the authors. Ranges between 0
                and 100 where 100 means that the authors were certain of the classification.
                Defaults to False.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string. See example.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                Query results table

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> parent = Simbad.query_hierarchy("2MASS J18511048-0615470",
            ...                                 detailed_hierarchy=False,
            ...                                 hierarchy="parents")  # doctest: +REMOTE_DATA
            >>> parent[["main_id", "ra", "dec"]] # doctest: +REMOTE_DATA
            <Table length=1>
             main_id     ra     dec
                        deg     deg
              object  float64 float64
            --------- ------- -------
            NGC  6705 282.766  -6.272
            """
            top, columns, joins, instance_criteria = self._get_query_parameters()

            sub_query = ("(SELECT oidref FROM ident "
                         f"WHERE id = '{name}') AS name")

            if detailed_hierarchy:
                columns.append(_Column("h_link", "link_bibcode", "hierarchy_bibcode"))
                columns.append(_Column("h_link", "membership", "membership_certainty"))

            if hierarchy == "parents":
                joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "parent"))]
                instance_criteria.append("h_link.child = name.oidref")
            elif hierarchy == "children":
                joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "child"))]
                instance_criteria.append("h_link.parent = name.oidref")
            elif hierarchy == "siblings":
                sub_query = ("(SELECT DISTINCT basic.oid FROM "
                             f"{sub_query}, basic JOIN h_link ON basic.oid = h_link.parent "
                             "WHERE h_link.child = name.oidref) AS parents")
                joins += [_Join("h_link", _Column("basic", "oid"), _Column("h_link", "child"))]
                instance_criteria.append("h_link.parent = parents.oid")
            else:
                raise ValueError("'hierarchy' can only take the values 'parents', "
                                 f"'siblings', or 'children'. Got '{hierarchy}'.")

            if criteria:
                instance_criteria.append(f"({criteria})")

            return self._query(top, columns, joins, instance_criteria,
                               from_table=f"{sub_query}, basic", distinct=True,
                               get_query_payload=get_query_payload, async_job=async_job)

        @deprecated_renamed_argument(["verbose"], new_name=[None],
                                     since=['0.4.8'], relax=True)
        def query_bibobj(self, bibcode, *, criteria=None,
                         get_query_payload=False, async_job=False,
                         verbose=False):
            """Query all the objects mentioned in an article.

            Parameters
            ----------
            bibcode : str
                the bibcode of the article
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                Query results table
            """
            top, columns, joins, instance_criteria = self._get_query_parameters()

            joins += [_Join("has_ref", _Column("basic", "oid"), _Column("has_ref", "oidref")),
                      _Join("ref", _Column("has_ref", "oidbibref"), _Column("ref", "oidbib"))]

            columns += [_Column("ref", "bibcode"),
                        _Column("has_ref", "obj_freq")]

            instance_criteria.append(f"bibcode = '{_adql_parameter(bibcode)}'")
            if criteria:
                instance_criteria.append(f"({criteria})")

            return self._query(top, columns, joins, instance_criteria,
                               get_query_payload=get_query_payload, async_job=async_job)

        @deprecated_renamed_argument(["verbose", "cache"], new_name=[None, None],
                                     since=['0.4.8', '0.4.8'], relax=True)
        def query_bibcode(self, bibcode, *, wildcard=False,
                          abstract=False, criteria=None, get_query_payload=False,
                          async_job=False, verbose=None, cache=None, ):
            """Query the references corresponding to a given bibcode.

            Wildcards may be used to specify bibcodes.

            Parameters
            ----------
            bibcode : str
                The bibcode of the article to be queried
            wildcard : bool, defaults to False
                When it is set to `True` it implies that the object is specified
                with wildcards.
            abstract : bool, defaults to False
                When this is set to `True`, the abstract of the article is also included
                to the result.
            criteria : str
                Criteria to be applied to the query. These should be written in the ADQL
                syntax in a single string. See example.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                Query results table

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> simbad = Simbad()
            >>> simbad.ROW_LIMIT = 5
            >>> simbad.query_bibcode("2016PhRvL.*", wildcard=True,
            ...                      criteria="title LIKE '%gravitational wave%coalescence.'") # doctest: +REMOTE_DATA
            <Table length=1>
                  bibcode                    doi               journal ... volume  year
                   object                   object              object ... int32  int16
            ------------------- ------------------------------ ------- ... ------ -----
            2016PhRvL.116x1103A 10.1103/PhysRevLett.116.241103   PhRvL ...    116  2016
            """
            ref_columns = ["bibcode", "doi", "journal", "nbobject", "page", "last_page",
                           "title", "volume", "year"]
            # abstract option
            if abstract:
                ref_columns.append("abstract")
            ref_columns = str(ref_columns).replace("'", '"')[1:-1]

            # take row limit
            if self.ROW_LIMIT != -1:
                query = f"SELECT TOP {self.ROW_LIMIT} {ref_columns} FROM ref WHERE "
            else:
                query = f"SELECT {ref_columns} FROM ref WHERE "

            if wildcard:
                query += f"regexp(lowercase(bibcode), '{_wildcard_to_regexp(bibcode.casefold())}') = 1"
            else:
                query += f"bibcode = '{_adql_parameter(bibcode)}'"

            if criteria:
                query += f" AND {criteria}"

            query += " ORDER BY bibcode"

            return self.query_tap(query, get_query_payload=get_query_payload,
                                  async_job=async_job)

        @deprecated_renamed_argument(["verbose", "cache"], new_name=[None, None],
                                     since=['0.4.8', '0.4.8'], relax=True)
        def query_objectids(self, object_name, *, criteria=None, get_query_payload=False,
                            async_job=False, verbose=None, cache=None):
            """Query SIMBAD with an object name.

            This returns a table of all names associated with that object.

            Parameters
            ----------
            object_name : str
                name of object to be queried
            criteria : str
                an additional criteria to constrain the result. As the output of this
                method has only one column, these criteria can only be imposed on
                the column ``ident.id``.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.

            Returns
            -------
            table : `~astropy.table.Table`
                The result of the query to SIMBAD.

            Examples
            --------
            Get all the names of the Bubble Nebula

            >>> from astroquery.simbad import Simbad
            >>> Simbad.query_objectids("bubble nebula") # doctest: +REMOTE_DATA
            <Table length=8>
                          id
                        object
            ------------------------------
            [ABB2014] WISE G112.212+00.229
                                 LBN   548
                                 NGC  7635
                                 SH  2-162
                      [KC97c] G112.2+00.2b
                     [L89b] 112.237+00.226
                        GRS G112.22 +00.22
                        NAME Bubble Nebula

            Get the NGC name of M101

            >>> from astroquery.simbad import Simbad
            >>> Simbad.query_objectids("m101", criteria="ident.id LIKE 'NGC%'") # doctest: +REMOTE_DATA
            <Table length=1>
                id
              object
            ---------
            NGC  5457
            """
            query = ("SELECT ident.id FROM ident AS id_typed JOIN ident USING(oidref)"
                     f"WHERE id_typed.id = '{_adql_parameter(object_name)}'")
            if criteria is not None:
                query += f" AND {criteria}"
            return self.query_tap(query, get_query_payload=get_query_payload,
                                  async_job=async_job)

        @deprecated(since="v0.4.8",
                    message=("'query_criteria' is deprecated. It uses the former sim-script "
                             "(SIMBAD specific) syntax "
                             "(see https://simbad.cds.unistra.fr/simbad/sim-fsam). "
                             "Possible replacements are the 'criteria' argument in the other "
                             "query methods or custom 'query_tap' queries. "
                             "These two replacements use the standard ADQL syntax."))
        def query_criteria(self, *args, get_query_payload=False, **kwargs):
            """Query SIMBAD based on any criteria [deprecated].

            This method is deprecated as it uses the former SIMBAD-specific sim-script syntax.
            There are two possible replacements that have been added with astroquery v0.4.8
            and that use the standard ADQL syntax. See the examples section.

            Parameters
            ----------
            args:
                String arguments passed directly to SIMBAD's script
                (e.g., 'region(box, GAL, 10.5 -10.5, 0.5d 0.5d)')
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            kwargs:
                Keyword / value pairs passed to SIMBAD's script engine
                (e.g., {'otype'='SNR'} will be rendered as otype=SNR)

            Returns
            -------
            table : `~astropy.table.Table`
                Query results table

            Examples
            --------

            Can be replaced by the ``criteria`` argument that was added in the
            other query_*** methods

            >>> from astroquery.simbad import Simbad
            >>> Simbad(ROW_LIMIT=5).query_region('M1', '2d', criteria="otype='G..'") # doctest: +REMOTE_DATA +IGNORE_OUTPUT
            <Table length=5>
              main_id            ra        ... coo_wavelength     coo_bibcode
                                deg        ...
               object         float64      ...      str1             object
            ------------ ----------------- ... -------------- -------------------
            LEDA  136099 85.48166666666667 ...                1996A&AS..117....1S
            LEDA  136047 83.66958333333332 ...                1996A&AS..117....1S
            LEDA  136057 84.64499999999998 ...                1996A&AS..117....1S
            LEDA 1630996 83.99208333333333 ...              O 2003A&A...412...45P
              2MFGC 4574 84.37534166666669 ...              I 2006AJ....131.1163S

            Or by custom-written ADQL queries

            >>> from astroquery.simbad import Simbad
            >>> Simbad.query_tap("SELECT TOP 5 main_id, sp_type"
            ...                  " FROM basic WHERE sp_type < 'F3'") # doctest: +REMOTE_DATA +IGNORE_OUTPUT
            <Table length=5>
              main_id   sp_type
               object    object
            ----------- -------
             HD  24033B     (A)
             HD  70218B     (A)
             HD 128284B   (A/F)
            CD-34  5319   (A/F)
              HD  80593   (A0)V
            """
            top, columns, joins, instance_criteria = self._get_query_parameters()
            list_kwargs = [f"{key}='{argument}'" for key, argument in kwargs.items()]
            added_criteria = f"({CriteriaTranslator.parse(' & '.join(list(list(args) + list_kwargs)))})"
            instance_criteria.append(added_criteria)
            if "otypes." in added_criteria:
                joins.append(_Join("otypes", _Column("basic", "oid"),
                                   _Column("otypes", "oidref")))
            if "allfluxes." in added_criteria:
                joins.append(_Join("allfluxes", _Column("basic", "oid"),
                                   _Column("allfluxes", "oidref")))
            return self._query(top, columns, joins, instance_criteria,
                               get_query_payload=get_query_payload)

        @deprecated_renamed_argument("get_adql", new_name="get_query_payload",
                                     since='0.4.8', relax=True)
        def list_tables(self, *, get_query_payload=False):
            """List the names and descriptions of the tables in SIMBAD.

            Parameters
            ----------
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            get_adql : bool, optional
                Deprecated since '0.4.8'. This is replaced by get_query_payload that contain
                more information than just the ADQL string

            Returns
            -------
            `~astropy.table.Table`
            """
            query = ("SELECT table_name, description"
                     " FROM TAP_SCHEMA.tables"
                     " WHERE schema_name = 'public'")
            return self.query_tap(query, get_query_payload=get_query_payload)

        @deprecated_renamed_argument("get_adql", new_name="get_query_payload",
                                     since='0.4.8', relax=True)
        def list_columns(self, *tables: str, keyword=None, get_query_payload=False):
            """Get the list of SIMBAD columns.

            Add tables names to restrict to some tables. Call the function without
            any parameter to get all columns names from all tables. The keyword argument
            looks for columns in the selected SIMBAD tables that contain the
            given keyword. The keyword search is not case-sensitive.

            Parameters
            ----------
            *tables : str, optional
                Add tables names as strings to restrict to these tables columns.
                This is not case-sensitive.
            keyword : str, optional
                A keyword to look for in column names, table names, or descriptions.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            get_adql : bool, optional
                Deprecated since '0.4.8'. This is replaced by get_query_payload that contain
                more information than just the ADQL string

            Returns
            -------
            `~astropy.table.Table`

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> Simbad.list_columns("ids", "ident") # doctest: +REMOTE_DATA
            <Table length=4>
            table_name column_name datatype ...  unit      ucd
              object      object    object  ... object    object
            ---------- ----------- -------- ... ------ -----------
                 ident          id  VARCHAR ...            meta.id
                 ident      oidref   BIGINT ...        meta.record
                   ids         ids  VARCHAR ...            meta.id
                   ids      oidref   BIGINT ...        meta.record

            >>> from astroquery.simbad import Simbad
            >>> Simbad.list_columns(keyword="filter") # doctest: +REMOTE_DATA
            <Table length=5>
             table_name column_name   datatype  ...  unit              ucd
               object      object      object   ... object            object
            ----------- ----------- ----------- ... ------ ---------------------------
                 filter description UNICODECHAR ...             meta.note;instr.filter
                 filter  filtername     VARCHAR ...        instr.bandpass;instr.filter
                 filter        unit     VARCHAR ...                          meta.unit
                   flux      filter     VARCHAR ...        instr.bandpass;instr.filter
            mesDiameter      filter        CHAR ...        instr.bandpass;instr.filter

            >>> from astroquery.simbad import Simbad
            >>> Simbad.list_columns("basic", keyword="object") # doctest: +REMOTE_DATA
            <Table length=4>
            table_name column_name datatype ...  unit         ucd
              object      object    object  ... object       object
            ---------- ----------- -------- ... ------ -----------------
                 basic     main_id  VARCHAR ...        meta.id;meta.main
                 basic   otype_txt  VARCHAR ...                src.class
                 basic         oid   BIGINT ...              meta.record
                 basic       otype  VARCHAR ...                src.class
            """
            query = ("SELECT table_name, column_name, datatype, description, unit, ucd"
                     " FROM TAP_SCHEMA.columns"
                     " WHERE table_name NOT LIKE 'TAP_SCHEMA.%'")
            # select the tables
            tables = tuple(map(str.casefold, tables))
            if len(tables) == 1:
                query += f" AND LOWERCASE(table_name) = '{tables[0]}'"
            elif len(tables) > 1:
                query += f" AND LOWERCASE(table_name) IN {tables}"
            # add the keyword condition
            if keyword is not None:
                condition = f"LIKE LOWERCASE('%{_adql_parameter(keyword)}%')"
                query += (f" AND ( (LOWERCASE(column_name) {condition})"
                          f" OR (LOWERCASE(description) {condition})"
                          f" OR (LOWERCASE(table_name) {condition}))")
            query += " ORDER BY table_name, principal DESC, column_name"
            return self.query_tap(query, get_query_payload=get_query_payload)

        def list_linked_tables(self, table: str, *, get_query_payload=False):
            """Expose the tables that can be non-obviously linked with the given table.

            This list contains only the links where the column names are not the same in the
            two tables. For example every ``oidref`` column of any table can be joined with
            any other ``oidref``. The same goes for every ``otype`` column even if this is not
            returned by this method.

            Parameters
            ----------
            table : str
                One of SIMBAD's tables name
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.

            Returns
            -------
            `~astropy.table.Table`
                The information necessary to join the given table to an other.

            Examples
            --------
            >>> from astroquery.simbad import Simbad
            >>> Simbad.list_linked_tables("otypes") # doctest: +REMOTE_DATA
            <Table length=2>
            from_table from_column target_table target_column
              object      object      object        object
            ---------- ----------- ------------ -------------
              otypedef       otype       otypes         otype
                otypes      oidref        basic           oid
            """
            query = ("SELECT from_table, from_column, target_table, target_column"
                     " FROM TAP_SCHEMA.key_columns JOIN TAP_SCHEMA.keys USING (key_id)"
                     f" WHERE (from_table = '{_adql_parameter(table)}')"
                     f" OR (target_table = '{_adql_parameter(table)}')")
            return self.query_tap(query, get_query_payload=get_query_payload)

        def query_tap(self, query: str, *, maxrec=10000, async_job=False,
                      get_query_payload=False, **uploads):
            """Query SIMBAD TAP service.

            Parameters
            ----------
            query : str
                A string containing the query written in the
                Astronomical Data Query Language (ADQL).
            maxrec : int, default: 10000
                The number of records to be returned. Its maximum value is given by
                `~astroquery.simbad.SimbadClass.hardlimit`.
            uploads : `~astropy.table.Table` | `~astropy.io.votable.tree.VOTableFile` | `~pyvo.dal.DALResults`
                Any number of local tables to be used in the *query*. In the *query*, these tables
                are referred as *TAP_UPLOAD.table_alias* where *TAP_UPLOAD* is imposed and *table_alias*
                is the keyword name you chose. The maximum number of lines for the uploaded tables is 200000.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.
            get_query_payload : bool, default=False
                When set to ``True`` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.

            Returns
            -------
            `~astropy.table.Table`
                The response returned by Simbad.

            Notes
            -----
            A TAP (Table Access Protocol) service allows to query data tables with
            queries written in ADQL (Astronomical Data Query Language), a flavor
            of the more general SQL (Structured Query Language).
            For more documentation about writing ADQL queries, you can read its official
            documentation (`ADQL documentation <https://ivoa.net/documents/ADQL/index.html>`__)
            or the `Simbad ADQL cheat sheet <http://simbad.cds.unistra.fr/simbad/tap/help/adqlHelp.html>`__.
            See also: a `graphic representation of Simbad's tables and their relations
            <http://simbad.cds.unistra.fr/simbad/tap/tapsearch.html>`__.

            See Also
            --------
            list_tables : The list of SIMBAD's tables.
            list_columns : SIMBAD's columns list, can be restricted to some tables and some keyword.
            list_linked_tables : Given a table, expose non-obvious possible joins with other tables.

            Examples
            --------

            To see the five oldest papers referenced in SIMBAD

            >>> from astroquery.simbad import Simbad
            >>> Simbad.query_tap("SELECT top 5 bibcode, title "
            ...                  "FROM ref ORDER BY bibcode") # doctest: +REMOTE_DATA
            <Table length=5>
                  bibcode       ...
                   object       ...
            ------------------- ...
            1850CDT..1784..227M ...
            1857AN.....45...89S ...
            1861MNRAS..21...68B ...
            1874MNRAS..34...75S ...
            1877AN.....89...13W ...

            Get the type for a list of objects

            >>> from astroquery.simbad import Simbad
            >>> Simbad.query_tap("SELECT main_id, otype"
            ...                  " FROM basic WHERE main_id IN ('m10', 'm13')") # doctest: +REMOTE_DATA
            <Table length=2>
            main_id otype
             object object
            ------- ------
              M  10    GlC
              M  13    GlC

            Upload a table to use in a query

            >>> from astroquery.simbad import Simbad
            >>> from astropy.table import Table
            >>> letters_table = Table([["a", "b", "c"]], names=["alphabet"])
            >>> Simbad.query_tap("SELECT TAP_UPLOAD.my_table_name.* from TAP_UPLOAD.my_table_name",
            ...                  my_table_name=letters_table) # doctest: +REMOTE_DATA
            <Table length=3>
            alphabet
              str1
            --------
                   a
                   b
                   c
            """
            if maxrec > self.hardlimit:
                raise ValueError(f"The maximum number of records cannot exceed {self.hardlimit}.")
            if query.count("'") % 2:
                raise ValueError("Query string contains an odd number of single quotes."
                                 " Escape the unpaired single quote by doubling it.\n"
                                 "ex: 'Barnard's galaxy' -> 'Barnard''s galaxy'.")
            if get_query_payload:
                return dict(TAPQuery(self.SIMBAD_URL, query, maxrec=maxrec, uploads=uploads))
            # without uploads we call the version with cache
            if uploads == {}:
                return _cached_query_tap(self.tap, query, maxrec=maxrec,
                                         async_job=async_job, timeout=self.timeout)
            # with uploads it has to be without cache
            if async_job:
                return self.tap.run_async(query, maxrec=maxrec,
                                          execution_duration=self.timeout,
                                          uploads=uploads).to_table()
            return self.tap.run_sync(query, maxrec=maxrec, uploads=uploads).to_table()

        @staticmethod
        def clear_cache():
            """Clear the cache of SIMBAD."""
            _cached_query_tap.cache_clear()
            gc.collect()

        # -----------------------------
        # Utility methods for query TAP
        # -----------------------------

        def _get_query_parameters(self):
            """Get the current building blocks of an ADQL query."""
            return tuple(map(copy.deepcopy, (self.ROW_LIMIT, self.columns_in_output, self.joins, self.criteria)))

        def _query(self, top, columns, joins, criteria, from_table="basic", distinct=False,
                   async_job=False, get_query_payload=False, **uploads):
            """Generate an ADQL string from the given query parameters and executes the query.

            Parameters
            ----------
            top : int
                number of lines to be returned
            columns : List[SimbadClass.Column]
                The list of columns to be included in the output.
            joins : List[SimbadClass.Join]
                The list of joins to be made with basic.
            criteria : List[str]
                A list of strings. These criteria will be joined
                with an AND clause.
            from_table : str, optional
                The table after 'FROM' in the ADQL string. Defaults to "basic".
            distinct : bool, optional
                Whether to add the DISTINCT instruction to the query.
            get_query_payload : bool, optional
                When set to `True` the method returns the HTTP request parameters without
                querying SIMBAD. The ADQL string is in the 'QUERY' key of the payload.
                Defaults to `False`.
            async_job: bool, optional
                When set to `True`, the query will be executed in asynchronous mode. This is
                better for very long queries, as it prevents transient failures to abort the
                query execution.
                Defaults to `False`.
            uploads : `~astropy.table.Table`
                Any number of local tables to be used in the *query*. In the *query*, these tables
                are referred as *TAP_UPLOAD.table_alias* where *TAP_UPLOAD* is imposed and *table_alias*
                is the keyword name you chose. The maximum number of lines for the uploaded tables is 200000.

            Returns
            -------
            `~astropy.table.Table`
                The result of the query to SIMBAD.
            """
            distinct_results = " DISTINCT" if distinct else ""
            top_part = f" TOP {top}" if top != -1 else ""

            # columns
            input_columns = [f'{column.table}."{column.name}" AS "{column.alias}"' if column.alias is not None
                             else f'{column.table}."{column.name}"' for column in columns]
            # remove possible duplicates
            unique_columns = []
            [unique_columns.append(column) for column in input_columns if column not in unique_columns]
            columns = " " + ", ".join(unique_columns)
            # selecting all columns is the only case where this should not be a string
            columns = columns.replace('"*"', "*")

            # joins
            if joins == []:
                join = ""
            else:
                unique_joins = []
                [unique_joins.append(join) for join in joins if join not in unique_joins]
                # the joined tables can have an alias. We handle the two cases here
                join = " " + " ".join([(f'{join.join_type} {join.table} AS {join.alias} '
                                        f'ON {join.column_left.table}."{join.column_left.name}" = '
                                        f'{join.alias}."{join.column_right.name}"')
                                       if join.alias is not None else
                                       (f'{join.join_type} {join.table} ON {join.column_left.table}."'
                                        f'{join.column_left.name}" = {join.column_right.table}."'
                                        f'{join.column_right.name}"') for join in unique_joins])

            # criteria
            if criteria != []:
                criteria = f" WHERE {' AND '.join(criteria)}"
            else:
                criteria = ""

            query = f"SELECT{distinct_results}{top_part}{columns} FROM {from_table}{join}{criteria}"

            response = self.query_tap(query, get_query_payload=get_query_payload,
                                      maxrec=self.hardlimit, async_job=async_job,
                                      **uploads)

            if len(response) == 0 and top != 0:
                warnings.warn("The request executed correctly, but there was no data corresponding"
                              " to these criteria in SIMBAD", NoResultsWarning)
            return response

    Simbad = SimbadClass()