import random
import re

from sqlite_static_helper import *

_TABLE_HEADER_RE = re.compile(
  r'CREATE\s+(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?'
  r'(?:[\w."`]+\.)?([\w"`]+)\s*\(',
  re.I,
)

_CONSTRAINT_HEADS = {'PRIMARY', 'UNIQUE', 'CHECK', 'FOREIGN', 'CONSTRAINT'}


def _split_top_level_commas(body: str) -> list[str]:
  depth = 0
  cur: list[str] = []
  out: list[str] = []
  in_str = False
  quote = ''
  for ch in body:
    if in_str:
      cur.append(ch)
      if ch == quote:
        in_str = False
      continue
    if ch in ("'", '"', '`'):
      in_str = True
      quote = ch
      cur.append(ch)
      continue
    if ch == '(':
      depth += 1
      cur.append(ch)
    elif ch == ')':
      depth -= 1
      cur.append(ch)
    elif ch == ',' and depth == 0:
      out.append(''.join(cur))
      cur = []
    else:
      cur.append(ch)
  if cur:
    out.append(''.join(cur))
  return out


def get_schema(sql: str) -> dict[str, list[str]]:
  schema: dict[str, list[str]] = {}
  cursor = 0
  while True:
    m = _TABLE_HEADER_RE.search(sql, cursor)
    if not m:
      break
    name = m.group(1).strip('"`')
    start = m.end()
    depth = 1
    j = start
    while j < len(sql) and depth > 0:
      c = sql[j]
      if c == '(':
        depth += 1
      elif c == ')':
        depth -= 1
      j += 1
    body = sql[start:j - 1] if depth == 0 else sql[start:]
    cols: list[str] = []
    for piece in _split_top_level_commas(body):
      piece = piece.strip()
      if not piece:
        continue
      head = piece.split()[0].strip(' ,"`[]')
      if head.upper() in _CONSTRAINT_HEADS:
        continue
      if re.fullmatch(r'[A-Za-z_]\w*', head):
        cols.append(head)
    if cols:
      schema[name] = cols
    cursor = j
  return schema or {}


def _pick_table(sql: str) -> str:
  return random.choice(list(get_schema(sql).keys()))


def _pick_table_col(sql: str) -> tuple[str, str]:
  schema = get_schema(sql)
  table = random.choice(list(schema.keys()))
  cols = schema[table] or ['rowid']
  return table, random.choice(cols)


def mut_append_select_star(s: str) -> str:
  """Append a SELECT * statement for any existing table in the query"""
  return s + '\n' + f'SELECT * FROM {_pick_table(s)};'


def mut_append_count(s: str) -> str:
  """Append a SELECT COUNT(*) statement for any existing table in the query"""
  return s + '\n' + f'\nSELECT COUNT(*) FROM {_pick_table(s)};'


def mut_append_join(s: str) -> str:
  """Append a JOIN statement between two existing tables in the query"""

  schema = get_schema(s)
  if not schema:
    return s
  tables = list(schema.keys())
  t1 = random.choice(tables)
  t2 = random.choice(tables)
  join_type = random.choice(JOINS)
  if 'NATURAL' in join_type or 'CROSS' in join_type:
    return s + '\n' + f'SELECT * FROM {t1} {join_type} {t2};'
  return s + '\n' + f'SELECT * FROM {t1} AS a {join_type} {t2} AS b ON a.rowid = b.rowid;'


