sqlproof 0.1.0a1__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.
Files changed (44) hide show
  1. sqlproof/__init__.py +32 -0
  2. sqlproof/_version.py +1 -0
  3. sqlproof/cli.py +151 -0
  4. sqlproof/client.py +159 -0
  5. sqlproof/config.py +42 -0
  6. sqlproof/contrib/__init__.py +3 -0
  7. sqlproof/contrib/supabase.py +136 -0
  8. sqlproof/core.py +344 -0
  9. sqlproof/coverage/__init__.py +6 -0
  10. sqlproof/coverage/diversity.py +11 -0
  11. sqlproof/coverage/plpgsql.py +5 -0
  12. sqlproof/coverage/schema_shape.py +7 -0
  13. sqlproof/exceptions.py +47 -0
  14. sqlproof/generators/__init__.py +21 -0
  15. sqlproof/generators/columns.py +93 -0
  16. sqlproof/generators/constraints.py +181 -0
  17. sqlproof/generators/functions.py +9 -0
  18. sqlproof/generators/graph.py +51 -0
  19. sqlproof/generators/rows.py +153 -0
  20. sqlproof/generators/sampling.py +15 -0
  21. sqlproof/generators/well_known.py +59 -0
  22. sqlproof/pytest_plugin.py +24 -0
  23. sqlproof/reporter/__init__.py +5 -0
  24. sqlproof/reporter/console.py +20 -0
  25. sqlproof/reporter/json_io.py +26 -0
  26. sqlproof/runners/__init__.py +14 -0
  27. sqlproof/runners/db.py +48 -0
  28. sqlproof/runners/migration.py +51 -0
  29. sqlproof/runners/overload.py +41 -0
  30. sqlproof/runners/property.py +119 -0
  31. sqlproof/runners/rls.py +40 -0
  32. sqlproof/runners/stateful.py +36 -0
  33. sqlproof/schema/__init__.py +27 -0
  34. sqlproof/schema/dependency_graph.py +38 -0
  35. sqlproof/schema/fingerprint.py +34 -0
  36. sqlproof/schema/introspect.py +229 -0
  37. sqlproof/schema/model.py +98 -0
  38. sqlproof/schema/parse_sql.py +206 -0
  39. sqlproof/testing.py +101 -0
  40. sqlproof/types.py +34 -0
  41. sqlproof-0.1.0a1.dist-info/METADATA +248 -0
  42. sqlproof-0.1.0a1.dist-info/RECORD +44 -0
  43. sqlproof-0.1.0a1.dist-info/WHEEL +4 -0
  44. sqlproof-0.1.0a1.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Literal, cast
