94 lines
3.2 KiB
Python
94 lines
3.2 KiB
Python
|
|
"""V1: apply_violet_ddl — splitting, one-POST-per-statement, idempotency."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from prod.clickhouse.violet.apply_violet_ddl import (
|
||
|
|
EXPECTED_TABLES,
|
||
|
|
SQL_DIR,
|
||
|
|
_strip_comments,
|
||
|
|
iter_statements,
|
||
|
|
main,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_statement_splitting_strips_comments_and_trailing_semis(tmp_path):
|
||
|
|
f = tmp_path / "01_x.sql"
|
||
|
|
f.write_text("-- a comment\nCREATE TABLE IF NOT EXISTS d.t (x Int8)\nENGINE=Memory;\n\n-- tail\n")
|
||
|
|
stmts = list(iter_statements(tmp_path))
|
||
|
|
assert len(stmts) == 1
|
||
|
|
fname, stmt = stmts[0]
|
||
|
|
assert fname == "01_x.sql"
|
||
|
|
assert "-- " not in stmt
|
||
|
|
assert not stmt.endswith(";")
|
||
|
|
|
||
|
|
|
||
|
|
def test_real_sql_dir_yields_one_create_per_file():
|
||
|
|
"""Every shipped .sql file must contain exactly ONE statement (the
|
||
|
|
one-POST-per-statement contract is enforceable only if files stay
|
||
|
|
single-statement)."""
|
||
|
|
by_file: dict[str, int] = {}
|
||
|
|
for fname, stmt in iter_statements(SQL_DIR):
|
||
|
|
by_file[fname] = by_file.get(fname, 0) + 1
|
||
|
|
assert stmt.upper().startswith(("CREATE TABLE IF NOT EXISTS", "CREATE DATABASE IF NOT EXISTS")), (
|
||
|
|
f"{fname}: non-idempotent or non-CREATE statement"
|
||
|
|
)
|
||
|
|
assert "dolphin_pink" not in stmt, f"{fname}: leftover dolphin_pink reference"
|
||
|
|
assert all(n == 1 for n in by_file.values()), f"multi-statement files: {by_file}"
|
||
|
|
# every expected table has a CREATE
|
||
|
|
created = {
|
||
|
|
stmt.split("dolphin_violet.")[1].split("\n")[0].split(" ")[0].split("(")[0].strip("`")
|
||
|
|
for _, stmt in iter_statements(SQL_DIR)
|
||
|
|
if "dolphin_violet." in stmt
|
||
|
|
}
|
||
|
|
assert EXPECTED_TABLES <= created, f"missing CREATEs: {EXPECTED_TABLES - created}"
|
||
|
|
|
||
|
|
|
||
|
|
def test_one_post_per_statement_and_double_apply_idempotent(monkeypatch):
|
||
|
|
posts: list[str] = []
|
||
|
|
|
||
|
|
def fake_apply(stmt, *, url, user, password):
|
||
|
|
posts.append(stmt)
|
||
|
|
|
||
|
|
import prod.clickhouse.violet.apply_violet_ddl as mod
|
||
|
|
|
||
|
|
monkeypatch.setattr(mod, "apply_statement", fake_apply)
|
||
|
|
monkeypatch.setattr(mod, "verify", lambda **kw: [])
|
||
|
|
rc1 = main(["--ch-url", "http://fake:0"])
|
||
|
|
n_first = len(posts)
|
||
|
|
rc2 = main(["--ch-url", "http://fake:0"])
|
||
|
|
assert rc1 == 0 and rc2 == 0
|
||
|
|
assert len(posts) == 2 * n_first # same statements re-posted
|
||
|
|
assert n_first == len(list(iter_statements(SQL_DIR)))
|
||
|
|
assert all(";" not in p for p in posts) # never multi-statement bodies
|
||
|
|
|
||
|
|
|
||
|
|
def test_verify_failure_path(monkeypatch):
|
||
|
|
import prod.clickhouse.violet.apply_violet_ddl as mod
|
||
|
|
|
||
|
|
monkeypatch.setattr(mod, "apply_statement", lambda *a, **k: None)
|
||
|
|
monkeypatch.setattr(mod, "verify", lambda **kw: ["violet_feed_divergence"])
|
||
|
|
assert main(["--ch-url", "http://fake:0"]) == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_dry_run_posts_nothing(monkeypatch, capsys):
|
||
|
|
import prod.clickhouse.violet.apply_violet_ddl as mod
|
||
|
|
|
||
|
|
def boom(*a, **k):
|
||
|
|
raise AssertionError("dry-run must not POST")
|
||
|
|
|
||
|
|
monkeypatch.setattr(mod, "apply_statement", boom)
|
||
|
|
assert main(["--dry-run", "--ch-url", "http://fake:0"]) == 0
|
||
|
|
out = capsys.readouterr().out
|
||
|
|
assert "[dry-run]" in out
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(pytest.main([__file__, "-v"]))
|