def mut_append_subquery(s: str) -> str:
  """Append a SELECT statement that queries a subquery"""

  table, col = _pick_table_col(s)
  style = random.choice([
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} t2 WHERE t2.{col} = {table}.{col});',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} t2 WHERE t2.{col} = {table}.{col});',
    f'SELECT * FROM {table} WHERE NOT EXISTS (SELECT 1 FROM {table} t2 WHERE t2.{col} = {table}.{col});',
    f'SELECT * FROM {table} WHERE {col} = (SELECT {col} FROM {table} t2 WHERE t2.{col} = {table}.{col} LIMIT 1);',
    f'SELECT {col}, (SELECT COUNT(*) FROM {table}) AS cnt FROM {table};',
    f'SELECT {col}, (SELECT MAX({col}) FROM {table}) AS max_val FROM {table};',
    f'SELECT {col}, (SELECT MIN({col}) FROM {table}) AS min_val FROM {table};',
    f'SELECT {col}, (SELECT AVG({col}) FROM {table}) AS avg_val FROM {table};',
    f'SELECT {col}, (SELECT SUM({col}) FROM {table}) AS sum_val FROM {table};',
    f'SELECT {col}, (SELECT {col} FROM {table} LIMIT 1) AS first_val FROM {table};',
    f'SELECT * FROM (SELECT * FROM {table}) AS sub;',
    f'SELECT * FROM (SELECT {col} FROM {table} WHERE {col} IS NOT NULL) AS sub;',
    f'SELECT * FROM (SELECT {col}, COUNT(*) FROM {table} GROUP BY {col}) AS sub;',
    f'SELECT * FROM (SELECT * FROM {table} ORDER BY {col}) AS sub LIMIT 10;',
    f'SELECT * FROM (SELECT * FROM {table}) AS t1 JOIN (SELECT * FROM {table}) AS t2 ON t1.{col} = t2.{col};',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE {col} > 0);',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE {col} < 0);',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE {col} = NULL);',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE 1 = 0);',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE 1 = 1);',
    f'SELECT * FROM {table} WHERE NOT EXISTS (SELECT 1 FROM {table} WHERE 1 = 0);',
    f'SELECT * FROM {table} WHERE NOT EXISTS (SELECT 1 FROM {table} WHERE 1 = 1);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE {col} IS NOT NULL);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE {col} IS NULL);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} LIMIT 1);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} LIMIT 0);',
    f'SELECT * FROM {table} WHERE {col} NOT IN (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} NOT IN (SELECT {col} FROM {table} WHERE {col} IS NOT NULL);',
    f'SELECT * FROM {table} WHERE {col} NOT IN (SELECT {col} FROM {table} WHERE {col} IS NULL);',
    f'SELECT * FROM {table} WHERE ({col}) IN (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE ({col}, {col}) IN (SELECT {col}, {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} > ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} < ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} = ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} >= ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} <= ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} <> ANY (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} > ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} < ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} = ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} >= ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} <= ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} <> ALL (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} > (SELECT AVG({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} < (SELECT AVG({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} > (SELECT MAX({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} < (SELECT MIN({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} = (SELECT COUNT(*) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} > (SELECT COUNT(*) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} = (SELECT SUM({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} = (SELECT GROUP_CONCAT({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE {col} IN (SELECT {col} FROM {table}));',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE EXISTS (SELECT 1 FROM {table}));',
    f'SELECT * FROM {table} WHERE {col} = (SELECT MAX({col}) FROM {table} WHERE {col} = (SELECT MAX({col}) FROM {table}));',
    f'SELECT * FROM (SELECT * FROM (SELECT * FROM {table}) AS inner1) AS inner2;',
    f'SELECT {col}, COUNT(*) FROM {table} GROUP BY {col} HAVING COUNT(*) > (SELECT COUNT(*) FROM {table} LIMIT 1);',
    f'SELECT {col}, COUNT(*) FROM {table} GROUP BY {col} HAVING COUNT(*) = (SELECT COUNT(*) FROM {table});',
    f'SELECT {col}, COUNT(*) FROM {table} GROUP BY {col} HAVING {col} IN (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} t1 JOIN {table} t2 ON t1.{col} = (SELECT {col} FROM {table} LIMIT 1);',
    f'SELECT * FROM {table} t1 LEFT JOIN {table} t2 ON t1.{col} = (SELECT {col} FROM {table} WHERE {col} = t1.{col});',
    f'SELECT * FROM {table} t1 RIGHT JOIN {table} t2 ON t1.{col} = (SELECT {col} FROM {table} WHERE {col} = t1.{col});',
    f'SELECT * FROM {table} t1 JOIN (SELECT * FROM {table}) AS sub ON t1.{col} = sub.{col};',
    f'SELECT {col} FROM {table} UNION SELECT {col} FROM (SELECT * FROM {table}) AS sub;',
    f'SELECT {col} FROM {table} INTERSECT SELECT {col} FROM (SELECT * FROM {table}) AS sub;',
    f'SELECT {col} FROM {table} EXCEPT SELECT {col} FROM (SELECT * FROM {table}) AS sub;',
    f'SELECT {col} FROM {table} UNION ALL SELECT {col} FROM (SELECT * FROM {table}) AS sub;',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table}) SELECT * FROM cte WHERE {col} IS NOT NULL;',
    f'WITH cte AS (SELECT {col}, COUNT(*) FROM {table} GROUP BY {col}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table}), cte2 AS (SELECT * FROM cte) SELECT * FROM cte2;',
    f'WITH RECURSIVE cte AS (SELECT {col} FROM {table} UNION ALL SELECT {col} FROM cte LIMIT 1) SELECT * FROM cte;',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE 1/0 = 0);'
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} LIMIT -1);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} LIMIT 0);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} ORDER BY {col} LIMIT 1 OFFSET 1000);',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT DISTINCT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT ALL {col} FROM {table});',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE {col} LIKE \"%%\");',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE {col} GLOB \"*\");',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} WHERE ROW_NUMBER() OVER (ORDER BY {col}) = 1);',
    f'SELECT * FROM {table} WHERE {col} = (SELECT {col} FROM {table} ORDER BY {col} LIMIT 1 OFFSET (SELECT COUNT(*) FROM {table} / 2));',
    f'SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table});',
    f'SELECT * FROM (SELECT * FROM {table}) AS sub;',
    f'SELECT {col} FROM {table} WHERE {col} = (SELECT MAX({col}) FROM {table});',
    f'SELECT * FROM {table} WHERE {col} NOT IN (SELECT {col} FROM {table});',
    f'SELECT * FROM {table} WHERE ({col}) IN (SELECT {col} FROM {table});',
  ])
  return s + '\n' + style


