"""
Database retrieval queries.
"""
import asyncio
from typing import Optional, List, Tuple, Collection, Iterator, Sequence
import itertools
import sqlite3

import wn
from wn import DatabaseError
from wn._types import Metadata
from wn._db import connect, NON_ROWID, schema_hash

# Local Types

_Pronunciation = Tuple[
    str,  # value
    str,  # variety
    str,  # notation
    bool,  # phonemic
    str,  # audio
]
_Tag = Tuple[str, str]  # tag, category
_Form = Tuple[str, Optional[str], Optional[str], int]  # form  # id  # script  # rowid
_Word = Tuple[
    str,  # id
    str,  # pos
    List[_Form],  # forms
    int,  # lexid
    int,  # rowid
]
_Synset = Tuple[
    str,  # id
    str,  # pos
    str,  # ili
    int,  # lexid
    int,  # rowid
]
_Synset_Relation = Tuple[str, int, str, str, str, int, int]  # relname, relid, *_Synset
_Sense = Tuple[
    str,  # id
    str,  # entry_id
    str,  # synset_id
    int,  # lexid
    int,  # rowid
]
_Sense_Relation = Tuple[str, int, str, str, str, int, int]  # relname, relid,  *_Sense
_Count = Tuple[int, int]  # count, count_id
_SyntacticBehaviour = Tuple[str, str, List[str]]  # id  # frame  # sense ids
_ILI = Tuple[
    Optional[str],  # id
    str,  # status
    Optional[str],  # definition
    int,  # rowid
]
_Lexicon = Tuple[
    int,  # rowid
    str,  # id
    str,  # label
    str,  # language
    str,  # email
    str,  # license
    str,  # version
    str,  # url
    str,  # citation
    str,  # logo
    Metadata,  # metadata
]

refreshing = False
created = False


async def refresh_keyword_matching_table(keywords: List[str]) -> None:
    if not keywords:
        return
    global created
    global refreshing
    refreshing = True
    if not wn.config.allow_multithreading:
        print("Hi")
        raise DatabaseError("Can not refresh table since multithreading is disabled")
    else:
        conn = connect()
        cur = conn.cursor()
        q1 = """
        DROP TABLE IF EXISTS keyword_matches
        """
        q2 = f"""
        CREATE TABLE keyword_matches AS
WITH sy AS (SELECT DISTINCT s3.synset_rowid
            FROM forms f
                     JOIN senses s ON f.entry_rowid = s.entry_rowid
                     JOIN senses s1 ON s.synset_rowid = s1.synset_rowid
                     JOIN forms f2 ON s1.entry_rowid = f2.entry_rowid
                     JOIN senses s3 ON f2.entry_rowid = s3.entry_rowid),
     graph AS (SELECT source_rowid AS sid1, target_rowid AS sid2, source_rowid AS start
               FROM synsets
                        JOIN synset_relations sr ON synsets.rowid = sr.source_rowid
                        JOIN relation_types rt ON sr.type_rowid = rt.rowid
               WHERE type = 'hypernym'
                 AND source_rowid IN sy),
     relations AS (SELECT sid1, sid2, start
                   FROM graph
                   UNION ALL
                   SELECT sr.source_rowid AS sid1, sr.target_rowid AS sid2, start
                   FROM relations rvr
                            JOIN synset_relations sr ON rvr.sid2 = sr.source_rowid
                            JOIN relation_types r ON sr.type_rowid = r.rowid
                   WHERE type = 'hypernym'),
     fr AS (SELECT f2.form, relations.start
            FROM relations
                     JOIN senses ON relations.sid2 = senses.synset_rowid
                     JOIN forms f2 ON senses.entry_rowid = f2.entry_rowid)
SELECT DISTINCT f3.form AS word, fr.form AS keyword
FROM fr
         JOIN senses ON senses.synset_rowid = fr.start
         JOIN forms f3 ON senses.entry_rowid = f3.entry_rowid
WHERE fr.form IN ({_qs(keywords)});
        """
        q3 = """
CREATE INDEX k_index ON keyword_matches (word)
        """
        cur.execute(q1)
        cur.execute(q2, keywords)
        cur.execute(q3)
        conn.commit()
        print("ok")
        created = True
        refreshing = False


