Development setup¶
Repository setup¶
git clone git@github.com:blu3pr1n7/pgcarter.git
cd pgcarter
uv venv --python 3.12
uv pip install -e ".[dev]"
The Makefile wraps the common workflows:
make dev # install with dev dependencies
make test # unit tests (no database needed)
make lint # ruff
make typecheck # mypy
make coverage # unit tests with coverage
Running tests¶
Integration tests (dockerised database)¶
A disposable PostgreSQL 16 instance is provided via docker-compose.yml (host
port 55432, isolated from any local Postgres):
make db-up # start the test DB and wait until healthy
make test-integration # run integration tests against it
make e2e # run the index CLI end-to-end into ./build/e2e
make e2e-analyse # run an online analysis into ./build/analysis
make test-all # unit + integration
make db-down # tear down (removes the data)
Linting & formatting¶
Style: line length 100, ruff rule sets E, F, I, UP, B. All public
functions are typed (mypy runs with disallow_untyped_defs).
Building the docs site locally¶
uv pip install -e ".[docs]" # or: make docs
mkdocs serve # live-reload preview at http://127.0.0.1:8000
mkdocs build --strict # production build (fails on warnings/broken links)
Extending pgcarter¶
Adding a new extractor¶
- Add a metadata dataclass to
pgcarter/models/__init__.py(the single source of truth shared by SQL generation, JSON, and templates). - Create
pgcarter/extractor/<asset>.pysubclassingExtractor, querying the catalogs and returning your model. - Wire it into
InventoryExtractor.extract()inpgcarter/extractor/__init__.py(wrap the call in_safe(...)so a failure is recorded, not fatal). - Emit it from the writers (
pgcarter/writers/) and add a template if it should appear in docs. - Add unit tests using the
FakeDBpattern intests/test_extractors.py.
Adding a new analysis rule¶
Checks are plugin-style — adding one requires only a new class:
from pgcarter.analyzer.models import WARNING, CheckResult
from pgcarter.analyzer.rules import ColumnCheck, register
@register
class MyCheck(ColumnCheck):
name = "my_check"
category = "column"
def applies(self, asset, ctx) -> bool:
table, column = asset
return column.nullable
def execute(self, asset, ctx) -> list[CheckResult]:
table, column = asset
return [self.result(severity=WARNING, message="...", table=table.qualified_name,
column=column.name)]
Subclass TableCheck, ColumnCheck, or DatabaseCheck, decorate with
@register, and the engine discovers it automatically. Any database access must
go through ctx.run(sql), which validates the statement as read-only. Add tests
in tests/test_analyzer.py.