def mut_append_cte(s: str) -> str:
  """Append a SELECT statement that queries a CTE"""

  table, col = _pick_table_col(s)
  n = random.randint(2, 100)
  style = random.choice([
    f'WITH cte AS (SELECT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table} WHERE {col} IS NOT NULL) SELECT * FROM cte;',
    f'WITH cte AS (SELECT DISTINCT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col}, COUNT(*) AS cnt FROM {table} GROUP BY {col}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table} ORDER BY {col} LIMIT 5) SELECT * FROM cte;',
    f'WITH cte(x) AS (SELECT {col} FROM {table}) SELECT x FROM cte;',
    f'WITH cte(a, b) AS (SELECT {col}, {col} FROM {table}) SELECT a, b FROM cte;',
    f'WITH cte(a, b, c) AS (SELECT {col}, {col}, {col} FROM {table}) SELECT a, b, c FROM cte;',
    f'WITH cte(x) AS (VALUES(1),(2),(3)) SELECT * FROM cte;',
    f'WITH cte(x, y) AS (VALUES(1,2),(3,4),(5,6)) SELECT * FROM cte;',
    f'WITH a AS (SELECT * FROM {table}), b AS (SELECT * FROM a) SELECT * FROM b;',
    f'WITH a AS (SELECT {col} FROM {table}), b AS (SELECT {col} FROM a), c AS (SELECT {col} FROM b) SELECT * FROM c;',
    f'WITH a AS (SELECT {col} FROM {table}), b AS (SELECT {col} FROM {table}) SELECT * FROM a JOIN b ON a.{col} = b.{col};',
    f'WITH a AS (SELECT {col} FROM {table}), b AS (SELECT COUNT(*) AS cnt FROM a) SELECT * FROM b;',
    f'WITH a AS (SELECT * FROM {table} WHERE {col} IS NOT NULL), b AS (SELECT * FROM a WHERE {col} > 0), c AS (SELECT * FROM b LIMIT 10) SELECT * FROM c;',
    f'WITH a AS (SELECT * FROM {table}), b AS (SELECT * FROM {table}) SELECT * FROM a UNION ALL SELECT * FROM b;',
    f'WITH m AS MATERIALIZED (SELECT {col} FROM {table}) SELECT * FROM m;',
    f'WITH n AS NOT MATERIALIZED (SELECT {col} FROM {table}) SELECT * FROM n;',
    f'WITH m AS MATERIALIZED (SELECT * FROM {table}) SELECT m.{col} FROM m JOIN m AS m2 ON m.{col} = m2.{col};',
    f'WITH n AS NOT MATERIALIZED (SELECT * FROM {table}) SELECT * FROM n WHERE {col} > 0;',
    f'WITH a AS MATERIALIZED (SELECT * FROM {table}), b AS NOT MATERIALIZED (SELECT * FROM a) SELECT * FROM b;',
    f'WITH a AS NOT MATERIALIZED (SELECT * FROM {table}), b AS MATERIALIZED (SELECT * FROM a) SELECT * FROM b;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT * FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT SUM(x) FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT MAX(x) FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT COUNT(*) FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT x FROM cnt WHERE x > {n//2};',
    f'WITH RECURSIVE pair(a,b) AS (VALUES(0,1) UNION ALL SELECT b, a+b FROM pair WHERE b<100) SELECT * FROM pair;',
    f'WITH RECURSIVE fib(a,b) AS (VALUES(1,1) UNION ALL SELECT b, a+b FROM fib WHERE b<1000) SELECT a FROM fib;',
    f'WITH RECURSIVE fib(a,b,n) AS (VALUES(1,1,1) UNION ALL SELECT b, a+b, n+1 FROM fib WHERE n<{n}) SELECT a, n FROM fib;',
    f'WITH RECURSIVE tree(id, parent, depth) AS (VALUES(1, NULL, 0) UNION ALL SELECT id+1, id, depth+1 FROM tree WHERE depth<{n}) SELECT * FROM tree;',
    f'WITH RECURSIVE tree(id, path) AS (VALUES(1, "/1") UNION ALL SELECT id+1, path || "/" || (id+1) FROM tree WHERE id<{n}) SELECT * FROM tree;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION SELECT x+1 FROM cnt WHERE x<{n}) SELECT * FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT * FROM cnt;',
    f'WITH RECURSIVE vals(x) AS (SELECT {col} FROM {table} WHERE {col} IS NOT NULL UNION ALL SELECT {col} FROM {table} WHERE {col} IS NOT NULL LIMIT 10) SELECT * FROM vals;',
    f'WITH RECURSIVE r AS (SELECT {col} FROM {table} UNION ALL SELECT {col} FROM {table} LIMIT 5) SELECT * FROM r;',
    f'WITH RECURSIVE r AS (SELECT {col} FROM {table} WHERE {col} IS NOT NULL UNION ALL SELECT {col} FROM r WHERE {col} > 0 LIMIT 10) SELECT * FROM r;',
    f'WITH RECURSIVE r AS (SELECT * FROM {table} LIMIT 1 UNION ALL SELECT t.* FROM {table} t JOIN r ON t.{col} = r.{col} LIMIT 10) SELECT * FROM r;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<1) SELECT * FROM cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<0) SELECT * FROM cnt;',
    f'WITH RECURSIVE empty(x) AS (SELECT 1 WHERE 1=0 UNION ALL SELECT x+1 FROM empty WHERE x<10) SELECT * FROM empty;',
    f'WITH RECURSIVE inf(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM inf) SELECT * FROM inf LIMIT 10;',
    f'WITH RECURSIVE self(x) AS (SELECT 1 UNION ALL SELECT x FROM self) SELECT * FROM self LIMIT 5;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(0) UNION ALL SELECT x-1 FROM cnt WHERE x>-{n}) SELECT * FROM cnt;',
    f'WITH RECURSIVE multi(a, b, c) AS (VALUES(1, 2, 3) UNION ALL SELECT a+1, b+1, c+1 FROM multi WHERE a<{n}) SELECT * FROM multi;',
    f'WITH RECURSIVE multi(a, b) AS (VALUES(1, "x") UNION ALL SELECT a+1, "x" FROM multi WHERE a<{n}) SELECT * FROM multi;',
    f'WITH RECURSIVE multi(a, b) AS (VALUES(1, 1.0) UNION ALL SELECT a+1, b*0.5 FROM multi WHERE a<{n}) SELECT * FROM multi;',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte JOIN {table} ON cte.{col} = {table}.{col};',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte LEFT JOIN {table} ON cte.{col} = {table}.{col};',
    f'WITH cte1 AS (SELECT * FROM {table}), cte2 AS (SELECT * FROM {table}) SELECT * FROM cte1 LEFT JOIN cte2 ON cte1.{col} = cte2.{col};',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte c1 JOIN cte c2 ON c1.{col} = c2.{col};',
    f'WITH cte AS (SELECT {col}, COUNT(*) AS cnt FROM {table} GROUP BY {col}) SELECT * FROM cte WHERE cnt > 1;',
    f'WITH cte AS (SELECT {col}, SUM({col}) AS total FROM {table} GROUP BY {col}) SELECT * FROM cte ORDER BY total DESC;',
    f'WITH cte AS (SELECT {col}, AVG({col}) AS avg_val FROM {table} GROUP BY {col} HAVING AVG({col}) > 0) SELECT * FROM cte;',
    f'WITH cte AS (SELECT COUNT(*) AS cnt FROM {table}) SELECT * FROM cte WHERE cnt > 0;',
    f'WITH cte AS (SELECT {col}, ROW_NUMBER() OVER (ORDER BY {col}) AS rn FROM {table}) SELECT * FROM cte WHERE rn <= 5;',
    f'WITH cte AS (SELECT {col}, RANK() OVER (ORDER BY {col}) AS rnk FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col}, SUM({col}) OVER (ORDER BY {col}) AS running FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col}, LAG({col}) OVER (ORDER BY {col}) AS prev FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col}, LEAD({col}) OVER (ORDER BY {col}) AS nxt FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table} WHERE {col} IN (SELECT {col} FROM {table} LIMIT 5)) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table} WHERE {col} = (SELECT MAX({col}) FROM {table})) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table} WHERE EXISTS (SELECT 1 FROM {table} WHERE {col} > 0)) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table} UNION SELECT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table} INTERSECT SELECT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table} EXCEPT SELECT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table} UNION ALL SELECT {col} FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table}) UPDATE {table} SET {col} = (SELECT {col} FROM cte LIMIT 1) WHERE 1=0;',
    f'WITH cte AS (SELECT * FROM {table} LIMIT 1) DELETE FROM {table} WHERE {col} IN (SELECT {col} FROM cte) AND 1=0;',
    f'WITH cte AS (SELECT 1 AS x) INSERT INTO {table}({col}) SELECT x FROM cte WHERE 1=0;',
    f'WITH a AS (SELECT * FROM {table}), RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}) SELECT * FROM a CROSS JOIN cnt;',
    f'WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<{n}), filtered AS (SELECT * FROM cnt WHERE x > {n//2}) SELECT * FROM filtered;',
    f'WITH a AS (SELECT * FROM {table}), b AS (SELECT * FROM a), c AS (SELECT * FROM b), d AS (SELECT * FROM c) SELECT * FROM d;',
    f'WITH a AS (SELECT {col} FROM {table}), b AS (SELECT {col} FROM a WHERE {col} IS NOT NULL), c AS (SELECT COUNT(*) AS cnt FROM b) SELECT cnt FROM c;',
    f'WITH cte(x) AS (VALUES(1),(2),(3)) SELECT * FROM cte WHERE x > 1;',
    f'WITH cte(a, b) AS (VALUES(1, "a"),(2, "b"),(3, "c")) SELECT * FROM cte;',
    f'WITH cte(x) AS (VALUES(1),(2),(3)) SELECT * FROM {table} WHERE {col} IN (SELECT x FROM cte);',
    f'WITH cte AS (SELECT NULL AS x) SELECT * FROM cte;',
    f'WITH cte AS (SELECT NULL AS x, NULL AS y) SELECT x, y, x IS NULL FROM cte;',
    f'WITH cte AS (SELECT {col} FROM {table} WHERE {col} IS NULL) SELECT * FROM cte;',
    f'WITH cte(x) AS (VALUES(NULL),(1),(NULL)) SELECT * FROM cte WHERE x IS NOT NULL;',
    f'WITH cte AS (SELECT 1 AS x) SELECT x + 0.5 FROM cte;',
    f'WITH cte AS (SELECT "123" AS x) SELECT x + 0 FROM cte;',
    f'WITH cte AS (SELECT 1.5 AS x) SELECT CAST(x AS TEXT) FROM cte;',
    f'WITH cte AS (SELECT NULL AS x) SELECT COALESCE(x, 0) FROM cte;',
    f'WITH cte AS (SELECT CASE WHEN {col} > 0 THEN "pos" ELSE "neg" END AS sign FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT CASE WHEN {col} IS NULL THEN 0 ELSE {col} END AS val FROM {table}) SELECT * FROM cte;',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte a, cte b WHERE a.{col} = b.{col};',
    f'WITH cte AS (SELECT * FROM {table}) SELECT * FROM cte WHERE {col} > 0 UNION ALL SELECT * FROM cte WHERE {col} <= 0;',
    f'WITH cte AS (SELECT * FROM {table}) SELECT (SELECT COUNT(*) FROM cte) AS total, * FROM cte LIMIT 1;',
  ])

  return s + '\n' + style