def match_for_keyword_in_hypernym_graph(
    word: str, keywords: List[str] = None, cur=None, recompute_with_new_keywords=False
) -> Iterator[Tuple[str]]:
    global refreshing
    global created
    if not keywords:
        keywords = wn.config.match_on_keywords
    if not keywords:
        return
    if cur is None:
        cur = connect().cursor()
    if (
        not created
        and (keywords == wn.config.match_on_keywords or recompute_with_new_keywords)
        and not refreshing
    ):
        refreshing = True
        asyncio.get_event_loop().create_task(refresh_keyword_matching_table(keywords))
        print(schema_hash(connect()))
    if not refreshing and keywords == wn.config.match_on_keywords:
        query = """
        SELECT keyword FROM keyword_matches WHERE word = ?
        """
        for row in cur.execute(query, [word]):
            yield row
        return
    query = f"""
    WITH sy AS (SELECT DISTINCT s3.synset_rowid
            FROM forms f
                     JOIN senses s ON f.entry_rowid = s.entry_rowid
                     JOIN senses s1 ON s.synset_rowid = s1.synset_rowid
                     JOIN forms f2 ON s1.entry_rowid = f2.entry_rowid
                     JOIN senses s3 ON f2.entry_rowid = s3.entry_rowid
            WHERE f2.form = ?),
     graph AS (SELECT source_rowid AS sid1, target_rowid AS sid2
               FROM synsets
                        JOIN synset_relations sr ON synsets.rowid = sr.source_rowid
                        JOIN relation_types rt ON sr.type_rowid = rt.rowid
               WHERE type = 'hypernym'
                 AND source_rowid IN sy),
     relations AS (SELECT sid1, sid2
                   FROM graph
                   UNION ALL
                   SELECT sr.source_rowid AS sid1, sr.target_rowid AS sid2
                   FROM relations rvr
                            JOIN synset_relations sr ON rvr.sid2 = sr.source_rowid
                            JOIN relation_types r ON sr.type_rowid = r.rowid
                   WHERE type = 'hypernym'),
     fr AS (SELECT f2.form
            FROM relations
                     JOIN senses ON relations.sid2 = senses.synset_rowid
                     JOIN forms f2 ON senses.entry_rowid = f2.entry_rowid)
SELECT DISTINCT fr.form AS keyword
FROM fr
WHERE fr.form IN ({_qs(keywords)})
    """
    for row in cur.execute(query, ([word] + keywords)):
        yield row


def find_lexicons(
    lexicon: str,
    lang: str = None,
) -> Iterator[_Lexicon]:
    cur = connect().cursor()
    found = False
    for specifier in lexicon.split():
        limit = "-1" if "*" in lexicon else "1"
        if ":" not in specifier:
            specifier += ":*"
        query = f"""
            SELECT DISTINCT rowid, id, label, language, email, license,
                            version, url, citation, logo
              FROM lexicons
             WHERE id || ":" || version GLOB :specifier
               AND (:language ISNULL OR language = :language)
             LIMIT {limit}
        """
        params = {"specifier": specifier, "language": lang}
        for row in cur.execute(query, params):
            yield row
            found = True
    # only raise an error when the query specifies something
    if not found and (lexicon != "*" or lang is not None):
        raise wn.Error(f"no lexicon found with lang={lang!r} and lexicon={lexicon!r}")


def get_lexicon(rowid: int) -> _Lexicon:
    conn = connect()
    return _get_lexicon(conn, rowid)


def _get_lexicon(conn: sqlite3.Connection, rowid: int) -> _Lexicon:
    query = """
        SELECT DISTINCT rowid, id, label, language, email, license,
                        version, url, citation, logo
        FROM lexicons
        WHERE rowid = ?
    """
    row: Optional[_Lexicon] = conn.execute(query, (rowid,)).fetchone()
    if row is None:
        raise LookupError(rowid)  # should we have a WnLookupError?
    return row


def get_modified(rowid: int) -> bool:
    query = "SELECT modified FROM lexicons WHERE rowid = ?"
    return connect().execute(query, (rowid,)).fetchone()[0]


def get_lexicon_dependencies(rowid: int) -> List[Tuple[str, str, str, Optional[int]]]:
    query = """
        SELECT provider_id, provider_version, provider_url, provider_rowid
          FROM lexicon_dependencies
         WHERE dependent_rowid = ?
    """
    return connect().execute(query, (rowid,)).fetchall()


