tesserakit-sql 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """Tessera sql pack."""
2
+
3
+ __version__ = "0.3.1"
tessera_sql/cli.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from tessera_core.models import RunContext
10
+
11
+ from tessera_sql.pack import SqlPack
12
+
13
+ console = Console()
14
+ sql_app = typer.Typer(help="Lint SQL files/migrations into a statement and table catalog.")
15
+
16
+
17
+ @sql_app.command("lint")
18
+ def lint_cmd(
19
+ input: Path = typer.Option(..., "--input", "-i", exists=True, readable=True, help="A .sql file or a directory of them."),
20
+ output: Path = typer.Option(Path("sql_pack"), "--output", "-o", help="Output directory."),
21
+ ) -> None:
22
+ """Parse and lint SQL; emit statement/table catalogs and findings."""
23
+ ctx = RunContext(job_name="sql", output_dir=output)
24
+ pack = SqlPack()
25
+ artifacts = pack.run(input_path=input, ctx=ctx, options={})
26
+
27
+ table = Table(title="SQL Pack Created")
28
+ table.add_column("Artifact")
29
+ table.add_column("Path")
30
+ table.add_column("Kind")
31
+ for art in artifacts:
32
+ table.add_row(art.name, str(art.path), art.kind)
33
+ console.print(table)
34
+
35
+ summary = Table(title="Run Summary")
36
+ summary.add_column("Metric")
37
+ summary.add_column("Value")
38
+ summary.add_row("run_id", ctx.run_id)
39
+ summary.add_row("statements", str(ctx.metadata.get("record_count", 0)))
40
+ summary.add_row("findings", str(ctx.metadata.get("finding_count", 0)))
41
+ console.print(summary)
42
+
43
+
44
+ def register(root_app: typer.Typer) -> None:
45
+ root_app.add_typer(sql_app, name="sql")
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from tessera_core.artifacts import write_jsonl, write_markdown
8
+ from tessera_core.models import Artifact, RunContext, ValidationFinding
9
+
10
+ from tessera_sql.loader import load_sql_records
11
+ from tessera_sql.schema import SqlStatement, SqlTable
12
+ from tessera_sql.validator import validate_sql_records
13
+
14
+
15
+ def load_records(input_path: Path, options: dict[str, Any]) -> list[SqlStatement]:
16
+ return load_sql_records(input_path, options)
17
+
18
+
19
+ def validate_records(statements: list[SqlStatement], options: dict[str, Any]) -> list[ValidationFinding]:
20
+ return validate_sql_records(statements, options)
21
+
22
+
23
+ def write_artifacts(statements: list[SqlStatement], ctx: RunContext, options: dict[str, Any]) -> list[Artifact]:
24
+ ctx.output_dir.mkdir(parents=True, exist_ok=True)
25
+ tables: list[SqlTable] = options.get("_tables", [])
26
+ findings: list[ValidationFinding] = ctx.metadata.get("findings") or validate_records(statements, options)
27
+
28
+ statements_jsonl = ctx.output_dir / "statements.jsonl"
29
+ tables_jsonl = ctx.output_dir / "tables.jsonl"
30
+ index_md = ctx.output_dir / "index.md"
31
+ validation_md = ctx.output_dir / "validation_report.md"
32
+ coverage_md = ctx.output_dir / "coverage_report.md"
33
+ tables_md = ctx.output_dir / "tables.md"
34
+
35
+ write_jsonl(statements_jsonl, [s.model_dump() for s in statements])
36
+ write_jsonl(tables_jsonl, [t.model_dump() for t in tables])
37
+ write_markdown(index_md, _render_index(statements, tables, options))
38
+ write_markdown(validation_md, _render_validation(statements, findings))
39
+ write_markdown(coverage_md, _render_coverage(statements))
40
+ write_markdown(tables_md, _render_tables(tables))
41
+
42
+ return [
43
+ Artifact(name="statements.jsonl", path=statements_jsonl, kind="jsonl"),
44
+ Artifact(name="tables.jsonl", path=tables_jsonl, kind="jsonl"),
45
+ Artifact(name="index.md", path=index_md, kind="markdown"),
46
+ Artifact(name="validation_report.md", path=validation_md, kind="markdown"),
47
+ Artifact(name="coverage_report.md", path=coverage_md, kind="markdown"),
48
+ Artifact(name="tables.md", path=tables_md, kind="markdown"),
49
+ ]
50
+
51
+
52
+ def _render_index(statements: list[SqlStatement], tables: list[SqlTable], options: dict[str, Any]) -> str:
53
+ lines = ["# SQL Catalog", ""]
54
+ lines.append(f"- Files: {options.get('_file_count', 0)}")
55
+ lines.append(f"- Statements: {len(statements)}")
56
+ lines.append(f"- Tables created: {len(tables)}")
57
+ lines.append("")
58
+ if not statements:
59
+ lines.append("_No statements found._")
60
+ return "\n".join(lines) + "\n"
61
+ lines.append("| Kind | Target | File:Line |")
62
+ lines.append("|---|---|---|")
63
+ for s in statements:
64
+ lines.append(f"| {s.kind} | {s.target or '-'} | `{s.file}:{s.lineno}` |")
65
+ return "\n".join(lines) + "\n"
66
+
67
+
68
+ def _render_validation(statements: list[SqlStatement], findings: list[ValidationFinding]) -> str:
69
+ lines = ["# Validation Report", ""]
70
+ lines.append(f"- Statements: {len(statements)}")
71
+ lines.append(f"- Findings: {len(findings)}")
72
+ lines.append("")
73
+ by_sev = Counter(f.severity for f in findings)
74
+ lines.append("## Severity Breakdown")
75
+ lines.append("")
76
+ for sev in ("error", "warning", "info"):
77
+ lines.append(f"- {sev}: {by_sev.get(sev, 0)}")
78
+ lines.append("")
79
+ if findings:
80
+ lines.append("## Findings")
81
+ lines.append("")
82
+ for f in findings[:200]:
83
+ lines.append(f"- **{f.severity.upper()}** `{f.code}`: {f.message}")
84
+ return "\n".join(lines)
85
+
86
+
87
+ def _render_coverage(statements: list[SqlStatement]) -> str:
88
+ lines = ["# Coverage Report", ""]
89
+ lines.append(f"- Statements: {len(statements)}")
90
+ if not statements:
91
+ return "\n".join(lines) + "\n"
92
+ kind_dist = Counter(s.kind for s in statements)
93
+ lines.append("")
94
+ lines.append("## Statement kinds")
95
+ lines.append("")
96
+ for kind, n in kind_dist.most_common():
97
+ lines.append(f"- `{kind}`: {n}")
98
+ return "\n".join(lines) + "\n"
99
+
100
+
101
+ def _render_tables(tables: list[SqlTable]) -> str:
102
+ lines = ["# Tables", ""]
103
+ lines.append(f"- Count: {len(tables)}")
104
+ lines.append("")
105
+ if not tables:
106
+ lines.append("_No CREATE TABLE statements found._")
107
+ return "\n".join(lines) + "\n"
108
+ for t in tables:
109
+ pk = "yes" if t.has_primary_key else "NO"
110
+ lines.append(f"## `{t.name}` (PK: {pk})")
111
+ lines.append("")
112
+ lines.append(f"- Source: `{t.file}:{t.lineno}`")
113
+ lines.append(f"- Columns ({len(t.columns)}): {', '.join(f'`{c}`' for c in t.columns) or '(none parsed)'}")
114
+ lines.append("")
115
+ return "\n".join(lines)
tessera_sql/loader.py ADDED
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from tessera_sql.parse import (
7
+ classify,
8
+ parse_create_table,
9
+ split_statements,
10
+ statement_flags,
11
+ )
12
+ from tessera_sql.schema import SqlStatement, SqlTable
13
+
14
+ _IGNORE = {
15
+ ".git", ".venv", "venv", "node_modules", "__pycache__", ".pytest_cache",
16
+ "dist", "build", ".tox", "target",
17
+ }
18
+
19
+
20
+ def discover_sql_files(root: Path) -> list[Path]:
21
+ if root.is_file():
22
+ return [root]
23
+ out: list[Path] = []
24
+ for p in sorted(root.rglob("*.sql")):
25
+ if any(part in _IGNORE for part in p.relative_to(root).parts):
26
+ continue
27
+ out.append(p)
28
+ return out
29
+
30
+
31
+ def load_sql_records(input_path: Path, options: dict[str, Any]) -> list[SqlStatement]:
32
+ """Parse SQL files into statements; stash discovered tables in options."""
33
+ root = input_path if input_path.is_dir() else input_path.parent
34
+ files = discover_sql_files(input_path if input_path.is_file() else root)
35
+
36
+ statements: list[SqlStatement] = []
37
+ tables: list[SqlTable] = []
38
+
39
+ for f in files:
40
+ try:
41
+ text = f.read_text(encoding="utf-8")
42
+ except (OSError, UnicodeDecodeError):
43
+ continue
44
+ rel = f.relative_to(root).as_posix() if f.is_relative_to(root) else f.name
45
+ for stmt_text, lineno in split_statements(text):
46
+ kind, target = classify(stmt_text)
47
+ flags = statement_flags(kind, stmt_text)
48
+ preview = " ".join(stmt_text.split())[:100]
49
+ statements.append(
50
+ SqlStatement(kind=kind, target=target, file=rel, lineno=lineno, preview=preview, flags=flags)
51
+ )
52
+ if kind == "create_table":
53
+ t = parse_create_table(stmt_text, target)
54
+ if t is not None:
55
+ t.file = rel
56
+ t.lineno = lineno
57
+ tables.append(t)
58
+
59
+ options["_tables"] = tables
60
+ options["_file_count"] = len(files)
61
+ options["_root"] = str(root)
62
+ return statements
tessera_sql/pack.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from tessera_core.jobpack import JobPack
7
+ from tessera_core.models import Artifact, RunContext, ValidationFinding
8
+
9
+ from tessera_sql.compiler import load_records, validate_records, write_artifacts
10
+
11
+
12
+ class SqlPack(JobPack):
13
+ name = "sql"
14
+ version = "0.3.1"
15
+
16
+ def normalize(self, input_path: Path, options: dict[str, Any]) -> list[Any]:
17
+ return load_records(input_path, options)
18
+
19
+ def validate(
20
+ self,
21
+ records: list[Any],
22
+ options: dict[str, Any],
23
+ ) -> list[ValidationFinding]:
24
+ return validate_records(records, options)
25
+
26
+ def generate(
27
+ self,
28
+ records: list[Any],
29
+ ctx: RunContext,
30
+ options: dict[str, Any],
31
+ ) -> list[Artifact]:
32
+ return write_artifacts(records, ctx, options)
33
+
34
+
35
+ def create_pack() -> SqlPack:
36
+ return SqlPack()
tessera_sql/parse.py ADDED
@@ -0,0 +1,156 @@
1
+ """Lightweight SQL parsing: strip comments, split statements, classify, extract.
2
+
3
+ Not a full SQL grammar. It strips comments, splits on top-level semicolons,
4
+ and uses keyword/regex heuristics to classify statements and pull out the
5
+ high-signal facts a migration reviewer cares about.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ from tessera_sql.schema import SqlStatement, SqlTable
13
+
14
+ _LINE_COMMENT = re.compile(r"--[^\n]*")
15
+ _BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
16
+ _IDENT = r'[`"\[]?([A-Za-z_][A-Za-z0-9_.$]*)[`"\]]?'
17
+
18
+
19
+ def strip_comments(sql: str) -> str:
20
+ sql = _BLOCK_COMMENT.sub(" ", sql)
21
+ sql = _LINE_COMMENT.sub("", sql)
22
+ return sql
23
+
24
+
25
+ def split_statements(sql: str) -> list[tuple[str, int]]:
26
+ """Split into (statement_text, line_number) on top-level semicolons.
27
+
28
+ Semicolons inside single/double quotes are ignored.
29
+ """
30
+ cleaned = strip_comments(sql)
31
+ statements: list[tuple[str, int]] = []
32
+ buf: list[str] = []
33
+ line = 1
34
+ start_line = 1
35
+ quote: str | None = None
36
+ for ch in cleaned:
37
+ if ch == "\n":
38
+ line += 1
39
+ if quote:
40
+ buf.append(ch)
41
+ if ch == quote:
42
+ quote = None
43
+ continue
44
+ if ch in ("'", '"'):
45
+ quote = ch
46
+ buf.append(ch)
47
+ continue
48
+ if ch == ";":
49
+ text = "".join(buf).strip()
50
+ if text:
51
+ statements.append((text, start_line))
52
+ buf = []
53
+ start_line = line
54
+ continue
55
+ if not buf and ch.strip() == "":
56
+ start_line = line
57
+ buf.append(ch)
58
+ tail = "".join(buf).strip()
59
+ if tail:
60
+ statements.append((tail, start_line))
61
+ return statements
62
+
63
+
64
+ def classify(stmt: str) -> tuple[str, str]:
65
+ """Return (kind, target_name)."""
66
+ s = stmt.lstrip()
67
+ low = s.lower()
68
+
69
+ def grab(pat: str) -> str:
70
+ m = re.search(pat, s, re.IGNORECASE)
71
+ return m.group(1) if m else ""
72
+
73
+ if low.startswith("create") and re.search(r"create\s+(temp\w*\s+)?table", low):
74
+ return "create_table", grab(rf"create\s+(?:temp\w*\s+)?table\s+(?:if\s+not\s+exists\s+)?{_IDENT}")
75
+ if low.startswith("create") and "index" in low.split("(")[0]:
76
+ return "create_index", grab(rf"index\s+(?:if\s+not\s+exists\s+)?{_IDENT}")
77
+ if low.startswith("alter"):
78
+ return "alter", grab(rf"alter\s+table\s+{_IDENT}")
79
+ if low.startswith("truncate"):
80
+ return "truncate", grab(rf"truncate\s+(?:table\s+)?{_IDENT}")
81
+ if low.startswith("drop"):
82
+ return "drop", grab(rf"drop\s+\w+\s+(?:if\s+exists\s+)?{_IDENT}")
83
+ if low.startswith("insert"):
84
+ return "insert", grab(rf"insert\s+into\s+{_IDENT}")
85
+ if low.startswith("update"):
86
+ return "update", grab(rf"update\s+{_IDENT}")
87
+ if low.startswith("delete"):
88
+ return "delete", grab(rf"delete\s+from\s+{_IDENT}")
89
+ if low.startswith("select") or low.startswith("with"):
90
+ return "select", ""
91
+ return "other", ""
92
+
93
+
94
+ def statement_flags(kind: str, stmt: str) -> dict:
95
+ low = stmt.lower()
96
+ flags: dict = {}
97
+ if kind in ("update", "delete"):
98
+ flags["has_where"] = bool(re.search(r"\bwhere\b", low))
99
+ if kind == "drop":
100
+ flags["if_exists"] = "if exists" in low
101
+ flags["drops_column"] = bool(re.search(r"\bdrop\s+column\b", low)) # only via ALTER, but guard anyway
102
+ if kind == "select":
103
+ # SELECT * (not count(*))
104
+ flags["select_star"] = bool(re.search(r"select\s+\*", low))
105
+ if kind == "create_table":
106
+ flags["if_not_exists"] = "if not exists" in low
107
+ if kind == "alter":
108
+ adds_col = bool(re.search(r"\badd\s+(column\s+)?", low))
109
+ flags["adds_column"] = adds_col
110
+ flags["drops_column"] = bool(re.search(r"\bdrop\s+(column\s+)?", low))
111
+ flags["renames"] = bool(re.search(r"\brename\b", low))
112
+ # locking risk: ADD COLUMN ... NOT NULL without a DEFAULT rewrites the table
113
+ if adds_col and re.search(r"\bnot\s+null\b", low) and not re.search(r"\bdefault\b", low):
114
+ flags["add_not_null_without_default"] = True
115
+ return flags
116
+
117
+
118
+ def parse_create_table(stmt: str, target: str) -> SqlTable | None:
119
+ m = re.search(r"\((.*)\)", stmt, re.DOTALL)
120
+ if not m:
121
+ return SqlTable(name=target, columns=[], has_primary_key=False)
122
+ body = m.group(1)
123
+ columns: list[str] = []
124
+ has_pk = bool(re.search(r"primary\s+key", body, re.IGNORECASE))
125
+ for part in _split_top_level(body):
126
+ p = part.strip()
127
+ if not p:
128
+ continue
129
+ low = p.lower()
130
+ if low.startswith(("primary key", "foreign key", "unique", "constraint", "check", "index", "key ")):
131
+ continue
132
+ m2 = re.match(_IDENT, p)
133
+ if m2:
134
+ columns.append(m2.group(1))
135
+ if "primary key" in low:
136
+ has_pk = True
137
+ return SqlTable(name=target, columns=columns, has_primary_key=has_pk)
138
+
139
+
140
+ def _split_top_level(body: str) -> list[str]:
141
+ parts: list[str] = []
142
+ depth = 0
143
+ buf: list[str] = []
144
+ for ch in body:
145
+ if ch == "(":
146
+ depth += 1
147
+ elif ch == ")":
148
+ depth -= 1
149
+ if ch == "," and depth == 0:
150
+ parts.append("".join(buf))
151
+ buf = []
152
+ else:
153
+ buf.append(ch)
154
+ if buf:
155
+ parts.append("".join(buf))
156
+ return parts
tessera_sql/schema.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SqlStatement(BaseModel):
9
+ """One SQL statement. Serialized to ``statements.jsonl``."""
10
+
11
+ kind: str # create_table / create_index / alter / drop / select / insert / update / delete / other
12
+ target: str = "" # table/index name when determinable
13
+ file: str = ""
14
+ lineno: int = 0
15
+ preview: str = "" # first ~100 chars, comments stripped
16
+ flags: dict[str, Any] = Field(default_factory=dict) # parser observations (has_where, if_exists, select_star, ...)
17
+
18
+
19
+ class SqlTable(BaseModel):
20
+ """A table declared by a CREATE TABLE. Serialized to ``tables.jsonl``."""
21
+
22
+ name: str
23
+ columns: list[str] = Field(default_factory=list)
24
+ has_primary_key: bool = False
25
+ file: str = ""
26
+ lineno: int = 0
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tessera_core.models import ValidationFinding
6
+
7
+ from tessera_sql.schema import SqlStatement, SqlTable
8
+
9
+
10
+ def validate_sql_records(statements: list[SqlStatement], options: dict[str, Any]) -> list[ValidationFinding]:
11
+ findings: list[ValidationFinding] = []
12
+
13
+ if not statements:
14
+ findings.append(ValidationFinding(severity="info", code="no_statements",
15
+ message="no SQL statements found", field=None))
16
+ return findings
17
+
18
+ for s in statements:
19
+ loc = f"{s.file}:{s.lineno}"
20
+
21
+ def f(severity: str, code: str, message: str) -> ValidationFinding:
22
+ return ValidationFinding(severity=severity, code=code, message=message,
23
+ field="sql", metadata={"file": s.file, "lineno": s.lineno, "kind": s.kind})
24
+
25
+ if s.kind == "delete" and s.flags.get("has_where") is False:
26
+ findings.append(f("error", "delete_without_where",
27
+ f"{loc}: DELETE without WHERE removes every row"))
28
+ if s.kind == "update" and s.flags.get("has_where") is False:
29
+ findings.append(f("warning", "update_without_where",
30
+ f"{loc}: UPDATE without WHERE writes every row"))
31
+ if s.kind == "drop" and not s.flags.get("if_exists"):
32
+ findings.append(f("warning", "drop_without_if_exists",
33
+ f"{loc}: DROP without IF EXISTS fails if the object is absent"))
34
+ if s.kind == "select" and s.flags.get("select_star"):
35
+ findings.append(f("info", "select_star",
36
+ f"{loc}: SELECT * couples the query to column order/shape"))
37
+
38
+ # --- migration-safety rules (the costly, easy-to-miss ones) ---
39
+ if s.kind == "truncate":
40
+ findings.append(f("warning", "truncate_table",
41
+ f"{loc}: TRUNCATE removes all rows and is often non-transactional / non-reversible"))
42
+ if s.kind == "alter" and s.flags.get("add_not_null_without_default"):
43
+ findings.append(f("error", "add_not_null_without_default",
44
+ f"{loc}: ADD COLUMN NOT NULL without DEFAULT rewrites the table and fails on existing rows"))
45
+ if s.kind == "alter" and s.flags.get("drops_column"):
46
+ findings.append(f("warning", "drop_column",
47
+ f"{loc}: dropping a column is destructive and irreversible; ensure no code still reads it"))
48
+ if s.kind == "alter" and s.flags.get("renames"):
49
+ findings.append(f("warning", "rename_breaks_compatibility",
50
+ f"{loc}: RENAME breaks any code/queries referencing the old name; prefer add-new + backfill + drop-old"))
51
+ if s.kind == "create_table" and not s.flags.get("if_not_exists"):
52
+ findings.append(f("info", "create_table_without_if_not_exists",
53
+ f"{loc}: CREATE TABLE without IF NOT EXISTS is not idempotent if the migration re-runs"))
54
+
55
+ tables: list[SqlTable] = options.get("_tables", [])
56
+ for t in tables:
57
+ if not t.has_primary_key:
58
+ findings.append(
59
+ ValidationFinding(
60
+ severity="warning", code="table_without_primary_key",
61
+ message=f"{t.file}:{t.lineno}: table `{t.name}` has no PRIMARY KEY",
62
+ field="sql", metadata={"table": t.name, "file": t.file, "lineno": t.lineno},
63
+ )
64
+ )
65
+
66
+ return findings
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: tesserakit-sql
3
+ Version: 0.4.0
4
+ Summary: SQL job pack for Tessera: lint SQL files/migrations into a statement and table catalog.
5
+ Project-URL: Homepage, https://github.com/ShaileshRawat1403/tessera
6
+ Project-URL: Repository, https://github.com/ShaileshRawat1403/tessera
7
+ Project-URL: Issues, https://github.com/ShaileshRawat1403/tessera/issues
8
+ Author: Shailesh Rawat
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: pydantic>=2.7
15
+ Requires-Dist: rich>=13.7
16
+ Requires-Dist: tesserakit-core>=0.1.0
17
+ Requires-Dist: typer>=0.12
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # tesserakit-sql
23
+
24
+ Lint SQL files and migrations into a statement and table catalog.
25
+
26
+ `tessera-sql` parses `.sql` files with lightweight heuristics (no database connection, no execution), builds a catalog of statements and declared tables, and flags high-signal migration-safety issues.
27
+
28
+ ## Lint SQL
29
+
30
+ ```bash
31
+ tessera sql lint --input migrations/ --output ./out/sql_pack
32
+ tessera sql lint --input schema.sql --output ./out/sql_pack
33
+ ```
34
+
35
+ Artifacts written:
36
+
37
+ ```text
38
+ statements.jsonl one SqlStatement per parsed statement (kind, target, flags)
39
+ tables.jsonl one SqlTable per CREATE TABLE (columns, primary-key flag)
40
+ index.md statement catalog
41
+ validation_report.md safety findings
42
+ coverage_report.md statement-kind distribution
43
+ tables.md table catalog with columns and PK status
44
+ ```
45
+
46
+ ## Lint rules
47
+
48
+ Query safety:
49
+
50
+ - `delete_without_where` (error) — `DELETE` with no `WHERE` removes every row
51
+ - `update_without_where` (warning) — `UPDATE` with no `WHERE` writes every row
52
+ - `select_star` (info) — `SELECT *` couples the query to column shape
53
+
54
+ Migration safety (the costly, easy-to-miss class):
55
+
56
+ - `add_not_null_without_default` (error) — `ALTER TABLE ... ADD COLUMN ... NOT NULL` with no `DEFAULT` rewrites the table and fails on existing rows
57
+ - `truncate_table` (warning) — `TRUNCATE` wipes all rows and is often non-transactional / irreversible
58
+ - `drop_column` (warning) — dropping a column is destructive and irreversible
59
+ - `rename_breaks_compatibility` (warning) — `RENAME` breaks code referencing the old name; prefer add-new + backfill + drop-old
60
+ - `drop_without_if_exists` (warning) — `DROP` without `IF EXISTS` fails if the object is absent
61
+ - `create_table_without_if_not_exists` (info) — non-idempotent if the migration re-runs
62
+
63
+ Schema:
64
+
65
+ - `table_without_primary_key` (warning) — a `CREATE TABLE` declares no `PRIMARY KEY`
66
+ - `no_statements` — nothing parsed
67
+
68
+ ## Limitations (v0.1)
69
+
70
+ Parsing is heuristic: comments are stripped, statements are split on top-level
71
+ semicolons (quote-aware), and classification is keyword/regex based. It is tuned
72
+ for migration and schema files, not for validating arbitrary vendor SQL dialects.
@@ -0,0 +1,12 @@
1
+ tessera_sql/__init__.py,sha256=69lvBTb7J-ehoWNfyemfNTQlneuXZXLzjLGMnqHXC70,47
2
+ tessera_sql/cli.py,sha256=hThCGPFana_Fjkz-7UJfb77BETSFm352QadvPItPjcI,1482
3
+ tessera_sql/compiler.py,sha256=EV24jVY7r4aNXpA5t8pv3T4zTdL7qYyxDs03g7NlZac,4718
4
+ tessera_sql/loader.py,sha256=FG6SLPO6-i6N905kS49rMT4a-FMyr_nM_3JpyvflrKM,2032
5
+ tessera_sql/pack.py,sha256=kU9QESCKawnIcaiz-3nHnwbqXr4M6RtT32vtCeBbzAc,905
6
+ tessera_sql/parse.py,sha256=L2VTFCbgOvWck9pDinS1MLP_zBw7yYMgiauGBpdq028,5355
7
+ tessera_sql/schema.py,sha256=2qeyOj1vg9RPq3gxNLybQqgsFtZ-fNj2U3ggdZdFZkU,837
8
+ tessera_sql/validator.py,sha256=LPXDBpwllPHzhCXILcAB4XIm2A18rRo6pkQIUFOFfZQ,3554
9
+ tesserakit_sql-0.4.0.dist-info/METADATA,sha256=aDOjOeJUXeC5Mnf8z5qMWy_JCIetTXT-Grre33onsXc,3016
10
+ tesserakit_sql-0.4.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ tesserakit_sql-0.4.0.dist-info/entry_points.txt,sha256=2s78QyHRF-WVtOIpKaNHsacV_1RMC8ft-eNuGBoSeKA,105
12
+ tesserakit_sql-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [tessera.commands]
2
+ sql = tessera_sql.cli:register
3
+
4
+ [tessera.jobpacks]
5
+ sql = tessera_sql.pack:create_pack