def mut_append_window(s: str) -> str:
  """Append a SELECT statement that applies a window function to an existing table in the query"""

  table, col = _pick_table_col(s)
  fn = random.choice(WINDOW_FUNCS).format(c=col)
  partition = f'PARTITION BY {col} ' if random.random() < 0.5 else ''
  order = f'ORDER BY {col}'
  frame = random.choice([
    '',
    ' ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW',
    ' RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING',
    ' GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING',
    ' ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW',
    ' ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING EXCLUDE GROUP',
    ' ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE TIES',
    ' RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING EXCLUDE NO OTHERS',
  ])

  return s + '\n' + f'SELECT {fn} OVER ({partition}{order}{frame}) FROM {table};'


def mut_append_filter(s: str) -> str:
  """Append a SELECT statement that uses FILTER"""

  table, col = _pick_table_col(s)

  return s + '\n' + (f'SELECT '
                     f'COUNT(*) FILTER (WHERE {col} IS NOT NULL), '
                     f'SUM(rowid) FILTER (WHERE {col} > 0), '
                     f'COUNT(*) FILTER (WHERE 1=0), '
                     f'COUNT(*) FILTER (WHERE 1=1), '
                     f'COUNT(*) FILTER (WHERE NULL), '
                     f'AVG({col}) FILTER (WHERE {col} > 0 AND {col} < 100), '
                     f'COUNT(*) FILTER (WHERE typeof({col}) = "text") '
                     f'FROM {table};')