def get_lexicon_extension_bases(rowid: int, depth: int = -1) -> List[int]:
    query = """
          WITH RECURSIVE ext(x, d) AS
               (SELECT base_rowid, 1
                  FROM lexicon_extensions
                 WHERE extension_rowid = :rowid
                 UNION SELECT base_rowid, d+1
                         FROM lexicon_extensions
                         JOIN ext ON extension_rowid = x)
        SELECT x FROM ext
         WHERE :depth < 0 OR d <= :depth
         ORDER BY d
    """
    rows = connect().execute(query, {"rowid": rowid, "depth": depth})
    return [row[0] for row in rows]


def get_lexicon_extensions(rowid: int, depth: int = -1) -> List[int]:
    query = """
          WITH RECURSIVE ext(x, d) AS
               (SELECT extension_rowid, 1
                  FROM lexicon_extensions
                 WHERE base_rowid = :rowid
                 UNION SELECT extension_rowid, d+1
                         FROM lexicon_extensions
                         JOIN ext ON base_rowid = x)
        SELECT x FROM ext
         WHERE :depth < 0 OR d <= :depth
         ORDER BY d
    """
    rows = connect().execute(query, {"rowid": rowid, "depth": depth})
    return [row[0] for row in rows]


def find_ilis(
    id: str = None,
    status: str = None,
    lexicon_rowids: Sequence[int] = None,
) -> Iterator[_ILI]:
    if status != "proposed":
        yield from _find_existing_ilis(
            id=id, status=status, lexicon_rowids=lexicon_rowids
        )
    if not id and (not status or status == "proposed"):
        yield from find_proposed_ilis(lexicon_rowids=lexicon_rowids)


def _find_existing_ilis(
    id: str = None,
    status: str = None,
    lexicon_rowids: Sequence[int] = None,
) -> Iterator[_ILI]:
    query = """
        SELECT DISTINCT i.id, ist.status, i.definition, i.rowid
          FROM ilis AS i
          JOIN ili_statuses AS ist ON i.status_rowid = ist.rowid
    """
    conditions: List[str] = []
    params: List = []
    if id:
        conditions.append("i.id = ?")
        params.append(id)
    if status:
        conditions.append("ist.status = ?")
        params.append(status)
    if lexicon_rowids:
        conditions.append(
            f"""
            i.rowid IN
            (SELECT ss.ili_rowid
               FROM synsets AS ss
              WHERE ss.lexicon_rowid IN ({_qs(lexicon_rowids)}))
        """
        )
        params.extend(lexicon_rowids)
    if conditions:
        query += "\n WHERE " + "\n   AND ".join(conditions)
    yield from connect().execute(query, params)


def find_proposed_ilis(
    synset_rowid: int = None,
    lexicon_rowids: Sequence[int] = None,
) -> Iterator[_ILI]:
    query = """
        SELECT null, "proposed", definition, rowid
          FROM proposed_ilis
    """
    conditions = []
    params = []
    if synset_rowid is not None:
        conditions.append("synset_rowid = ?")
        params.append(synset_rowid)
    if lexicon_rowids:
        conditions.append(
            f"""
            synset_rowid IN
            (SELECT ss.rowid FROM synsets AS ss
              WHERE ss.lexicon_rowid IN ({_qs(lexicon_rowids)}))
        """
        )
        params.extend(lexicon_rowids)
    if conditions:
        query += "\n WHERE " + "\n   AND ".join(conditions)
    yield from connect().execute(query, params)


