driftbrake 0.0.1__tar.gz
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.
- driftbrake-0.0.1/.gitignore +40 -0
- driftbrake-0.0.1/LICENSE +21 -0
- driftbrake-0.0.1/PKG-INFO +88 -0
- driftbrake-0.0.1/README.md +48 -0
- driftbrake-0.0.1/driftbrake.example.yml +51 -0
- driftbrake-0.0.1/pyproject.toml +119 -0
- driftbrake-0.0.1/src/driftbrake/__init__.py +64 -0
- driftbrake-0.0.1/src/driftbrake/classifiers/__init__.py +4 -0
- driftbrake-0.0.1/src/driftbrake/classifiers/impact_classifier.py +107 -0
- driftbrake-0.0.1/src/driftbrake/classifiers/type_compatibility.py +117 -0
- driftbrake-0.0.1/src/driftbrake/cli.py +328 -0
- driftbrake-0.0.1/src/driftbrake/comparators/__init__.py +3 -0
- driftbrake-0.0.1/src/driftbrake/comparators/schema_comparator.py +449 -0
- driftbrake-0.0.1/src/driftbrake/config/__init__.py +3 -0
- driftbrake-0.0.1/src/driftbrake/config/settings.py +133 -0
- driftbrake-0.0.1/src/driftbrake/contracts/__init__.py +6 -0
- driftbrake-0.0.1/src/driftbrake/contracts/loader.py +69 -0
- driftbrake-0.0.1/src/driftbrake/contracts/writer.py +56 -0
- driftbrake-0.0.1/src/driftbrake/exceptions.py +20 -0
- driftbrake-0.0.1/src/driftbrake/guard.py +194 -0
- driftbrake-0.0.1/src/driftbrake/models.py +229 -0
- driftbrake-0.0.1/src/driftbrake/readers/__init__.py +7 -0
- driftbrake-0.0.1/src/driftbrake/readers/base.py +19 -0
- driftbrake-0.0.1/src/driftbrake/readers/json_reader.py +84 -0
- driftbrake-0.0.1/src/driftbrake/readers/postgres.py +181 -0
- driftbrake-0.0.1/src/driftbrake/reporters/__init__.py +8 -0
- driftbrake-0.0.1/src/driftbrake/reporters/html_report.py +213 -0
- driftbrake-0.0.1/src/driftbrake/reporters/json_report.py +29 -0
- driftbrake-0.0.1/src/driftbrake/reporters/markdown_report.py +127 -0
- driftbrake-0.0.1/src/driftbrake/reporters/terminal.py +171 -0
- driftbrake-0.0.1/templates/base.html +306 -0
- driftbrake-0.0.1/templates/secao_breaking.html +21 -0
- driftbrake-0.0.1/templates/secao_safe.html +17 -0
- driftbrake-0.0.1/templates/secao_warning.html +21 -0
- driftbrake-0.0.1/templates/tabela.html +11 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
.env
|
|
11
|
+
.venv
|
|
12
|
+
venv/
|
|
13
|
+
ENV/
|
|
14
|
+
|
|
15
|
+
*.env
|
|
16
|
+
*.db
|
|
17
|
+
*.sqlite
|
|
18
|
+
credentials.json
|
|
19
|
+
secrets.json
|
|
20
|
+
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
*.log
|
|
29
|
+
logs/
|
|
30
|
+
|
|
31
|
+
*.tmp
|
|
32
|
+
temp/
|
|
33
|
+
tmp/
|
|
34
|
+
|
|
35
|
+
*.bak
|
|
36
|
+
*.backup
|
|
37
|
+
|
|
38
|
+
uv.lock
|
|
39
|
+
examples/
|
|
40
|
+
testes/
|
driftbrake-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuri Pontes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: driftbrake
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Detect, classify, and block schema drift in PostgreSQL before your pipelines break
|
|
5
|
+
Project-URL: Homepage, https://github.com/yurivski/driftbrake
|
|
6
|
+
Project-URL: Repository, https://github.com/yurivski/driftbrake
|
|
7
|
+
Project-URL: Issues, https://github.com/yurivski/driftbrake/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yurivski/driftbrake/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Yuri Pontes <yurilbrok23@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: contract-testing,data-engineering,database,postgresql,schema
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Database
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: jinja2>=3.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0
|
|
29
|
+
Requires-Dist: typer>=0.9
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine; extra == 'dev'
|
|
37
|
+
Provides-Extra: postgres
|
|
38
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
DriftBrake
|
|
43
|
+
==========
|
|
44
|
+
|
|
45
|
+
DriftBrake is a Python tool that validates schema contracts before data pipelines run.
|
|
46
|
+
|
|
47
|
+
It reads the current PostgreSQL schema automatically, compares it against a versioned
|
|
48
|
+
contract, classifies changes by impact (BREAKING, WARNING, SAFE), and blocks pipelines
|
|
49
|
+
when incompatible changes are detected, before they cause failures in production.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
The tool
|
|
53
|
+
========
|
|
54
|
+
|
|
55
|
+
DriftBrake is not a migration tool. It does not apply changes to the database and does
|
|
56
|
+
not generate SQL scripts.
|
|
57
|
+
|
|
58
|
+
It runs BEFORE pipelines, verifying that the actual database still respects the
|
|
59
|
+
contract expected by its data consumers.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Example
|
|
63
|
+
=======
|
|
64
|
+
|
|
65
|
+
Data pipelines fail silently when the database schema changes without warning:
|
|
66
|
+
|
|
67
|
+
- column removed or renamed
|
|
68
|
+
- data type altered
|
|
69
|
+
- NOT NULL added without a default
|
|
70
|
+
- foreign key modified
|
|
71
|
+
|
|
72
|
+
This tool runs an automatic validation before the pipeline starts and blocks the
|
|
73
|
+
execution if the database is no longer compatible with the expected contract.
|
|
74
|
+
|
|
75
|
+
Usage:
|
|
76
|
+
|
|
77
|
+
driftbrake init # creates the schema.lock.json contract
|
|
78
|
+
driftbrake check # checks whether the database has changed
|
|
79
|
+
driftbrake diff # compares two states without touching the contract
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Documentation
|
|
83
|
+
=============
|
|
84
|
+
|
|
85
|
+
- DOCUMENTATION.md - https://github.com/yurivski/driftbrake/blob/main/DOCUMENTATION.md
|
|
86
|
+
- CHANGELOG.md - https://github.com/yurivski/driftbrake/blob/main/CHANGELOG.md
|
|
87
|
+
- YML file - https://github.com/yurivski/driftbrake/blob/main/driftbrake.example.yml
|
|
88
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
```text
|
|
2
|
+
DriftBrake
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
DriftBrake is a Python tool that validates schema contracts before data pipelines run.
|
|
6
|
+
|
|
7
|
+
It reads the current PostgreSQL schema automatically, compares it against a versioned
|
|
8
|
+
contract, classifies changes by impact (BREAKING, WARNING, SAFE), and blocks pipelines
|
|
9
|
+
when incompatible changes are detected, before they cause failures in production.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
The tool
|
|
13
|
+
========
|
|
14
|
+
|
|
15
|
+
DriftBrake is not a migration tool. It does not apply changes to the database and does
|
|
16
|
+
not generate SQL scripts.
|
|
17
|
+
|
|
18
|
+
It runs BEFORE pipelines, verifying that the actual database still respects the
|
|
19
|
+
contract expected by its data consumers.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Example
|
|
23
|
+
=======
|
|
24
|
+
|
|
25
|
+
Data pipelines fail silently when the database schema changes without warning:
|
|
26
|
+
|
|
27
|
+
- column removed or renamed
|
|
28
|
+
- data type altered
|
|
29
|
+
- NOT NULL added without a default
|
|
30
|
+
- foreign key modified
|
|
31
|
+
|
|
32
|
+
This tool runs an automatic validation before the pipeline starts and blocks the
|
|
33
|
+
execution if the database is no longer compatible with the expected contract.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
|
|
37
|
+
driftbrake init # creates the schema.lock.json contract
|
|
38
|
+
driftbrake check # checks whether the database has changed
|
|
39
|
+
driftbrake diff # compares two states without touching the contract
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Documentation
|
|
43
|
+
=============
|
|
44
|
+
|
|
45
|
+
- DOCUMENTATION.md - https://github.com/yurivski/driftbrake/blob/main/DOCUMENTATION.md
|
|
46
|
+
- CHANGELOG.md - https://github.com/yurivski/driftbrake/blob/main/CHANGELOG.md
|
|
47
|
+
- YML file - https://github.com/yurivski/driftbrake/blob/main/driftbrake.example.yml
|
|
48
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# driftbrake.example.yml
|
|
2
|
+
# Copie este arquivo para driftbrake.yml e personalize conforme necessário.
|
|
3
|
+
#
|
|
4
|
+
# Este arquivo controla como o DriftBrake compara schemas, quais
|
|
5
|
+
# níveis de severidade causam falhas e quais tabelas/colunas ignorar.
|
|
6
|
+
|
|
7
|
+
# Níveis de severidade que causam código de saída não-zero (falha).
|
|
8
|
+
# Valores possíveis: BREAKING, WARNING, SAFE
|
|
9
|
+
fail_on:
|
|
10
|
+
- BREAKING
|
|
11
|
+
|
|
12
|
+
# Níveis de severidade que emitem avisos (mas não causam falha por padrão).
|
|
13
|
+
warn_on:
|
|
14
|
+
- WARNING
|
|
15
|
+
|
|
16
|
+
# Filtragem de schemas
|
|
17
|
+
schemas:
|
|
18
|
+
# Comparar apenas estes schemas (deixe vazio para incluir todos)
|
|
19
|
+
include:
|
|
20
|
+
- public
|
|
21
|
+
# Excluir estes schemas da comparação
|
|
22
|
+
exclude: []
|
|
23
|
+
|
|
24
|
+
# Filtragem de tabelas
|
|
25
|
+
tables:
|
|
26
|
+
# Ignorar completamente estas tabelas (nenhuma comparação realizada)
|
|
27
|
+
ignore:
|
|
28
|
+
- alembic_version
|
|
29
|
+
- django_migrations
|
|
30
|
+
- flyway_schema_history
|
|
31
|
+
|
|
32
|
+
# Filtragem de colunas
|
|
33
|
+
columns:
|
|
34
|
+
# Ignorar colunas específicas por tabela (padrões glob suportados)
|
|
35
|
+
ignore:
|
|
36
|
+
# Ignorar 'updated_at' em todas as tabelas que correspondem a 'orders*'
|
|
37
|
+
orders:
|
|
38
|
+
- updated_at
|
|
39
|
+
- created_at
|
|
40
|
+
customers:
|
|
41
|
+
- last_login
|
|
42
|
+
|
|
43
|
+
# Sobrescritas de regras personalizadas
|
|
44
|
+
# Estas substituem a classificação padrão para tipos específicos de alteração.
|
|
45
|
+
rules:
|
|
46
|
+
# Tratar alterações de posição ordinal como SAFE em vez de WARNING
|
|
47
|
+
ordinal_position_changed: SAFE
|
|
48
|
+
# Tratar alterações de valor padrão como WARNING (comportamento padrão)
|
|
49
|
+
default_changed: WARNING
|
|
50
|
+
# Tratar alterações de restrição unique como SAFE
|
|
51
|
+
# unique_changed: SAFE
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "driftbrake"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Detect, classify, and block schema drift in PostgreSQL before your pipelines break"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Yuri Pontes", email = "yurilbrok23@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["schema", "database", "data-engineering", "postgresql", "contract-testing"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Database",
|
|
27
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"sqlalchemy>=2.0,<3.0",
|
|
31
|
+
"python-dotenv>=1.0",
|
|
32
|
+
"pyyaml>=6.0",
|
|
33
|
+
"jinja2>=3.0",
|
|
34
|
+
"rich>=13.0",
|
|
35
|
+
"typer>=0.9",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
postgres = ["psycopg2-binary>=2.9"]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"pytest-cov",
|
|
43
|
+
"ruff",
|
|
44
|
+
"mypy",
|
|
45
|
+
"build",
|
|
46
|
+
"twine",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://github.com/yurivski/driftbrake"
|
|
51
|
+
Repository = "https://github.com/yurivski/driftbrake"
|
|
52
|
+
Issues = "https://github.com/yurivski/driftbrake/issues"
|
|
53
|
+
Changelog = "https://github.com/yurivski/driftbrake/blob/main/CHANGELOG.md"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
driftbrake = "driftbrake.cli:app"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.wheel]
|
|
59
|
+
packages = ["src/driftbrake"]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build]
|
|
62
|
+
exclude = [
|
|
63
|
+
# venvs
|
|
64
|
+
".venv*",
|
|
65
|
+
"venv",
|
|
66
|
+
|
|
67
|
+
".git",
|
|
68
|
+
".github",
|
|
69
|
+
|
|
70
|
+
".gitignore",
|
|
71
|
+
".gitattributes",
|
|
72
|
+
".claude",
|
|
73
|
+
".vscode",
|
|
74
|
+
".idea",
|
|
75
|
+
|
|
76
|
+
".pytest_cache",
|
|
77
|
+
".ruff_cache",
|
|
78
|
+
".mypy_cache",
|
|
79
|
+
"__pycache__",
|
|
80
|
+
"*.pyc",
|
|
81
|
+
"*.pyo",
|
|
82
|
+
|
|
83
|
+
"dist",
|
|
84
|
+
"build",
|
|
85
|
+
"*.egg-info",
|
|
86
|
+
|
|
87
|
+
"tests",
|
|
88
|
+
"examples",
|
|
89
|
+
"imagens",
|
|
90
|
+
"fonte",
|
|
91
|
+
"DOCUMENTATION.md",
|
|
92
|
+
"CHANGELOG.md",
|
|
93
|
+
"Makefile",
|
|
94
|
+
"schema.lock.json",
|
|
95
|
+
|
|
96
|
+
"uv.lock",
|
|
97
|
+
"requirements.txt",
|
|
98
|
+
|
|
99
|
+
".env",
|
|
100
|
+
".env.*",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
testpaths = ["tests"]
|
|
105
|
+
pythonpath = ["src"]
|
|
106
|
+
|
|
107
|
+
[tool.ruff]
|
|
108
|
+
src = ["src"]
|
|
109
|
+
line-length = 100
|
|
110
|
+
target-version = "py311"
|
|
111
|
+
|
|
112
|
+
[tool.ruff.lint]
|
|
113
|
+
select = ["E", "F", "I", "N", "UP"]
|
|
114
|
+
|
|
115
|
+
[tool.mypy]
|
|
116
|
+
python_version = "3.11"
|
|
117
|
+
mypy_path = "src"
|
|
118
|
+
strict = false
|
|
119
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DriftBrake
|
|
3
|
+
=========
|
|
4
|
+
|
|
5
|
+
A schema contract guard for data pipelines.
|
|
6
|
+
Detects, classifies, and reports schema changes in PostgreSQL databases.
|
|
7
|
+
|
|
8
|
+
Quick start:
|
|
9
|
+
from driftbrake import SchemaGuard
|
|
10
|
+
|
|
11
|
+
SchemaGuard.from_env(
|
|
12
|
+
contract_path="schema.lock.json",
|
|
13
|
+
fail_on=["BREAKING"],
|
|
14
|
+
).assert_compatible()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from driftbrake.classifiers.impact_classifier import ImpactClassifier
|
|
18
|
+
from driftbrake.comparators.schema_comparator import SchemaComparator
|
|
19
|
+
from driftbrake.exceptions import (
|
|
20
|
+
BreakingSchemaChangeError,
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
SchemaConnectionError,
|
|
23
|
+
SchemaContractNotFoundError,
|
|
24
|
+
SchemaDetectorError,
|
|
25
|
+
)
|
|
26
|
+
from driftbrake.guard import SchemaGuard
|
|
27
|
+
from driftbrake.models import (
|
|
28
|
+
ChangeType,
|
|
29
|
+
ColumnSchema,
|
|
30
|
+
DatabaseSchema,
|
|
31
|
+
DiffResult,
|
|
32
|
+
SchemaChange,
|
|
33
|
+
Severity,
|
|
34
|
+
TableSchema,
|
|
35
|
+
)
|
|
36
|
+
from driftbrake.readers.json_reader import JsonSchemaReader
|
|
37
|
+
from driftbrake.readers.postgres import PostgresSchemaReader
|
|
38
|
+
|
|
39
|
+
__version__ = "0.2.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# High-level API
|
|
43
|
+
"SchemaGuard",
|
|
44
|
+
# Comparators and classifiers
|
|
45
|
+
"SchemaComparator",
|
|
46
|
+
"ImpactClassifier",
|
|
47
|
+
# Readers
|
|
48
|
+
"PostgresSchemaReader",
|
|
49
|
+
"JsonSchemaReader",
|
|
50
|
+
# Models
|
|
51
|
+
"DatabaseSchema",
|
|
52
|
+
"TableSchema",
|
|
53
|
+
"ColumnSchema",
|
|
54
|
+
"SchemaChange",
|
|
55
|
+
"DiffResult",
|
|
56
|
+
"Severity",
|
|
57
|
+
"ChangeType",
|
|
58
|
+
# Exceptions
|
|
59
|
+
"SchemaDetectorError",
|
|
60
|
+
"SchemaContractNotFoundError",
|
|
61
|
+
"SchemaConnectionError",
|
|
62
|
+
"BreakingSchemaChangeError",
|
|
63
|
+
"ConfigurationError",
|
|
64
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Classificador de impacto para alterações de schema.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from driftbrake.classifiers.type_compatibility import classify_type_change
|
|
6
|
+
from driftbrake.models import ChangeType, ColumnSchema, SchemaChange, Severity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImpactClassifier:
|
|
10
|
+
"""
|
|
11
|
+
- Objetos removidos são sempre BREAKING.
|
|
12
|
+
- Colunas nullable adicionadas são SAFE.
|
|
13
|
+
- Colunas NOT NULL adicionadas sem default são BREAKING.
|
|
14
|
+
- Alterações de tipo são avaliadas pela matriz de compatibilidade de tipos.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, custom_rules: dict | None = None) -> None:
|
|
18
|
+
self.custom_rules = custom_rules or {}
|
|
19
|
+
|
|
20
|
+
def classify_table_added(self, schema: str, table: str) -> Severity:
|
|
21
|
+
return Severity.SAFE
|
|
22
|
+
|
|
23
|
+
def classify_table_removed(self, schema: str, table: str) -> Severity:
|
|
24
|
+
return Severity.BREAKING
|
|
25
|
+
|
|
26
|
+
def classify_column_added(self, column: ColumnSchema) -> Severity:
|
|
27
|
+
"""
|
|
28
|
+
- Adicionada nullable: SAFE
|
|
29
|
+
- Adicionada NOT NULL com default: WARNING
|
|
30
|
+
- Adicionada NOT NULL sem default: BREAKING
|
|
31
|
+
"""
|
|
32
|
+
if column.nullable:
|
|
33
|
+
return Severity.SAFE
|
|
34
|
+
if column.default is not None:
|
|
35
|
+
return Severity.WARNING
|
|
36
|
+
return Severity.BREAKING
|
|
37
|
+
|
|
38
|
+
def classify_column_removed(self, column_name: str) -> Severity:
|
|
39
|
+
return Severity.BREAKING
|
|
40
|
+
|
|
41
|
+
def classify_type_change(self, old_type: str, new_type: str) -> Severity:
|
|
42
|
+
return classify_type_change(old_type, new_type)
|
|
43
|
+
|
|
44
|
+
def classify_nullable_change(self, old_nullable: bool, new_nullable: bool) -> Severity:
|
|
45
|
+
if not old_nullable and new_nullable:
|
|
46
|
+
# NOT NULL removido -> nullable permitido: WARNING (afrouxamento)
|
|
47
|
+
return Severity.WARNING
|
|
48
|
+
if old_nullable and not new_nullable:
|
|
49
|
+
# nullable removido -> NOT NULL adicionado: BREAKING
|
|
50
|
+
return Severity.BREAKING
|
|
51
|
+
return Severity.SAFE
|
|
52
|
+
|
|
53
|
+
def classify_default_change(self, old_default: object, new_default: object) -> Severity:
|
|
54
|
+
return Severity.WARNING
|
|
55
|
+
|
|
56
|
+
def classify_primary_key_change(self, old_pk: bool, new_pk: bool) -> Severity:
|
|
57
|
+
return Severity.BREAKING
|
|
58
|
+
|
|
59
|
+
def classify_unique_change(self, old_unique: bool, new_unique: bool) -> Severity:
|
|
60
|
+
return Severity.WARNING
|
|
61
|
+
|
|
62
|
+
def classify_foreign_key_change(
|
|
63
|
+
self, old_fk: list, new_fk: list
|
|
64
|
+
) -> Severity:
|
|
65
|
+
old_has = bool(old_fk)
|
|
66
|
+
new_has = bool(new_fk)
|
|
67
|
+
if not old_has and new_has:
|
|
68
|
+
# FK adicionada
|
|
69
|
+
return Severity.WARNING
|
|
70
|
+
# FK alterada ou removida
|
|
71
|
+
return Severity.BREAKING
|
|
72
|
+
|
|
73
|
+
def classify_ordinal_position_change(
|
|
74
|
+
self, old_pos: int, new_pos: int
|
|
75
|
+
) -> Severity:
|
|
76
|
+
return Severity.WARNING
|
|
77
|
+
|
|
78
|
+
def classify_possible_rename(
|
|
79
|
+
self, removed_col: str, added_col: str
|
|
80
|
+
) -> Severity:
|
|
81
|
+
return Severity.WARNING
|
|
82
|
+
|
|
83
|
+
def build_change(
|
|
84
|
+
self,
|
|
85
|
+
change_type: ChangeType,
|
|
86
|
+
severity: Severity,
|
|
87
|
+
schema_name: str,
|
|
88
|
+
table_name: str,
|
|
89
|
+
column_name: str | None,
|
|
90
|
+
field_name: str | None,
|
|
91
|
+
old_value: object,
|
|
92
|
+
new_value: object,
|
|
93
|
+
description: str,
|
|
94
|
+
suggestion: str | None = None,
|
|
95
|
+
) -> SchemaChange:
|
|
96
|
+
return SchemaChange(
|
|
97
|
+
change_type=change_type,
|
|
98
|
+
severity=severity,
|
|
99
|
+
schema_name=schema_name,
|
|
100
|
+
table_name=table_name,
|
|
101
|
+
column_name=column_name,
|
|
102
|
+
field_name=field_name,
|
|
103
|
+
old_value=old_value,
|
|
104
|
+
new_value=new_value,
|
|
105
|
+
description=description,
|
|
106
|
+
suggestion=suggestion,
|
|
107
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matriz de compatibilidade de tipos PostgreSQL.
|
|
3
|
+
|
|
4
|
+
Classifica alterações de tipo como SAFE, WARNING ou BREAKING com base nas
|
|
5
|
+
regras de coerção e cast implícito do PostgreSQL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from driftbrake.models import Severity
|
|
13
|
+
|
|
14
|
+
# Regras de compatibilidade explícitas como triplas (from_pattern, to_pattern, severity).
|
|
15
|
+
# Os padrões são comparados sem diferenciação de maiúsculas usando substring ou regex.
|
|
16
|
+
_COMPAT_RULES: list[tuple[str, str, Severity]] = [
|
|
17
|
+
# Expansões de VARCHAR: seguro
|
|
18
|
+
("varchar", "text", Severity.SAFE),
|
|
19
|
+
("character varying", "text", Severity.SAFE),
|
|
20
|
+
|
|
21
|
+
# Alargamento numérico: seguro (tipos menores promovidos)
|
|
22
|
+
("smallint", "integer", Severity.SAFE),
|
|
23
|
+
("smallint", "bigint", Severity.SAFE),
|
|
24
|
+
("real", "double precision", Severity.SAFE),
|
|
25
|
+
|
|
26
|
+
# Estreitamento numérico: crítico
|
|
27
|
+
("bigint", "integer", Severity.BREAKING),
|
|
28
|
+
("bigint", "smallint", Severity.BREAKING),
|
|
29
|
+
("integer", "smallint", Severity.BREAKING),
|
|
30
|
+
("double precision", "real", Severity.BREAKING),
|
|
31
|
+
|
|
32
|
+
# integer -> bigint: aviso (alargamento, mas pode afetar o comportamento da app / tipos ORM)
|
|
33
|
+
("integer", "bigint", Severity.WARNING),
|
|
34
|
+
|
|
35
|
+
# Data/hora: date -> timestamp é aviso (sem perda de dados, mas semântica muda)
|
|
36
|
+
("date", "timestamp", Severity.WARNING),
|
|
37
|
+
("timestamp", "date", Severity.BREAKING),
|
|
38
|
+
("timestamp", "timestamptz", Severity.WARNING),
|
|
39
|
+
("timestamptz", "timestamp", Severity.WARNING),
|
|
40
|
+
|
|
41
|
+
# Estreitamento de text/varchar: crítico
|
|
42
|
+
("text", "varchar", Severity.BREAKING),
|
|
43
|
+
("text", "character varying", Severity.BREAKING),
|
|
44
|
+
("text", "numeric", Severity.BREAKING),
|
|
45
|
+
("text", "integer", Severity.BREAKING),
|
|
46
|
+
("text", "bigint", Severity.BREAKING),
|
|
47
|
+
|
|
48
|
+
# numeric para text: crítico
|
|
49
|
+
("numeric", "text", Severity.BREAKING),
|
|
50
|
+
("integer", "text", Severity.WARNING),
|
|
51
|
+
("bigint", "text", Severity.WARNING),
|
|
52
|
+
|
|
53
|
+
# Alterações de boolean: crítico
|
|
54
|
+
("boolean", "integer", Severity.BREAKING),
|
|
55
|
+
("integer", "boolean", Severity.BREAKING),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_type(type_str: str) -> str:
|
|
60
|
+
# Normaliza a string de tipo para comparação: minúsculas e sem espaços extras.
|
|
61
|
+
return type_str.strip().lower()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_varchar_length(type_str: str) -> int | None:
|
|
65
|
+
# Extrai o comprimento de VARCHAR(n) ou CHARACTER VARYING(n).
|
|
66
|
+
match = re.search(r"(?:varchar|character varying)\s*\((\d+)\)", type_str, re.IGNORECASE)
|
|
67
|
+
if match:
|
|
68
|
+
return int(match.group(1))
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_numeric_precision(type_str: str) -> tuple[int, int] | None:
|
|
73
|
+
# Extrai (precisão, escala) de NUMERIC(p, s) ou DECIMAL(p, s).
|
|
74
|
+
match = re.search(r"(?:numeric|decimal)\s*\((\d+)\s*,\s*(\d+)\)", type_str, re.IGNORECASE)
|
|
75
|
+
if match:
|
|
76
|
+
return int(match.group(1)), int(match.group(2))
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def classify_type_change(old_type: str, new_type: str) -> Severity:
|
|
81
|
+
# Classifica uma alteração de tipo de coluna como SAFE, WARNING ou BREAKING.
|
|
82
|
+
if _normalize_type(old_type) == _normalize_type(new_type):
|
|
83
|
+
return Severity.SAFE
|
|
84
|
+
|
|
85
|
+
old_norm = _normalize_type(old_type)
|
|
86
|
+
new_norm = _normalize_type(new_type)
|
|
87
|
+
|
|
88
|
+
# Regras de VARCHAR(n) -> VARCHAR(m)
|
|
89
|
+
old_len = _extract_varchar_length(old_norm)
|
|
90
|
+
new_len = _extract_varchar_length(new_norm)
|
|
91
|
+
if old_len is not None and new_len is not None:
|
|
92
|
+
if new_len >= old_len:
|
|
93
|
+
return Severity.SAFE
|
|
94
|
+
return Severity.BREAKING
|
|
95
|
+
|
|
96
|
+
# VARCHAR(n) -> TEXT: seguro
|
|
97
|
+
if old_len is not None and "text" in new_norm:
|
|
98
|
+
return Severity.SAFE
|
|
99
|
+
|
|
100
|
+
# NUMERIC(p1,s) -> NUMERIC(p2,s): seguro se p2 >= p1
|
|
101
|
+
old_num = _extract_numeric_precision(old_norm)
|
|
102
|
+
new_num = _extract_numeric_precision(new_norm)
|
|
103
|
+
if old_num is not None and new_num is not None:
|
|
104
|
+
old_prec, old_scale = old_num
|
|
105
|
+
new_prec, new_scale = new_num
|
|
106
|
+
if new_scale == old_scale and new_prec >= old_prec:
|
|
107
|
+
return Severity.SAFE
|
|
108
|
+
if new_prec < old_prec or new_scale != old_scale:
|
|
109
|
+
return Severity.BREAKING
|
|
110
|
+
|
|
111
|
+
# Aplica regras explícitas (verifica se a substring está contida)
|
|
112
|
+
for from_pat, to_pat, severity in _COMPAT_RULES:
|
|
113
|
+
if from_pat in old_norm and to_pat in new_norm:
|
|
114
|
+
return severity
|
|
115
|
+
|
|
116
|
+
# Padrão: alteração de tipo desconhecida é BREAKING (conservador)
|
|
117
|
+
return Severity.BREAKING
|