def mut_append_aggregate(s: str) -> str:
  """Append a SELECT statement that uses an aggregate function"""

  table, col = _pick_table_col(s)
  fn = random.choice(AGG_FUNCS)
  if fn == 'COUNT' and random.random() < 0.5:
    agg = 'COUNT(*)'
  elif fn == 'GROUP_CONCAT' and random.random() < 0.5:
    agg = f"GROUP_CONCAT({col}, '|')"
  elif fn == 'STRING_AGG' and random.random() < 0.5:
    agg = f"STRING_AGG({col}, '|')"
  else:
    agg = f"{fn}({col})"

  return s + '\n' + f'SELECT {agg} FROM {table};'


def mut_append_update(s: str) -> str:
  """Append an UPDATE statement"""

  table, col = _pick_table_col(s)
  val = random.choice([
    str(random.randint(-100, 100)),
    'NULL',
    "'x'",
    "''",
    f'{col} + 1',
    'CURRENT_TIMESTAMP',
    f"json_object('k', {col})",
  ])
  where = random.choice([
    '',
    f' WHERE {col} IS NOT NULL',
    ' WHERE rowid = 1',
    ' WHERE 1',
    f' WHERE {col} BETWEEN 0 AND 100',
  ])
  returning = ' RETURNING *' if random.random() < 0.4 else ''

  return s + '\n' + f'UPDATE {table} SET {col} = {val}{where}{returning};'