def find_entries(
    id: str = None,
    forms: Sequence[str] = None,
    pos: str = None,
    lexicon_rowids: Sequence[int] = None,
    normalized: bool = False,
    search_all_forms: bool = False,
) -> Iterator[_Word]:
    conn = connect()
    cte = ""
    params: List = []
    conditions = []
    if id:
        conditions.append("e.id = ?")
        params.append(id)
    if forms:
        cte = f"WITH wordforms(s) AS (VALUES {_vs(forms)})"
        or_norm = "OR normalized_form IN wordforms" if normalized else ""
        and_rank = "" if search_all_forms else "AND rank = 0"
        conditions.append(
            f"""
            e.rowid IN
               (SELECT entry_rowid
                  FROM forms
                 WHERE (form IN wordforms {or_norm}) {and_rank})
        """.strip()
        )
        params.extend(forms)
    if pos:
        conditions.append("e.pos = ?")
        params.append(pos)
    if lexicon_rowids:
        conditions.append(f"e.lexicon_rowid IN ({_qs(lexicon_rowids)})")
        params.extend(lexicon_rowids)

    condition = ""
    if conditions:
        condition = "WHERE " + "\n           AND ".join(conditions)

    query = f"""
          {cte}
        SELECT DISTINCT e.lexicon_rowid, e.rowid, e.id, e.pos,
                        f.form, f.id, f.script, f.rowid
          FROM entries AS e
          JOIN forms AS f ON f.entry_rowid = e.rowid
         {condition}
         ORDER BY e.rowid, e.id, f.rank
    """

    rows: Iterator[
        Tuple[int, int, str, str, str, Optional[str], Optional[str], int]
    ] = conn.execute(query, params)
    groupby = itertools.groupby
    for key, group in groupby(rows, lambda row: row[0:4]):
        lexid, rowid, id, pos = key
        wordforms = [(row[4], row[5], row[6], row[7]) for row in group]
        yield (id, pos, wordforms, lexid, rowid)


def find_senses(
    id: str = None,
    forms: Sequence[str] = None,
    pos: str = None,
    lexicon_rowids: Sequence[int] = None,
    normalized: bool = False,
    search_all_forms: bool = False,
) -> Iterator[_Sense]:
    conn = connect()
    cte = ""
    params: List = []
    conditions = []
    if id:
        conditions.append("s.id = ?")
        params.append(id)
    if forms:
        cte = f"WITH wordforms(s) AS (VALUES {_vs(forms)})"
        or_norm = "OR normalized_form IN wordforms" if normalized else ""
        and_rank = "" if search_all_forms else "AND rank = 0"
        conditions.append(
            f"""
            s.entry_rowid IN
               (SELECT entry_rowid
                  FROM forms
                 WHERE (form IN wordforms {or_norm}) {and_rank})
        """.strip()
        )
        params.extend(forms)
    if pos:
        conditions.append("e.pos = ?")
        params.append(pos)
    if lexicon_rowids:
        conditions.append(f"s.lexicon_rowid IN ({_qs(lexicon_rowids)})")
        params.extend(lexicon_rowids)

    condition = ""
    if conditions:
        condition = "WHERE " + "\n           AND ".join(conditions)

    query = f"""
          {cte}
        SELECT DISTINCT s.id, e.id, ss.id, s.lexicon_rowid, s.rowid
          FROM senses AS s
          JOIN entries AS e ON e.rowid = s.entry_rowid
          JOIN synsets AS ss ON ss.rowid = s.synset_rowid
         {condition}
    """

    rows: Iterator[_Sense] = conn.execute(query, params)
    yield from rows


def find_synsets(
    id: str = None,
    forms: Sequence[str] = None,
    pos: str = None,
    ili: str = None,
    lexicon_rowids: Sequence[int] = None,
    normalized: bool = False,
    search_all_forms: bool = False,
) -> Iterator[_Synset]:
    conn = connect()
    cte = ""
    join = ""
    conditions = []
    order = ""
    params: List = []
    if id:
        conditions.append("ss.id = ?")
        params.append(id)
    if forms:
        cte = f"WITH wordforms(s) AS (VALUES {_vs(forms)})"
        or_norm = "OR normalized_form IN wordforms" if normalized else ""
        and_rank = "" if search_all_forms else "AND rank = 0"
        join = f"""\
          JOIN (SELECT _s.entry_rowid, _s.synset_rowid, _s.entry_rank
                  FROM forms AS f
                  JOIN senses AS _s ON _s.entry_rowid = f.entry_rowid
                 WHERE (f.form IN wordforms {or_norm}) {and_rank}) AS s
            ON s.synset_rowid = ss.rowid
        """.strip()
        params.extend(forms)
        order = "ORDER BY s.entry_rowid, s.entry_rank"
    if pos:
        conditions.append("ss.pos = ?")
        params.append(pos)
    if ili:
        conditions.append(
            "ss.ili_rowid IN (SELECT ilis.rowid FROM ilis WHERE ilis.id = ?)"
        )
        params.append(ili)
    if lexicon_rowids:
        conditions.append(f"ss.lexicon_rowid IN ({_qs(lexicon_rowids)})")
        params.extend(lexicon_rowids)

    condition = ""
    if conditions:
        condition = "WHERE " + "\n           AND ".join(conditions)

    query = f"""
          {cte}
        SELECT DISTINCT ss.id, ss.pos,
                        (SELECT ilis.id FROM ilis WHERE ilis.rowid=ss.ili_rowid),
                        ss.lexicon_rowid, ss.rowid
          FROM synsets AS ss
          {join}
         {condition}
         {order}
    """

    rows: Iterator[_Synset] = conn.execute(query, params)
    yield from rows


