docs: def _get_lineage(self, tax_id, merge_obsolete=True):
          """Return a list of [(rank, tax_id)] describing the lineage of
          tax_id. If ``merge_obsolete`` is True and ``tax_id`` has been
          replaced, use the corresponding value in table merged.
  
          """
  
          # Be sure we aren't working with an obsolete tax_id
          if merge_obsolete:
              tax_id = self._get_merged(tax_id)
  
          # Note: joining with ranks seems like a no-op, but for some
          # reason it results in a faster query using sqlite, as well as
          # an ordering from leaf --> root. Might be a better idea to
          # sort explicitly if this is the expected behavior, but it
          # seems like for the most part, the lineage is converted to a
          # dict and the order is irrelevant.
          cmd = """
          WITH RECURSIVE a AS (
           SELECT tax_id, parent_id, rank
            FROM {nodes}
            WHERE tax_id = {}
          UNION ALL
           SELECT p.tax_id, p.parent_id, p.rank
            FROM a JOIN {nodes} p ON a.parent_id = p.tax_id
          )
          SELECT a.rank, a.tax_id FROM a
          JOIN {ranks} using(rank)
          """.format(self.placeholder, nodes=self.nodes, ranks=self.ranks_table)
  
          # with some versions of sqlite3, an error is raised when no
          # rows are returned; with others, an empty list is returned.
          try:
              with self.engine.connect() as con:
                  result = con.execute(cmd, (tax_id,))
                  # reorder so that root is first
                  lineage = result.fetchall()[::-1]
          except sqlalchemy.exc.ResourceClosedError:
              lineage = []
  
          if not lineage:
              raise ValueError('tax id "{}" not found'.format(tax_id))
  
          return lineage