def mut_append_delete(s: str) -> str:
  """Append a DELETE statement"""

  table, col = _pick_table_col(s)
  where = random.choice([
    f'WHERE rowid = {random.randint(1, 100)}',
    f'WHERE {col} IS NULL',
    'WHERE 0',
    'WHERE 1',
    f'WHERE {col} > (SELECT AVG({col}) FROM {table})',
  ])
  returning = ' RETURNING *' if random.random() < 0.4 else ''

  return s + '\n' + f'DELETE FROM {table} {where}{returning};'


def mut_append_index(s: str) -> str:
  """Append a CREATE INDEX statement"""

  table, col = _pick_table_col(s)
  unique = random.choice(['', 'UNIQUE '])
  where = random.choice(['', f' WHERE {col} IS NOT NULL', f' WHERE {col} > 0'])
  expr = random.choice(
    [col, f'lower({col})', f'({col} + 1)', f'{col} COLLATE NOCASE'])
  suffix = random.randint(0, 9999)

  return s + '\n' + f'CREATE {unique} INDEX IF NOT EXISTS idx_{table}_{suffix} ON {table}({expr}){where};'


def mut_append_view(s: str) -> str:
  """Append a CREATE VIEW statement"""

  table, col = _pick_table_col(s)
  suffix = random.randint(0, 9999)
  temp = random.choice(['', 'TEMP ', 'TEMPORARY '])

  return s + '\n' + f'CREATE {temp} VIEW IF NOT EXISTS v_{table}_{suffix} AS SELECT {col} FROM {table};'