def get_synsets_for_ilis(
    ilis: Collection[str],
    lexicon_rowids: Sequence[int],
) -> Iterator[_Synset]:
    conn = connect()
    query = f"""
        SELECT DISTINCT ss.id, ss.pos, ili.id, ss.lexicon_rowid, ss.rowid
          FROM synsets as ss
          JOIN ilis as ili ON ss.ili_rowid = ili.rowid
         WHERE ili.id IN ({_qs(ilis)})
           AND ss.lexicon_rowid IN ({_qs(lexicon_rowids)})
    """
    params = *ilis, *lexicon_rowids
    result_rows: Iterator[_Synset] = conn.execute(query, params)
    yield from result_rows


def get_synset_relations(
    source_rowids: Collection[int],
    relation_types: Collection[str],
    lexicon_rowids: Sequence[int],
) -> Iterator[_Synset_Relation]:
    conn = connect()
    params: List = []
    constraint = ""
    if relation_types and "*" not in relation_types:
        constraint = f"WHERE type IN ({_qs(relation_types)})"
        params.extend(relation_types)
    params.extend(source_rowids)
    params.extend(lexicon_rowids)
    query = f"""
          WITH rt(rowid, type) AS
               (SELECT rowid, type FROM relation_types {constraint})
        SELECT DISTINCT rel.type, rel.rowid, tgt.id, tgt.pos,
                        (SELECT ilis.id FROM ilis WHERE ilis.rowid = tgt.ili_rowid),
                        tgt.lexicon_rowid, tgt.rowid
          FROM (SELECT rt.type, target_rowid, srel.rowid
                  FROM synset_relations AS srel
                  JOIN rt ON srel.type_rowid = rt.rowid
                 WHERE source_rowid IN ({_qs(source_rowids)})
                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
               ) AS rel
          JOIN synsets AS tgt
            ON tgt.rowid = rel.target_rowid
    """
    result_rows: Iterator[_Synset_Relation] = conn.execute(query, params)
    yield from result_rows


def get_definitions(
    synset_rowid: int,
    lexicon_rowids: Sequence[int],
) -> List[Tuple[str, str, str, int]]:
    conn = connect()
    query = f"""
        SELECT d.definition,
               d.language,
               (SELECT s.id FROM senses AS s WHERE s.rowid=d.sense_rowid),
               d.rowid
          FROM definitions AS d
         WHERE d.synset_rowid = ?
           AND d.lexicon_rowid IN ({_qs(lexicon_rowids)})
    """
    return conn.execute(query, (synset_rowid, *lexicon_rowids)).fetchall()


_SANITIZED_EXAMPLE_PREFIXES = {
    "senses": "sense",
    "synsets": "synset",
}


def get_examples(
    rowid: int,
    table: str,
    lexicon_rowids: Sequence[int],
) -> List[Tuple[str, str, int]]:
    conn = connect()
    prefix = _SANITIZED_EXAMPLE_PREFIXES.get(table)
    if prefix is None:
        raise wn.Error(f"'{table}' does not have examples")
    query = f"""
        SELECT example, language, rowid
          FROM {prefix}_examples
         WHERE {prefix}_rowid = ?
           AND lexicon_rowid IN ({_qs(lexicon_rowids)})
    """
    return conn.execute(query, (rowid, *lexicon_rowids)).fetchall()