5
+
6
+ from pglast import parse_sql as parse_postgres_sql
7
+ from pglast.enums import ConstrType
8
+ from pglast.stream import RawStream
9
+
10
+ from sqlproof.exceptions import SqlProofSchemaError
11
+ from sqlproof.schema.model import CheckConstraint, Column, ForeignKey, PgType, SchemaInfo, Table
12
+
13
+
14
+ def parse_schema_sql(sql: str, *, schema: str = "public") -> SchemaInfo:
15
+ try:
16
+ statements: tuple[Any, ...] = tuple(parse_postgres_sql(sql))
17
+ except Exception as exc:
18
+ raise SqlProofSchemaError(str(exc)) from exc
19
+
20
+ enums: list[PgType] = []
21
+ enum_names: dict[str, PgType] = {}
22
+ for raw_statement in statements:
23
+ statement: Any = raw_statement.stmt
24
+ if type(statement).__name__ == "CreateEnumStmt":
25
+ enum_schema, enum_name = _qualified_parts(statement.typeName, default_schema=schema)
26
+ enum = PgType(
27
+ kind="enum",
28
+ name=enum_name,
29
+ enum_values=tuple(_sval(value) for value in statement.vals),
30
+ )
31
+ enums.append(enum)
32
+ enum_names[enum_name] = enum
33
+ enum_names[f"{enum_schema}.{enum_name}"] = enum
34
+
35
+ tables = tuple(
36
+ _parse_table_statement(raw_statement.stmt, enum_names, schema)
37
+ for raw_statement in statements
38
+ if type(raw_statement.stmt).__name__ == "CreateStmt"
39
+ )
40
+ if not tables and "CREATE TABLE" in sql.upper():
41
+ raise SqlProofSchemaError("Could not parse CREATE TABLE statement.")
42
+ return SchemaInfo(tables=tables, enums=tuple(enums))
43
+
44
+
45
+ def _parse_table_statement(statement: Any, enum_names: dict[str, PgType], schema: str) -> Table:
46
+ relation = statement.relation
47
+ table_schema = relation.schemaname or schema
48
+ table_name = relation.relname
49
+ columns: list[Column] = []
50
+ primary_key: tuple[str, ...] = ()
51
+ foreign_keys: list[ForeignKey] = []
52
+ unique_constraints: list[tuple[str, ...]] = []
53
+ check_constraints: list[CheckConstraint] = []
54
+
55
+ for element in statement.tableElts or ():
56
+ if type(element).__name__ == "ColumnDef":
57
+ column = _parse_column(element, enum_names)
58
+ columns.append(column)
59
+ for constraint in element.constraints or ():
60
+ if constraint.contype == ConstrType.CONSTR_PRIMARY:
61
+ primary_key = (column.name,)
62
+ elif constraint.contype == ConstrType.CONSTR_UNIQUE:
63
+ unique_constraints.append((column.name,))
64
+ elif constraint.contype == ConstrType.CONSTR_FOREIGN:
65
+ foreign_keys.append(_parse_foreign_key(constraint, columns=(column.name,)))
66
+ elif constraint.contype == ConstrType.CONSTR_CHECK:
67
+ check_constraints.append(_parse_check(constraint))
68
+ continue
69
+ if element.contype == ConstrType.CONSTR_PRIMARY:
70
+ primary_key = _constraint_keys(element)
71
+ elif element.contype == ConstrType.CONSTR_UNIQUE:
72
+ unique_constraints.append(_constraint_keys(element))
73
+ elif element.contype == ConstrType.CONSTR_FOREIGN:
74
+ foreign_keys.append(_parse_foreign_key(element))
75
+ elif element.contype == ConstrType.CONSTR_CHECK:
76
+ check_constraints.append(_parse_check(element))
77
+
78
+ return Table(
79
+ schema=table_schema,
80
+ name=table_name,
81
+ columns=tuple(columns),
82
+ primary_key=primary_key,
83
+ foreign_keys=tuple(foreign_keys),
84
+ unique_constraints=tuple(unique_constraints),
85
+ check_constraints=tuple(check_constraints),
86
+ )
87
+
88
+
89
+ def _parse_column(column: Any, enum_names: dict[str, PgType]) -> Column:
90
+ constraints = tuple(column.constraints or ())
91
+ pg_type = _parse_type_node(column.typeName, enum_names)
92
+ identity = _identity_for_constraints(constraints)
93
+ primary = any(constraint.contype == ConstrType.CONSTR_PRIMARY for constraint in constraints)
94
+ not_null = (
95
+ column.is_not_null
96
+ or primary
97
+ or any(constraint.contype == ConstrType.CONSTR_NOTNULL for constraint in constraints)
98
+ )
99
+ return Column(
100
+ name=column.colname,
101
+ type=pg_type,
102
+ nullable=not not_null,
103
+ default=_default_for_constraints(constraints),
104
+ is_generated=pg_type.name in {"serial", "bigserial"} or identity is not None,
105
+ identity=identity,
106
+ )
107
+
108
+
109
+ def _parse_type_node(type_node: Any, enum_names: dict[str, PgType]) -> PgType:
110
+ parts = tuple(_sval(part) for part in type_node.names)
111
+ name = _normalize_type_name(".".join(parts))
112
+ if name in enum_names:
113
+ return enum_names[name]
114
+ unqualified = name.rsplit(".", 1)[-1]
115
+ if unqualified in enum_names:
116
+ return enum_names[unqualified]
117
+ modifiers = tuple(_const_int(modifier) for modifier in type_node.typmods or ())
118
+ return PgType(kind="scalar", name=unqualified, modifiers=modifiers)
119
+
120
+
121
+ def _parse_foreign_key(constraint: Any, *, columns: tuple[str, ...] | None = None) -> ForeignKey:
122
+ return ForeignKey(
123
+ columns=columns or tuple(_sval(value) for value in constraint.fk_attrs),
124
+ referenced_table=constraint.pktable.relname,
125
+ referenced_columns=tuple(_sval(value) for value in constraint.pk_attrs),
126
+ on_delete=_referential_action(constraint.fk_del_action),
127
+ on_update=_referential_action(constraint.fk_upd_action),
128
+ referenced_schema=getattr(constraint.pktable, "schemaname", None),
129
+ )
130
+
131
+
132
+ def _parse_check(constraint: Any) -> CheckConstraint:
133
+ return CheckConstraint(_render(constraint.raw_expr))
134
+
135
+
136
+ def _constraint_keys(constraint: Any) -> tuple[str, ...]:
137
+ return tuple(_sval(value) for value in constraint.keys)
138
+
139
+
140
+ def _default_for_constraints(constraints: tuple[Any, ...]) -> str | None:
141
+ for constraint in constraints:
142
+ if constraint.contype == ConstrType.CONSTR_DEFAULT:
143
+ return _render(constraint.raw_expr)
144
+ return None
145
+
146
+
147
+ def _identity_for_constraints(
148
+ constraints: tuple[Any, ...],
149
+ ) -> Literal["always", "by_default"] | None:
150
+ for constraint in constraints:
151
+ if constraint.contype == ConstrType.CONSTR_IDENTITY:
152
+ if constraint.generated_when == "a":
153
+ return "always"
154
+ if constraint.generated_when == "d":
155
+ return "by_default"
156
+ return None
157
+
158
+
159
+ def _referential_action(
160
+ action: str,
161
+ ) -> Literal["NO ACTION", "RESTRICT", "CASCADE", "SET NULL", "SET DEFAULT"]:
162
+ return cast(
163
+ Literal["NO ACTION", "RESTRICT", "CASCADE", "SET NULL", "SET DEFAULT"],
164
+ {
165
+ "a": "NO ACTION",
166
+ "r": "RESTRICT",
167
+ "c": "CASCADE",
168
+ "n": "SET NULL",
169
+ "d": "SET DEFAULT",
170
+ "\x00": "NO ACTION",
171
+ }.get(action, "NO ACTION"),
172
+ )
173
+
174
+
175
+ def _qualified_parts(parts: tuple[Any, ...], *, default_schema: str) -> tuple[str, str]:
176
+ values = tuple(_sval(part) for part in parts)
177
+ if len(values) == 1:
178
+ return default_schema, values[0]
179
+ return values[-2], values[-1]
180
+
181
+
182
+ def _sval(node: Any) -> str:
183
+ return str(node.sval)
184
+
185
+
186
+ def _const_int(node: Any) -> int:
187
+ return int(node.val.ival)
188
+
189
+
190
+ def _render(node: Any) -> str:
191
+ stream = RawStream() # type: ignore[no-untyped-call]
192
+ return str(stream(node))
193
+
194
+
195
+ def _normalize_type_name(name: str) -> str:
196
+ normalized = re.sub(r"\s+", " ", name.lower())
197
+ return {
198
+ "pg_catalog.int2": "smallint",
199
+ "pg_catalog.int4": "integer",
200
+ "pg_catalog.int8": "bigint",
201
+ "pg_catalog.float4": "real",
202
+ "pg_catalog.float8": "double precision",
203
+ "pg_catalog.bool": "boolean",
204
+ "pg_catalog.varchar": "varchar",
205
+ "pg_catalog.bpchar": "char",
206
+ }.get(normalized, normalized)
sqlproof/testing.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from contextlib import AbstractContextManager, ExitStack
5
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
6
+
7
+ from hypothesis import strategies as st
8
+ from hypothesis.stateful import RuleBasedStateMachine
9
+ from hypothesis.strategies import SearchStrategy
10
+
11
+ from sqlproof.exceptions import SqlProofUsageError
12
+ from sqlproof.generators.graph import Dataset, SizeSpec, dataset_strategy
13
+ from sqlproof.schema.model import Column, PgType, SchemaInfo, Table
14
+
15
+ if TYPE_CHECKING:
16
+ from sqlproof.client import SqlProofClient
17
+ from sqlproof.core import SqlProof
18
+
19
+ _T = TypeVar("_T")
20
+
21
+
22
+ class SqlProofStateMachine(RuleBasedStateMachine):
23
+ """Base class for Hypothesis stateful tests against a SqlProof database.
24
+
25
+ Each example leases an isolated client via `proof.client_for_dataset(...)`
26
+ so that writes from one example are rolled back before the next begins.
27
+ Subclasses define `@rule`s and `@invariant`s as usual, and override
28
+ `on_setup()` for per-example fixture creation. `self.db` is the live
29
+ `SqlProofClient`.
30
+
31
+ Run a machine with `SqlProof.run_state_machine(MyMachine, settings=...)`,
32
+ which binds the proof and dispatches to `run_state_machine_as_test`.
33
+ """
34
+
35
+ initial_dataset: ClassVar[dict[str, list[dict[str, Any]]]] = {}
36
+ _sqlproof_proof: ClassVar[SqlProof | None] = None
37
+
38
+ db: SqlProofClient
39
+
40
+ def __init__(self) -> None:
41
+ if self._sqlproof_proof is None:
42
+ msg = (
43
+ "SqlProofStateMachine cannot be instantiated directly. "
44
+ "Use SqlProof.run_state_machine(YourMachine) to run it."
45
+ )
46
+ raise SqlProofUsageError(msg)
47
+ super().__init__()
48
+ self._stack = ExitStack()
49
+ self.db = self._stack.enter_context(
50
+ self._sqlproof_proof.client_for_dataset(dict(self.initial_dataset))
51
+ )
52
+ self.on_setup()
53
+
54
+ def on_setup(self) -> None:
55
+ """Override to seed per-example fixtures. `self.db` is ready."""
56
+
57
+ def enter(self, cm: AbstractContextManager[_T]) -> _T:
58
+ """Enter `cm` and tie its lifetime to this example.
59
+
60
+ Use for resources that need to live across rules within an example
61
+ and be released between examples — JWT-claim contexts, savepoints,
62
+ mocked clocks, etc. The context manager is closed during `teardown`
63
+ in reverse-entry order.
64
+ """
65
+ return self._stack.enter_context(cm)
66
+
67
+ def teardown(self) -> None:
68
+ if hasattr(self, "_stack"):
69
+ self._stack.close()
70
+
71
+
72
+ def schemas(max_tables: int = 3, max_columns: int = 5) -> SearchStrategy[SchemaInfo]:
73
+ del max_columns
74
+ table_names = st.lists(
75
+ st.sampled_from(["users", "orders", "products", "scores", "events"]),
76
+ min_size=1,
77
+ max_size=max_tables,
78
+ unique=True,
79
+ )
80
+
81
+ def build(names: list[str]) -> SchemaInfo:
82
+ integer = PgType("scalar", "integer")
83
+ tables = tuple(
84
+ Table(
85
+ schema="public",
86
+ name=name,
87
+ columns=(Column("id", integer, False, None, False),),
88
+ primary_key=("id",),
89
+ foreign_keys=(),
90
+ unique_constraints=(),
91
+ check_constraints=(),
92
+ )
93
+ for name in names
94
+ )
95
+ return SchemaInfo(tables=tables)
96
+
97
+ return table_names.map(build)
98
+
99
+
100
+ def datasets_for(schema: SchemaInfo, sizes: Mapping[str, SizeSpec]) -> SearchStrategy[Dataset]:
101
+ return dataset_strategy(schema, sizes=sizes)
sqlproof/types.py ADDED
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from sqlproof.schema.parse_sql import parse_schema_sql
6
+
7
+
8
+ def generate_types(schema_file: Path, *, style: str = "typeddict") -> str:
9
+ schema = parse_schema_sql(schema_file.read_text(encoding="utf-8"))
10
+ lines = ["from __future__ import annotations"]
11
+ if style == "typeddict":
12
+ lines.append("from typing import TypedDict")
13
+ for table in schema.tables:
14
+ class_name = _class_name(table.name)
15
+ lines.extend(["", f"class {class_name}(TypedDict):"])
16
+ for column in table.columns:
17
+ lines.append(f" {column.name}: object")
18
+ elif style == "dataclass":
19
+ lines.append("from dataclasses import dataclass")
20
+ for table in schema.tables:
21
+ lines.extend(["", "@dataclass", f"class {_class_name(table.name)}:"])
22
+ for column in table.columns:
23
+ lines.append(f" {column.name}: object")
24
+ else:
25
+ lines.append("from pydantic import BaseModel")
26
+ for table in schema.tables:
27
+ lines.extend(["", f"class {_class_name(table.name)}(BaseModel):"])
28
+ for column in table.columns:
29
+ lines.append(f" {column.name}: object")
30
+ return "\n".join(lines) + "\n"
31
+
32
+
33
+ def _class_name(table_name: str) -> str:
34
+ return "".join(part.capitalize() for part in table_name.split("_")).removesuffix("s")
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlproof
3
+ Version: 0.1.0a1
4
+ Summary: Property-based testing for PostgreSQL schemas and SQL behavior. Early-stage alpha — APIs may change.
5
+ Project-URL: Homepage, https://sqlproof.com
6
+ Project-URL: Documentation, https://sqlproof.com
7
+ Project-URL: Repository, https://github.com/alialavia/sqlproof
8
+ Project-URL: Issues, https://github.com/alialavia/sqlproof/issues
9
+ Project-URL: Changelog, https://github.com/alialavia/sqlproof/blob/main/CHANGELOG.md
10
+ Author: SqlProof contributors
11
+ License: MIT
12
+ Keywords: hypothesis,postgres,postgresql,property-based-testing,rls,sql,testing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Hypothesis
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Database
24
+ Classifier: Topic :: Database :: Database Engines/Servers
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Classifier: Topic :: Software Development :: Testing
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: hypothesis>=6.100
30
+ Requires-Dist: pglast>=6.0
31
+ Requires-Dist: psycopg[binary]>=3.1
32
+ Requires-Dist: rich>=13.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: mutmut; extra == 'dev'
35
+ Requires-Dist: mypy; extra == 'dev'
36
+ Requires-Dist: pyright; extra == 'dev'
37
+ Requires-Dist: pytest-benchmark; extra == 'dev'
38
+ Requires-Dist: pytest-cov; extra == 'dev'
39
+ Requires-Dist: pytest-xdist; extra == 'dev'
40
+ Requires-Dist: pytest>=8.0; extra == 'dev'
41
+ Requires-Dist: ruff; extra == 'dev'
42
+ Requires-Dist: syrupy; extra == 'dev'
43
+ Requires-Dist: uv; extra == 'dev'
44
+ Provides-Extra: pydantic
45
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
46
+ Provides-Extra: testcontainers
47
+ Requires-Dist: testcontainers[postgres]>=4.0; extra == 'testcontainers'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # SqlProof
51
+
52
+ [![CI](https://github.com/alialavia/sqlproof/actions/workflows/ci.yml/badge.svg)](https://github.com/alialavia/sqlproof/actions/workflows/ci.yml)
53
+ [![codecov](https://codecov.io/gh/alialavia/sqlproof/branch/main/graph/badge.svg)](https://codecov.io/gh/alialavia/sqlproof)
54
+ [![PyPI](https://img.shields.io/pypi/v/sqlproof?include_prereleases)](https://pypi.org/project/sqlproof/)
55
+
56
+ **→ Full docs: [sqlproof.com](https://sqlproof.com)**
57
+
58
+ > ⚠️ **Early-stage alpha (`0.1.0a1`).** APIs are unstable and may change without
59
+ > deprecation warnings until 0.x stabilizes. Postgres edge cases and Hypothesis
60
+ > shrink behavior are still being discovered, and coverage of the schema surface
61
+ > area is incomplete. **Do not rely on this for production test suites yet.**
62
+ > Bug reports and reproductions welcome —
63
+ > [open an issue](https://github.com/alialavia/sqlproof/issues).
64
+
65
+ Property-based testing for PostgreSQL schemas and SQL behavior. Define properties about
66
+ your database code; SqlProof generates valid datasets with Hypothesis, executes your
67
+ queries through `psycopg`, and saves the smallest counterexample it finds.
68
+
69
+ ## Install
70
+
71
+ Alpha releases are gated behind a pre-release flag so you don't get one by accident:
72
+
73
+ ```bash
74
+ pip install --pre sqlproof
75
+ # or:
76
+ uv add --prerelease=allow sqlproof
77
+ ```
78
+
79
+ Requires Python 3.11+ and PostgreSQL 13+.
80
+
81
+ ## Quick Start
82
+
83
+ Given a schema file:
84
+
85
+ ```sql
86
+ -- schema.sql
87
+ CREATE TABLE orders (
88
+ id SERIAL PRIMARY KEY,
89
+ customer_id INTEGER NOT NULL,
90
+ total NUMERIC(10,2) NOT NULL CHECK (total >= 0)
91
+ );
92
+
93
+ CREATE TABLE line_items (
94
+ id SERIAL PRIMARY KEY,
95
+ order_id INTEGER NOT NULL REFERENCES orders(id),
96
+ quantity INTEGER NOT NULL CHECK (quantity > 0),
97
+ price NUMERIC(10,2) NOT NULL CHECK (price > 0)
98
+ );
99
+ ```
100
+
101
+ Write property tests with pytest:
102
+
103
+ ```python
104
+ from sqlproof import SqlProof, sqlproof
105
+
106
+ proof = SqlProof.from_schema_file("./schema.sql")
107
+
108
+
109
+ @sqlproof(proof, sizes={"orders": 20, "line_items": 50}, runs=50)
110
+ def test_no_orphan_line_items(db):
111
+ rows = db.query("""
112
+ SELECT li.id
113
+ FROM line_items li
114
+ LEFT JOIN orders o ON li.order_id = o.id
115
+ WHERE o.id IS NULL
116
+ """)
117
+ assert rows == []
118
+ ```
119
+
120
+ SqlProof will:
121
+
122
+ 1. Parse your schema (tables, columns, FKs, CHECK constraints, enums)
123
+ 2. Topologically order tables by foreign-key dependencies
124
+ 3. Generate datasets that respect common type, FK, CHECK, UNIQUE, and NOT NULL constraints
125
+ 4. Run your property with Hypothesis-managed execution and shrinking
126
+ 5. Save the shrunk counterexample as JSON when a property fails
127
+
128
+ ## API
129
+
130
+ See the full API reference at [sqlproof.com](https://sqlproof.com).
131
+
132
+ ### Quick reference
133
+
134
+ ```python
135
+ proof = SqlProof.from_schema_file("./schema.sql")
136
+ proof = SqlProof.from_connection_string("postgresql://localhost/postgres")
137
+
138
+ proof.check("name", sizes={"orders": 10}, property=lambda db: ...)
139
+ proof.invariant(
140
+ "no bad rows",
141
+ sizes={"orders": 10},
142
+ query="SELECT id FROM orders WHERE total < 0",
143
+ expect_empty=True,
144
+ )
145
+
146
+ proof.disconnect()
147
+ ```
148
+
149
+ ### Schema Sources
150
+
151
+ **SQL file** — SqlProof parses `CREATE TABLE`, `CREATE TYPE ... AS ENUM`, foreign keys, CHECK constraints, UNIQUE constraints, and column types directly from `.sql` files.
152
+
153
+ **Connection string** — Pass a `postgresql://` URL and SqlProof introspects the live database via `information_schema` and `pg_catalog`.
154
+
155
+ ```python
156
+ proof = SqlProof.from_connection_string("postgresql://localhost:5432/mydb")
157
+ ```
158
+
159
+ ### Custom Column Generators
160
+
161
+ SqlProof maps PostgreSQL types to Hypothesis strategies and refines simple range,
162
+ `IN (...)`, length, FK, and unique constraints. The public customization API is present
163
+ for v0.1 and will grow with richer per-column strategy overrides.
164
+
165
+ ### The `db` Client
166
+
167
+ The property function receives a `SqlProofClient`:
168
+
169
+ ```python
170
+ rows = db.query("SELECT id, total FROM orders WHERE total >= %s", 0)
171
+ total = db.scalar("SELECT count(*) FROM orders")
172
+ typed = db.query_typed("SELECT id, total FROM orders", OrderRow)
173
+ data = db.get_generated_data()
174
+ ```
175
+
176
+ - `query()` returns a list of dictionaries.
177
+ - `query_typed()` maps rows into `TypedDict`, dataclass, or Pydantic models.
178
+ - `get_generated_data()` returns the dataset for the current run.
179
+
180
+ ## Failure Output
181
+
182
+ When a property fails, SqlProof throws with a formatted counterexample:
183
+
184
+ ```text
185
+ Property failed: order totals match sum of line items
186
+ Failure: AssertionError: expected totals to match
187
+ Row context: {'order_id': 1}
188
+ Dataset shape: {'orders': {'rows': 1}, 'line_items': {'rows': 2}}
189
+ ```
190
+
191
+ Counterexamples are written under `.sqlproof/failures/` and can be inspected with:
192
+
193
+ ```bash
194
+ sqlproof report .sqlproof/failures/test_name.json
195
+ sqlproof report .sqlproof/failures/test_name.json --format json
196
+ sqlproof replay .sqlproof/failures/test_name.json
197
+ ```
198
+
199
+ ## How It Works
200
+
201
+ 1. **Schema parsing** — Reads your SQL file (or introspects a live DB) to extract tables, columns, types, foreign keys, CHECK/UNIQUE constraints, and enums
202
+
203
+ 2. **Dependency ordering** — Topologically sorts tables by foreign key references so parent rows are always inserted first
204
+
205
+ 3. **Data generation** — Maps PostgreSQL types to Hypothesis strategies and applies constraint-aware generation for CHECK, UNIQUE, NOT NULL, and FK constraints
206
+
207
+ 4. **Isolated execution** — Schema-file proofs run against an in-memory client for fast local feedback. DSN-backed proofs introspect PostgreSQL, insert generated data inside savepoints, run the property, then roll back the run.
208
+
209
+ 5. **Shrinking** — When a property fails, Hypothesis shrinks the dataset to find the simplest counterexample
210
+
211
+ ## Supported PostgreSQL Types
212
+
213
+ `integer`, `smallint`, `bigint`, `serial`, `bigserial`, `numeric(p,s)`, `real`, `double precision`, `boolean`, `text`, `varchar(n)`, `char(n)`, `uuid`, `timestamp`, `timestamptz`, `date`, `time`, `json`, `jsonb`, `bytea`, and custom `ENUM` types.
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ git clone https://github.com/your-org/sqlproof.git
219
+ cd sqlproof
220
+ uv sync --extra dev
221
+
222
+ uv run pytest
223
+ uv run ruff check src tests
224
+ uv run pyright src/sqlproof
225
+ uv run mypy src/sqlproof
226
+ ```
227
+
228
+ ### Postgres-backed tests
229
+
230
+ Integration tests are optional and read `SQLPROOF_TEST_DATABASE_URL`:
231
+
232
+ ```bash
233
+ SQLPROOF_TEST_DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:5432/postgres' uv run pytest tests/integration
234
+ uv run pytest tests/benchmarks
235
+ ```
236
+
237
+ The integration tests create a temporary schema named `sqlproof_it_*` and drop it at the end.
238
+
239
+ ### Why SqlProof tests itself with properties
240
+
241
+ SqlProof uses Hypothesis internally, and its own tests use properties for schema
242
+ fingerprinting, dependency ordering, FK validity, constraint generation, shrinking,
243
+ parser idempotence, and counterexample replay. This keeps the library honest about
244
+ the same invariants it asks users to write.
245
+
246
+ ## License
247
+
248
+ MIT
@@ -0,0 +1,44 @@
1
+ sqlproof/__init__.py,sha256=aweqOOTRvEusJ2wvHgojpnBWZtxrheQ39J1GkcfceGM,871
2
+ sqlproof/_version.py,sha256=lmvUfjeqbjbsnD2PLlUjZbqEGZrq6yUD6t2LZsM61ak,24
3
+ sqlproof/cli.py,sha256=24QggNSyi71nwrV_AJ6fTBoUSCbFud5FjhUZQi-W5N0,5516
4
+ sqlproof/client.py,sha256=5rZn2OTDfMMvcPGkLVaj5R_lD4rwunptUgpqqMakjU0,5604
5
+ sqlproof/config.py,sha256=ASE8RXF8bWeDdR8_ys1bBOdAbdqp8ItwsXP8iZnCj0I,1375
6
+ sqlproof/core.py,sha256=j_bV9bxwWlTKSwdjjJ5XnjG_1wEvlOReuMTXJUAAZW0,11789
7
+ sqlproof/exceptions.py,sha256=0lYb9pYsxtU6Or_JP2YYvLkbUJ024VeWMUiFtU2YiLw,1168
8
+ sqlproof/pytest_plugin.py,sha256=0MbhCh7Sl5Pn5eD3QTRTjlpsAICxXJaKUAq8NEtH1Ug,942
9
+ sqlproof/testing.py,sha256=X1FtKVMl_OILrQWlgUwpZ2aXVq56mF6Td7ZOW3xj6NQ,3551
10
+ sqlproof/types.py,sha256=-MXEhHjd0vqPnVi5QYOeIQmsVimQP2qV_vyRq0I09ww,1404
11
+ sqlproof/contrib/__init__.py,sha256=eEYYkxX5Hz9woXVOBJ2H2_CQoEih0vH6nRt3sH2Z8v8,49
12
+ sqlproof/contrib/supabase.py,sha256=KeSTJXOCW6BeQkvnY8J765ZOM0wQcVtyDZ6A4Nr4gXY,4344
13
+ sqlproof/coverage/__init__.py,sha256=Rdg4sB9kESdai9qXUJFzf4vl9Pl9EcwwwQoD09qgzIg,217
14
+ sqlproof/coverage/diversity.py,sha256=s15dkD5O7kJb-Vp0Chsnbd0lrrCwsEhoobrdbwtKHKc,331
15
+ sqlproof/coverage/plpgsql.py,sha256=0jUENbB-mwAKqbQE94oTPMFOZ0ujnXVIJ4Lgh5tq9ag,88
16
+ sqlproof/coverage/schema_shape.py,sha256=JTx2ZQL3SLgbs3aymV1DtCnna_8bfvZ8zn04Epnjbmg,236
17
+ sqlproof/generators/__init__.py,sha256=ak5kxCLHUn0V7dKnRUafPFBbhYpxRSQsi1wdkT1rUk0,595
18
+ sqlproof/generators/columns.py,sha256=Tk-3vlXL6-zNpEEc5t5JP_YkMwDNQHw6g_c5QIZZwKk,3189
19
+ sqlproof/generators/constraints.py,sha256=khuP9PTP25oTKsPN6gJKMk0m3rQ91jxHnopBnySrFF8,5635
20
+ sqlproof/generators/functions.py,sha256=XqvPJ97VuaRIu-ca4I1nm6dfUB3juWaY56FWR076Q1E,164
21
+ sqlproof/generators/graph.py,sha256=5WyX4MUt_zosLFUx6V0nkvtBVvGtXITGKAP8w7RrM9k,1549
22
+ sqlproof/generators/rows.py,sha256=CJgWlaV-nnt5l3twVj81ZrJ4yj7sptWdDVLcqCunwHk,5526
23
+ sqlproof/generators/sampling.py,sha256=D6xvP3txBy_4iPEblbSX_CYiMwxREjhFpBIGHaufpXY,400
24
+ sqlproof/generators/well_known.py,sha256=bYfcXD87-DUC0lxP64BmcA-0deribng55awp2b3pYuQ,2245
25
+ sqlproof/reporter/__init__.py,sha256=rKqWCGcE2XXz6DRLgO749jH7KKF6KmMIgACuijtcauE,131
26
+ sqlproof/reporter/console.py,sha256=_pP6oHHmeJikJqgHW_Hb8u-CS-rxAhkveZIgxJkC5rI,752
27
+ sqlproof/reporter/json_io.py,sha256=aSYNG8214flmspVJe-9D20S1x_6k3aCtO85OMXTnrkg,834
28
+ sqlproof/runners/__init__.py,sha256=q3c-lvqx9-GzqiJEGM-zKGTYCfyjqQY8DRK2Cre9ppY,616
29
+ sqlproof/runners/db.py,sha256=pzRTvBAwmeBIDyNQl5bzu2_IfGw05hyA18CuTqyY8Mc,1557
30
+ sqlproof/runners/migration.py,sha256=ryvlKcPG8CBfYbCcI3Y8A4ghi5sERFDY9MGjWWQea8o,1472
31
+ sqlproof/runners/overload.py,sha256=BSkkJUlGZzJBRJ3jrIOqS4jeygPBMvansSmHRdixlRE,1349
32
+ sqlproof/runners/property.py,sha256=REnMlKld5V8_c8oZTz2viVAbF0qSb4ODn6XTsXg7Ch8,3534
33
+ sqlproof/runners/rls.py,sha256=XnUFrMge_bVfun5K0ItvWi5URXpwLgUNG0H2VGK2pBE,1213
34
+ sqlproof/runners/stateful.py,sha256=9TqcCgbJGN0vf3FJql5aXkuW0z221D7LhYIb4MlQcBI,1103
35
+ sqlproof/schema/__init__.py,sha256=oNxJFeW6mq8ueeIV-S-J8I0NyDj7oeDCEXj-Y2kn-aI,533
36
+ sqlproof/schema/dependency_graph.py,sha256=EfEHJVLv0F2REZXkPi4FRW_C29QZGp_bTk3boaX9S6M,1377
37
+ sqlproof/schema/fingerprint.py,sha256=PKRCwnNXBz_E7-LeDrcm1TGmbAK9nuuL7xkLsripW6o,1111
38
+ sqlproof/schema/introspect.py,sha256=Iem32N_Ol_XM0yLjtFnJ6vE3D361V9RrQzLOjXoF4Hs,8968
39
+ sqlproof/schema/model.py,sha256=cEgdTdXpOjQqN9zU8XCf25p-cmxF_bvwLHaMVu6x9Sk,2738
40
+ sqlproof/schema/parse_sql.py,sha256=kvMZ5D3Y9z_qOTH9k197bmLwv80GLPHKkWt-dH-67JY,7695
41
+ sqlproof-0.1.0a1.dist-info/METADATA,sha256=Juw_FOQz4aOhGiNGyJcqM-412loodOMugl92zIEw5WI,8827
42
+ sqlproof-0.1.0a1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
43
+ sqlproof-0.1.0a1.dist-info/entry_points.txt,sha256=BLzK4-9qJ7cVkVwVE_eCQtBMTMSPmjxvQkFW8RAAli4,93
44
+ sqlproof-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ sqlproof = sqlproof.cli:main
3
+
4
+ [pytest11]
5
+ sqlproof = sqlproof.pytest_plugin