def mut_append_trigger(s: str) -> str:
  """Append a CREATE TRIGGER statement"""

  table, col = _pick_table_col(s)
  when = random.choice(['BEFORE', 'AFTER'])
  event = random.choice(['INSERT', 'UPDATE', 'DELETE', f'UPDATE OF {col}'])
  suffix = random.randint(0, 9999)
  body = random.choice([
    "SELECT RAISE(IGNORE)",
    "SELECT RAISE(FAIL, 'no')",
    "SELECT RAISE(ABORT, 'abort')",
    "SELECT RAISE(ROLLBACK, 'rb')",
    f"INSERT INTO {table}({col}) VALUES (NULL)",
  ])
  fer = ' FOR EACH ROW' if random.random() < 0.5 else ''

  return s + '\n' + f'CREATE TRIGGER IF NOT EXISTS trg_{table}_{suffix} {when} {event} ON {table}{fer} BEGIN {body}; END;'


def mut_wrap_savepoint(s: str) -> str:
  """Wrap the query inside SAVEPOINT-RELEASE/ROLLBACK"""

  name = f'sp{random.randint(0, 9999)}'
  end = random.choice([
    f'RELEASE {name};',
    f'ROLLBACK TO {name};\nRELEASE {name};',
    f'ROLLBACK TRANSACTION TO SAVEPOINT {name};\nRELEASE SAVEPOINT {name};',
  ])

  return f'SAVEPOINT {name};\n{s}\n{end}'


def mut_wrap_transaction(s: str) -> str:
  """Wrap the query inside a transaction"""

  begin = random.choice([
    "BEGIN", "BEGIN DEFERRED", "BEGIN IMMEDIATE", "BEGIN EXCLUSIVE",
    "BEGIN TRANSACTION", "BEGIN DEFERRED TRANSACTION",
    "BEGIN IMMEDIATE TRANSACTION", "BEGIN EXCLUSIVE TRANSACTION"
  ])
  end = random.choice(
    ['COMMIT;', 'ROLLBACK;', 'END;', 'END TRANSACTION;', 'COMMIT TRANSACTION;'])

  return f'{begin};\n{s}\n{end}'


def mut_wrap_attach_detach(s: str) -> str:
  """Wrap the query inside ATTACH and DETACH statements"""

  alias = f'aux{random.randint(0, 99)}'

  return (f"ATTACH DATABASE ':memory:' AS {alias};\n"
          f"{s}\n"
          f"DETACH DATABASE {alias};")


def mut_append_vacuum(s: str) -> str:
  """Append a VACUUM statement"""

  return s + '\n' + random.choice([
    'VACUUM;',
    'VACUUM main;',
  ])


def mut_append_analyze(s: str) -> str:
  """Append an ANALYZE statement"""

  table = _pick_table(s)

  return s + '\n' + random.choice([
    'ANALYZE;',
    f'ANALYZE {table};',
  ])


def mut_append_reindex(s: str) -> str:
  """Append a REINDEX statement"""

  table = _pick_table(s)

  return s + '\n' + random.choice([
    f'REINDEX {table};',
    'REINDEX;',
  ])


def mut_append_null_insert(s: str) -> str:
  """Append an INSERT statement with NULL"""

  schema = get_schema(s)
  table = random.choice(list(schema.keys()))
  cols = schema[table]

  if not cols:
    return s + '\n' + f'INSERT INTO {table} DEFAULT VALUES;'

  vals = ', '.join('NULL' for _ in cols)

  return s + '\n' + f'INSERT INTO {table} VALUES ({vals});'


def mut_append_insert_default(s: str) -> str:
  """Append an INSERT statement with DEFAULT VALUES"""
  return s + '\n' + f'INSERT INTO {_pick_table(s)} DEFAULT VALUES;'


def mut_append_insert_select(s: str) -> str:
  """Append an INSERT statement that duplicates all rows in the table"""

  table = _pick_table(s)

  return s + '\n' + f'INSERT INTO {table} SELECT * FROM {table};'