def find_syntactic_behaviours(
    id: str = None,
    lexicon_rowids: Sequence[int] = None,
) -> Iterator[_SyntacticBehaviour]:
    conn = connect()
    query = """
        SELECT sb.id, sb.frame, s.id
          FROM syntactic_behaviours AS sb
          JOIN syntactic_behaviour_senses AS sbs
            ON sbs.syntactic_behaviour_rowid = sb.rowid
          JOIN senses AS s
            ON s.rowid = sbs.sense_rowid
    """
    conditions: List[str] = []
    params: List = []
    if id:
        conditions.append("sb.id = ?")
        params.append(id)
    if lexicon_rowids:
        conditions.append(f"sb.lexicon_rowid IN ({_qs(lexicon_rowids)})")
        params.extend(lexicon_rowids)
    if conditions:
        query += "\n WHERE " + "\n   AND ".join(conditions)
    rows: Iterator[Tuple[str, str, str]] = conn.execute(query, params)
    for key, group in itertools.groupby(rows, lambda row: row[0:2]):
        id, frame = key
        sense_ids = [row[2] for row in group]
        yield id, frame, sense_ids


def get_syntactic_behaviours(
    rowid: int,
    lexicon_rowids: Sequence[int],
) -> List[str]:
    conn = connect()
    query = f"""
        SELECT sb.frame
          FROM syntactic_behaviours AS sb
          JOIN syntactic_behaviour_senses AS sbs
            ON sbs.syntactic_behaviour_rowid = sb.rowid
         WHERE sbs.sense_rowid = ?
           AND sb.lexicon_rowid IN ({_qs(lexicon_rowids)})
    """
    return [row[0] for row in conn.execute(query, (rowid, *lexicon_rowids))]


def _get_senses(
    rowid: int,
    sourcetype: str,
    lexicon_rowids: Sequence[int],
) -> Iterator[_Sense]:
    conn = connect()
    query = f"""
        SELECT s.id, e.id, ss.id, s.lexicon_rowid, s.rowid
          FROM senses AS s
          JOIN entries AS e
            ON e.rowid = s.entry_rowid
          JOIN synsets AS ss
            ON ss.rowid = s.synset_rowid
         WHERE s.{sourcetype}_rowid = ?
           AND s.lexicon_rowid IN ({_qs(lexicon_rowids)})
         ORDER BY s.{sourcetype}_rank
    """
    return conn.execute(query, (rowid, *lexicon_rowids))


def get_entry_senses(rowid: int, lexicon_rowids: Sequence[int]) -> Iterator[_Sense]:
    yield from _get_senses(rowid, "entry", lexicon_rowids)


def get_synset_members(rowid: int, lexicon_rowids: Sequence[int]) -> Iterator[_Sense]:
    yield from _get_senses(rowid, "synset", lexicon_rowids)


def get_sense_relations(
    source_rowid: int,
    relation_types: Collection[str],
    lexicon_rowids: Sequence[int],
) -> Iterator[_Sense_Relation]:
    params: List = []
    constraint = ""
    if relation_types and "*" not in relation_types:
        constraint = f"WHERE type IN ({_qs(relation_types)})"
        params.extend(relation_types)
    params.append(source_rowid)
    params.extend(lexicon_rowids)
    query = f"""
          WITH rt(rowid, type) AS
               (SELECT rowid, type FROM relation_types {constraint})
        SELECT DISTINCT rel.type, rel.rowid,
                        s.id, e.id, ss.id,
                        s.lexicon_rowid, s.rowid
          FROM (SELECT rt.type, target_rowid, srel.rowid
                  FROM sense_relations AS srel
                  JOIN rt ON srel.type_rowid = rt.rowid
                 WHERE source_rowid = ?
                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
               ) AS rel
          JOIN senses AS s
            ON s.rowid = rel.target_rowid
          JOIN entries AS e
            ON e.rowid = s.entry_rowid
          JOIN synsets AS ss
            ON ss.rowid = s.synset_rowid
    """
    rows: Iterator[_Sense_Relation] = connect().execute(query, params)
    yield from rows


def get_sense_synset_relations(
    source_rowid: int,
    relation_types: Collection[str],
    lexicon_rowids: Sequence[int],
) -> Iterator[_Synset_Relation]:
    params: List = []
    constraint = ""
    if "*" not in relation_types:
        constraint = f"WHERE type IN ({_qs(relation_types)})"
        params.extend(relation_types)
    params.append(source_rowid)
    params.extend(lexicon_rowids)
    query = f"""
          WITH rt(rowid, type) AS
               (SELECT rowid, type FROM relation_types {constraint})
        SELECT DISTINCT rel.type, rel.rowid, ss.id, ss.pos,
                        (SELECT ilis.id FROM ilis WHERE ilis.rowid = ss.ili_rowid),
                        ss.lexicon_rowid, ss.rowid
          FROM (SELECT rt.type, target_rowid, srel.rowid
                  FROM sense_synset_relations AS srel
                  JOIN rt ON srel.type_rowid = rt.rowid
                 WHERE source_rowid = ?
                   AND lexicon_rowid IN ({_qs(lexicon_rowids)})
               ) AS rel
          JOIN synsets AS ss
            ON ss.rowid = rel.target_rowid
    """
    rows: Iterator[_Synset_Relation] = connect().execute(query, params)
    yield from rows


_SANITIZED_METADATA_TABLES = {
    "ilis": "ilis",
    "proposed_ilis": "proposed_ilis",
    "lexicons": "lexicons",
    "entries": "entries",
    "senses": "senses",
    "synsets": "synsets",
    "sense_relations": "sense_relations",
    "sense_synset_relations": "sense_synset_relations",
    "synset_relations": "synset_relations",
    "sense_examples": "sense_examples",
    "counts": "counts",
    "synset_examples": "synset_examples",
    "definitions": "definitions",
}


def get_metadata(rowid: int, table: str) -> Metadata:
    conn = connect()
    tablename = _SANITIZED_METADATA_TABLES.get(table)
    if tablename is None:
        raise wn.Error(f"'{table}' does not contain metadata")
    query = f"SELECT metadata FROM {tablename} WHERE rowid=?"
    return conn.execute(query, (rowid,)).fetchone()[0] or {}


_SANITIZED_LEXICALIZED_TABLES = {
    "senses": "senses",
    "synsets": "synsets",
}


def get_lexicalized(rowid: int, table: str) -> bool:
    conn = connect()
    tablename = _SANITIZED_LEXICALIZED_TABLES.get(table)
    if tablename is None:
        raise wn.Error(f"'{table}' does not mark lexicalization")
    if rowid == NON_ROWID:
        return False
    query = f"SELECT lexicalized FROM {tablename} WHERE rowid=?"
    return conn.execute(query, (rowid,)).fetchone()[0]


def get_adjposition(rowid: int) -> Optional[str]:
    conn = connect()
    query = "SELECT adjposition FROM adjpositions WHERE sense_rowid = ?"
    row = conn.execute(query, (rowid,)).fetchone()
    if row:
        return row[0]
    return None


def get_form_pronunciations(form_rowid: int) -> List[_Pronunciation]:
    # TODO: restrict by lexicon ids
    conn = connect()
    query = """
        SELECT value, variety, notation, phonemic, audio
          FROM pronunciations
         WHERE form_rowid = ?
    """
    rows: List[_Pronunciation] = conn.execute(query, (form_rowid,)).fetchall()
    return rows


def get_form_tags(form_rowid: int) -> List[_Tag]:
    # TODO: restrict by lexicon ids
    conn = connect()
    query = "SELECT tag, category FROM tags WHERE form_rowid = ?"
    rows: List[_Tag] = conn.execute(query, (form_rowid,)).fetchall()
    return rows


def get_sense_counts(sense_rowid: int, lexicon_rowids: Sequence[int]) -> List[_Count]:
    conn = connect()
    query = f"""
        SELECT count, rowid
          FROM counts
         WHERE sense_rowid = ?
           AND lexicon_rowid IN ({_qs(lexicon_rowids)})
    """
    rows: List[_Count] = conn.execute(query, (sense_rowid, *lexicon_rowids)).fetchall()
    return rows


def get_lexfile(synset_rowid: int) -> Optional[str]:
    conn = connect()
    query = """
        SELECT lf.name
          FROM lexfiles AS lf
          JOIN synsets AS ss ON ss.lexfile_rowid = lf.rowid
         WHERE ss.rowid = ?
    """
    row = conn.execute(query, (synset_rowid,)).fetchone()
    if row is not None and row[0] is not None:
        return row[0]
    return None


def _qs(xs: Collection) -> str:
    return ",".join("?" * len(xs))


def _vs(xs: Collection) -> str:
    return ",".join(["(?)"] * len(xs))


def _kws(xs: Collection) -> str:
    return ",".join(f":{x}" for x in xs)