def mut_append_insert_or_x(s: str) -> str:
  """Append an INSERT OR ROLLBACK/ABORT/FAIL/IGNORE/REPLACE statement"""

  schema = get_schema(s)
  table = random.choice(list(schema.keys()))
  cols = schema[table] or ['rowid']
  vals = ', '.join(
    random.choice(['NULL', '0', "''", "'x'",
                   str(random.randint(-10, 10))]) for _ in cols)
  kind = random.choice(
    ['OR IGNORE', 'OR REPLACE', 'OR FAIL', 'OR ABORT', 'OR ROLLBACK'])

  return s + '\n' + f'INSERT {kind} INTO {table} VALUES ({vals});'


def mut_append_upsert(s: str) -> str:
  """Append an INSERT statement that updates the existing set if there is a conflict"""

  schema = get_schema(s)
  table = random.choice(list(schema.keys()))
  cols = schema[table]
  if not cols:
    return s
  target = cols[0]
  upd = ', '.join(f'{c} = excluded.{c}' for c in cols[:3])
  vals = ', '.join(random.choice(['NULL', '1', "'x'"]) for _ in cols)

  return s + '\n' + f'INSERT INTO {table} VALUES ({vals}) ON CONFLICT({target}) DO UPDATE SET {upd};'


def mut_append_alter_rename_table(s: str) -> str:
  """Append an ALTER statement that renames an existing table"""

  table = _pick_table(s)
  suffix = random.randint(0, 9999)

  return s + '\n' + f'ALTER TABLE {table} RENAME TO {table}_r{suffix};'


def mut_append_alter_rename_column(s: str) -> str:
  """Append an ALTER statement that renames a column of an existing table"""
  schema = get_schema(s)
  table = random.choice(list(schema.keys()))
  cols = schema[table]
  if not cols:
    return s
  col = random.choice(cols)
  suffix = random.randint(0, 9999)
  return s + '\n' + f'ALTER TABLE {table} RENAME COLUMN {col} TO {col}_r{suffix};'


def mut_append_alter_add_column(s: str) -> str:
  """Append an ALTER statement that adds a column to an existing table"""

  table = _pick_table(s)
  suffix = random.randint(0, 9999)
  type = random.choice(TYPES)
  extras = random.choice([
    '',
    ' DEFAULT NULL',
    " DEFAULT ''",
    ' DEFAULT 0',
    ' DEFAULT CURRENT_TIMESTAMP',
    ' COLLATE NOCASE',
    ' NOT NULL DEFAULT 0',
    ' UNIQUE',
    ' DEFAULT (random())',
    ' DEFAULT (abs(random()) % 1000)',
    ' COLLATE RTRIM',
  ])

  return s + '\n' + f'ALTER TABLE {table} ADD COLUMN extra_{suffix} {type}{extras};'


def mut_append_alter_drop_column(s: str) -> str:
  """Append an ALTER statement that drops a column from an existing table"""

  schema = get_schema(s)
  table = random.choice(list(schema.keys()))
  cols = schema[table]
  if not cols:
    return s
  col = random.choice(cols)

  return s + '\n' + f'ALTER TABLE {table} DROP COLUMN {col};'


def mut_append_alter_drop_table_index_view_trigger(s: str) -> str:
  """Append an ALTER statement that drops a table, index, view, or trigger"""

  schema = get_schema(s)
  if not schema:
    return s
  table = random.choice(list(schema.keys()))
  obj = random.choice(['TABLE', 'INDEX', 'VIEW', 'TRIGGER'])

  return s + '\n' + f'DROP {obj} IF EXISTS {table};'


def mut_repeat(s: str) -> str:
  """Repeat the query 1-10 times"""
  return f'{s}\n' * random.randint(1, 10)


def mut_prepend_random_pragma(s: str) -> str:
  """Prepend a PRAGMA statement"""
  return random.choice(PRAGMAS) + '\n' + s


def mut_append_random_pragma(s: str) -> str:
  """Append a PRAGMA statement"""
  return s + '\n' + random.choice(PRAGMAS)


def mut_prepend_random_meta_command(s: str) -> str:
  """Prepend a meta-command"""
  return random.choice(META_COMMANDS) + '\n' + s
