dbbridgekit 0.1.0__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.
- dbbridgekit-0.1.0/LICENSE +21 -0
- dbbridgekit-0.1.0/PKG-INFO +203 -0
- dbbridgekit-0.1.0/README.md +167 -0
- dbbridgekit-0.1.0/dbbridge/__init__.py +22 -0
- dbbridgekit-0.1.0/dbbridge/cli.py +177 -0
- dbbridgekit-0.1.0/dbbridge/codemod/__init__.py +3 -0
- dbbridgekit-0.1.0/dbbridge/codemod/codemod.py +194 -0
- dbbridgekit-0.1.0/dbbridge/compat/__init__.py +3 -0
- dbbridgekit-0.1.0/dbbridge/compat/runtime.py +193 -0
- dbbridgekit-0.1.0/dbbridge/core/__init__.py +2 -0
- dbbridgekit-0.1.0/dbbridge/core/ir.py +106 -0
- dbbridgekit-0.1.0/dbbridge/core/parser.py +60 -0
- dbbridgekit-0.1.0/dbbridge/core/planner.py +44 -0
- dbbridgekit-0.1.0/dbbridge/core/renderer.py +57 -0
- dbbridgekit-0.1.0/dbbridge/core/sql_lex.py +61 -0
- dbbridgekit-0.1.0/dbbridge/core/types.py +47 -0
- dbbridgekit-0.1.0/dbbridge/dialects/__init__.py +6 -0
- dbbridgekit-0.1.0/dbbridge/dialects/_stub.py +31 -0
- dbbridgekit-0.1.0/dbbridge/dialects/mysql.py +16 -0
- dbbridgekit-0.1.0/dbbridge/dialects/postgres.py +292 -0
- dbbridgekit-0.1.0/dbbridge/dialects/sqlite.py +279 -0
- dbbridgekit-0.1.0/dbbridge/dialects/sqlserver.py +15 -0
- dbbridgekit-0.1.0/dbbridge/migration/__init__.py +11 -0
- dbbridgekit-0.1.0/dbbridge/migration/data_migration.py +76 -0
- dbbridgekit-0.1.0/dbbridge/migration/rollback.py +57 -0
- dbbridgekit-0.1.0/dbbridge/migration/schema_migration.py +56 -0
- dbbridgekit-0.1.0/dbbridge/migration/validator.py +85 -0
- dbbridgekit-0.1.0/dbbridge/reports/__init__.py +3 -0
- dbbridgekit-0.1.0/dbbridge/reports/report.py +58 -0
- dbbridgekit-0.1.0/dbbridge/scanner/__init__.py +3 -0
- dbbridgekit-0.1.0/dbbridge/scanner/scanner.py +176 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/PKG-INFO +203 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/SOURCES.txt +49 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/dependency_links.txt +1 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/entry_points.txt +2 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/requires.txt +8 -0
- dbbridgekit-0.1.0/dbbridgekit.egg-info/top_level.txt +1 -0
- dbbridgekit-0.1.0/pyproject.toml +55 -0
- dbbridgekit-0.1.0/setup.cfg +4 -0
- dbbridgekit-0.1.0/tests/test_cli.py +215 -0
- dbbridgekit-0.1.0/tests/test_codemod.py +234 -0
- dbbridgekit-0.1.0/tests/test_compat_runtime.py +168 -0
- dbbridgekit-0.1.0/tests/test_ir.py +37 -0
- dbbridgekit-0.1.0/tests/test_migration.py +169 -0
- dbbridgekit-0.1.0/tests/test_package.py +16 -0
- dbbridgekit-0.1.0/tests/test_planner.py +76 -0
- dbbridgekit-0.1.0/tests/test_postgres_dialect.py +246 -0
- dbbridgekit-0.1.0/tests/test_report.py +56 -0
- dbbridgekit-0.1.0/tests/test_scanner.py +192 -0
- dbbridgekit-0.1.0/tests/test_sql_lex.py +56 -0
- dbbridgekit-0.1.0/tests/test_sqlite_dialect.py +228 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Otavio R Santana
|
|
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,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbbridgekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Plataforma genérica de conversão/análise/migração entre bancos de dados via Intermediate Representation (IR) canônica — SQLite, PostgreSQL, MySQL (em progresso), SQL Server (planejado).
|
|
5
|
+
Author-email: Otavio R Santana <tvostrodrigues8@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tvost2/dbbridge
|
|
8
|
+
Project-URL: Repository, https://github.com/tvost2/dbbridge
|
|
9
|
+
Project-URL: Issues, https://github.com/tvost2/dbbridge/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/tvost2/dbbridge/blob/main/docs/guide.md
|
|
11
|
+
Project-URL: Changelog, https://github.com/tvost2/dbbridge/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: sql,sqlite,postgresql,mysql,migration,ir,database
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
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 :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# DBBridge
|
|
38
|
+
|
|
39
|
+
Plataforma genérica de conversão, análise e migração entre bancos de dados, baseada numa
|
|
40
|
+
**Intermediate Representation (IR)** canônica — não um conversor pareado por combinação de bancos.
|
|
41
|
+
|
|
42
|
+
```txt
|
|
43
|
+
Banco origem → Parser do dialeto origem → IR canônica → Renderer do dialeto destino → Banco destino
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Evolução do [PostgresModel](../postgresmodel) generalizada: em vez de assumir SQLite→PostgreSQL
|
|
47
|
+
sempre, qualquer par de dialetos suportados passa pelo mesmo pipeline.
|
|
48
|
+
|
|
49
|
+
## Status (Fase 1)
|
|
50
|
+
|
|
51
|
+
| Dialeto | Parser | Renderer |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| SQLite | ✅ | ✅ |
|
|
54
|
+
| PostgreSQL | ✅ | ✅ |
|
|
55
|
+
| MySQL | estrutura registrada (Fase 2) | estrutura registrada (Fase 2) |
|
|
56
|
+
| SQL Server | estrutura registrada (Fase 5) | estrutura registrada (Fase 5) |
|
|
57
|
+
|
|
58
|
+
`dbbridge.dialects.mysql`/`sqlserver` já registram um dialeto "conhecido, ainda não implementado" —
|
|
59
|
+
`dbbridge dialects` lista os dois, e chamar `.parse()`/`.render_schema()` neles levanta
|
|
60
|
+
`NotImplementedError` com a fase planejada, em vez de um erro genérico de dialeto desconhecido.
|
|
61
|
+
|
|
62
|
+
167 testes, 99% de cobertura (`pytest --cov=dbbridge`). Os poucos pontos não cobertos são código
|
|
63
|
+
genuinamente inalcançável (stubs de método abstrato, guard `if __name__ == "__main__"`) — ver
|
|
64
|
+
[docs/guide.md](docs/guide.md#cobertura-de-testes).
|
|
65
|
+
|
|
66
|
+
## Instalação
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e .
|
|
70
|
+
# ou, pra rodar contra PostgreSQL de verdade:
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Uso rápido
|
|
75
|
+
|
|
76
|
+
### Traduzir um schema
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
dbbridge translate --from sqlite --to postgres schema.sql
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Casos que o parser/renderer não reconhecem com segurança viram `REVIEW_REQUIRED` no stderr — nunca
|
|
83
|
+
uma tradução inventada.
|
|
84
|
+
|
|
85
|
+
### Escanear um projeto Python
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
dbbridge scan --from sqlite --to postgres ./meu_projeto
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Encontra `execute()`/`executemany()` com SQL potencialmente incompatível entre os dois dialetos:
|
|
92
|
+
placeholders (`?` vs `%s`), `sqlite_master`, `PRAGMA`, `INSERT OR IGNORE`/`OR REPLACE`,
|
|
93
|
+
case-sensitivity de `LIKE`, autoincrement, e mais.
|
|
94
|
+
|
|
95
|
+
### Migrar código-fonte automaticamente
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
dbbridge patch --from sqlite --to postgres ./meu_projeto # dry-run, mostra o plano
|
|
99
|
+
dbbridge apply --from sqlite --to postgres ./meu_projeto # aplica com backup automático
|
|
100
|
+
dbbridge rollback ./meu_projeto # desfaz, restaura do backup
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Reescreve só literais de string simples sem ambiguidade (troca de placeholder). `f-strings`,
|
|
104
|
+
concatenação e casos ambíguos (aspas duplas, `INSERT OR REPLACE`, etc.) nunca são alterados
|
|
105
|
+
automaticamente — viram `REVIEW_REQUIRED`/`SKIPPED_COMPLEX_EXPRESSION` no plano.
|
|
106
|
+
|
|
107
|
+
### Migrar dados entre bancos de verdade
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dbbridge migrate-data --from sqlite --to postgres \
|
|
111
|
+
--source-dsn ./app.db --target-dsn "host=localhost dbname=app" --tables users leads
|
|
112
|
+
dbbridge validate --from sqlite --to postgres \
|
|
113
|
+
--source-dsn ./app.db --target-dsn "host=localhost dbname=app" --tables users leads
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Copia em lotes, nunca faz `DROP`/`TRUNCATE`, sempre acrescenta. `validate` compara contagens e
|
|
117
|
+
checksum (SHA-256) linha a linha quando a tabela é pequena o bastante.
|
|
118
|
+
|
|
119
|
+
### Compatibility Mode (código antigo continua rodando)
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from dbbridge import connect
|
|
123
|
+
|
|
124
|
+
db = connect(source_dialect="sqlite", target_dialect="postgres", dsn="host=localhost dbname=app")
|
|
125
|
+
db.execute("SELECT * FROM users WHERE id=?", (1,)) # vira %s por baixo, roda no Postgres de verdade
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Permite migrar a aplicação por partes: o código continua escrito na sintaxe de origem enquanto o
|
|
129
|
+
banco de verdade já é o destino.
|
|
130
|
+
|
|
131
|
+
## Exemplos executáveis
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
python examples/translate_schema.py # traduz um schema via API Python
|
|
135
|
+
python examples/scan_and_patch_project.py # scan -> patch -> apply -> rollback num mini-app
|
|
136
|
+
python examples/compat_mode.py # Compatibility Mode de ponta a ponta
|
|
137
|
+
DBBRIDGE_EXAMPLE_PG_DSN="host=localhost dbname=..." python examples/full_data_migration.py
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Todos os quatro rodam de verdade (não são pseudocódigo) — os três primeiros só precisam do SQLite
|
|
141
|
+
da stdlib; o último precisa de um PostgreSQL acessível via a variável de ambiente indicada.
|
|
142
|
+
|
|
143
|
+
## Arquitetura
|
|
144
|
+
|
|
145
|
+
```txt
|
|
146
|
+
dbbridge/
|
|
147
|
+
cli.py # scan|report|translate|patch|apply|migrate-data|validate|rollback|doctor|dialects
|
|
148
|
+
core/
|
|
149
|
+
ir.py # Schema, Table, Column, ForeignKey, UniqueConstraint, CheckConstraint, Index, View, Trigger, Enum
|
|
150
|
+
types.py # CanonicalType, Ambiguity
|
|
151
|
+
parser.py / renderer.py # contratos + registry (register_parser/get_parser/available_parsers, idem renderer)
|
|
152
|
+
planner.py # translate(sql, source, target) — o pipeline completo numa função
|
|
153
|
+
dialects/
|
|
154
|
+
sqlite.py, postgres.py # implementados
|
|
155
|
+
mysql.py, sqlserver.py # stubs registrados (NotImplementedError com a fase planejada)
|
|
156
|
+
scanner/scanner.py # AST — encontra SQL arriscado por par de dialetos
|
|
157
|
+
codemod/codemod.py # AST — reescreve só literais simples sem ambiguidade
|
|
158
|
+
migration/
|
|
159
|
+
schema_migration.py # traduz + aplica DDL (aditivo, nunca DROP/TRUNCATE)
|
|
160
|
+
data_migration.py # copia dados em lotes
|
|
161
|
+
validator.py # contagens + checksum
|
|
162
|
+
rollback.py # restaura código a partir de backup
|
|
163
|
+
compat/runtime.py # connect() — Compatibility Mode
|
|
164
|
+
reports/report.py # normaliza qualquer resultado pra texto/JSON
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Por que IR em vez de conversores pareados: com N dialetos, um conversor direto por par cresce O(N²)
|
|
168
|
+
e duplica a mesma lógica de tipos/constraints em cada combinação. Com um modelo canônico no meio,
|
|
169
|
+
cada dialeto novo precisa de só 1 parser + 1 renderer (O(N)) pra já converter de/para todos os
|
|
170
|
+
outros já implementados.
|
|
171
|
+
|
|
172
|
+
## Segurança
|
|
173
|
+
|
|
174
|
+
- Toda alteração de código gera backup antes de escrever, e pode ser revertida via `dbbridge rollback`.
|
|
175
|
+
- Migração de schema/dados é sempre aditiva — nunca `DROP`/`TRUNCATE`/`DELETE`.
|
|
176
|
+
- Casos ambíguos (aspas duplas em literal, `INSERT OR REPLACE`, f-strings, expressões dinâmicas)
|
|
177
|
+
nunca são "adivinhados" — viram `REVIEW_REQUIRED` pra revisão manual.
|
|
178
|
+
- `apply_changes` verifica `ast.parse()` do arquivo resultante antes de considerar sucesso; se o
|
|
179
|
+
resultado tiver `SyntaxError`, reverte sozinho a partir do backup.
|
|
180
|
+
|
|
181
|
+
## Fases
|
|
182
|
+
|
|
183
|
+
1. **IR + SQLite ↔ PostgreSQL** — atual, completo: parser/renderer dos dois dialetos, scanner,
|
|
184
|
+
codemod, migração de schema/dados, validação por checksum, rollback, CLI, Compatibility Mode.
|
|
185
|
+
2. MySQL (parser + renderer reais).
|
|
186
|
+
3. Codemod genérico validado contra os 3 dialetos.
|
|
187
|
+
4. Data migration validada nos 4 pares centrais (SQLite/Postgres/MySQL cruzados).
|
|
188
|
+
5. SQL Server.
|
|
189
|
+
6. **Fases futuras (levantadas, ainda não priorizadas):** shadow migration (rodar origem e destino
|
|
190
|
+
em paralelo comparando resultados antes do cutover final), query replay (reproduzir tráfego real
|
|
191
|
+
de produção contra o destino como teste de carga/compatibilidade), dashboard web, relatórios em
|
|
192
|
+
HTML/PDF (hoje só texto/JSON via `reports/report.py`), mecanismo de plugin formal pra dialetos
|
|
193
|
+
externos (hoje a extensão já é possível via `register_parser`/`register_renderer` — falta só
|
|
194
|
+
empacotar como plugin instalável separadamente, ex. entry points do Python), e suporte a bancos
|
|
195
|
+
não-relacionais como MongoDB (exigiria uma IR paralela pra documentos/coleções — ver
|
|
196
|
+
[docs/compatibility-matrix.md](docs/compatibility-matrix.md#roadmap-além-do-sql-relacional)).
|
|
197
|
+
|
|
198
|
+
Cada fase só avança depois da anterior validada — ver [docs/guide.md](docs/guide.md) para detalhes
|
|
199
|
+
de arquitetura e extensão pra novos dialetos, [docs/cli-reference.md](docs/cli-reference.md) pra
|
|
200
|
+
todos os comandos, [docs/compatibility-matrix.md](docs/compatibility-matrix.md) pro que já
|
|
201
|
+
funciona vs. planejado, [docs/migration-tutorial.md](docs/migration-tutorial.md) pro passo a passo
|
|
202
|
+
completo, e [docs/production-checklist.md](docs/production-checklist.md) antes de rodar contra
|
|
203
|
+
dados reais.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# DBBridge
|
|
2
|
+
|
|
3
|
+
Plataforma genérica de conversão, análise e migração entre bancos de dados, baseada numa
|
|
4
|
+
**Intermediate Representation (IR)** canônica — não um conversor pareado por combinação de bancos.
|
|
5
|
+
|
|
6
|
+
```txt
|
|
7
|
+
Banco origem → Parser do dialeto origem → IR canônica → Renderer do dialeto destino → Banco destino
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Evolução do [PostgresModel](../postgresmodel) generalizada: em vez de assumir SQLite→PostgreSQL
|
|
11
|
+
sempre, qualquer par de dialetos suportados passa pelo mesmo pipeline.
|
|
12
|
+
|
|
13
|
+
## Status (Fase 1)
|
|
14
|
+
|
|
15
|
+
| Dialeto | Parser | Renderer |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| SQLite | ✅ | ✅ |
|
|
18
|
+
| PostgreSQL | ✅ | ✅ |
|
|
19
|
+
| MySQL | estrutura registrada (Fase 2) | estrutura registrada (Fase 2) |
|
|
20
|
+
| SQL Server | estrutura registrada (Fase 5) | estrutura registrada (Fase 5) |
|
|
21
|
+
|
|
22
|
+
`dbbridge.dialects.mysql`/`sqlserver` já registram um dialeto "conhecido, ainda não implementado" —
|
|
23
|
+
`dbbridge dialects` lista os dois, e chamar `.parse()`/`.render_schema()` neles levanta
|
|
24
|
+
`NotImplementedError` com a fase planejada, em vez de um erro genérico de dialeto desconhecido.
|
|
25
|
+
|
|
26
|
+
167 testes, 99% de cobertura (`pytest --cov=dbbridge`). Os poucos pontos não cobertos são código
|
|
27
|
+
genuinamente inalcançável (stubs de método abstrato, guard `if __name__ == "__main__"`) — ver
|
|
28
|
+
[docs/guide.md](docs/guide.md#cobertura-de-testes).
|
|
29
|
+
|
|
30
|
+
## Instalação
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
# ou, pra rodar contra PostgreSQL de verdade:
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Uso rápido
|
|
39
|
+
|
|
40
|
+
### Traduzir um schema
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dbbridge translate --from sqlite --to postgres schema.sql
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Casos que o parser/renderer não reconhecem com segurança viram `REVIEW_REQUIRED` no stderr — nunca
|
|
47
|
+
uma tradução inventada.
|
|
48
|
+
|
|
49
|
+
### Escanear um projeto Python
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dbbridge scan --from sqlite --to postgres ./meu_projeto
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Encontra `execute()`/`executemany()` com SQL potencialmente incompatível entre os dois dialetos:
|
|
56
|
+
placeholders (`?` vs `%s`), `sqlite_master`, `PRAGMA`, `INSERT OR IGNORE`/`OR REPLACE`,
|
|
57
|
+
case-sensitivity de `LIKE`, autoincrement, e mais.
|
|
58
|
+
|
|
59
|
+
### Migrar código-fonte automaticamente
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
dbbridge patch --from sqlite --to postgres ./meu_projeto # dry-run, mostra o plano
|
|
63
|
+
dbbridge apply --from sqlite --to postgres ./meu_projeto # aplica com backup automático
|
|
64
|
+
dbbridge rollback ./meu_projeto # desfaz, restaura do backup
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Reescreve só literais de string simples sem ambiguidade (troca de placeholder). `f-strings`,
|
|
68
|
+
concatenação e casos ambíguos (aspas duplas, `INSERT OR REPLACE`, etc.) nunca são alterados
|
|
69
|
+
automaticamente — viram `REVIEW_REQUIRED`/`SKIPPED_COMPLEX_EXPRESSION` no plano.
|
|
70
|
+
|
|
71
|
+
### Migrar dados entre bancos de verdade
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
dbbridge migrate-data --from sqlite --to postgres \
|
|
75
|
+
--source-dsn ./app.db --target-dsn "host=localhost dbname=app" --tables users leads
|
|
76
|
+
dbbridge validate --from sqlite --to postgres \
|
|
77
|
+
--source-dsn ./app.db --target-dsn "host=localhost dbname=app" --tables users leads
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Copia em lotes, nunca faz `DROP`/`TRUNCATE`, sempre acrescenta. `validate` compara contagens e
|
|
81
|
+
checksum (SHA-256) linha a linha quando a tabela é pequena o bastante.
|
|
82
|
+
|
|
83
|
+
### Compatibility Mode (código antigo continua rodando)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from dbbridge import connect
|
|
87
|
+
|
|
88
|
+
db = connect(source_dialect="sqlite", target_dialect="postgres", dsn="host=localhost dbname=app")
|
|
89
|
+
db.execute("SELECT * FROM users WHERE id=?", (1,)) # vira %s por baixo, roda no Postgres de verdade
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Permite migrar a aplicação por partes: o código continua escrito na sintaxe de origem enquanto o
|
|
93
|
+
banco de verdade já é o destino.
|
|
94
|
+
|
|
95
|
+
## Exemplos executáveis
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python examples/translate_schema.py # traduz um schema via API Python
|
|
99
|
+
python examples/scan_and_patch_project.py # scan -> patch -> apply -> rollback num mini-app
|
|
100
|
+
python examples/compat_mode.py # Compatibility Mode de ponta a ponta
|
|
101
|
+
DBBRIDGE_EXAMPLE_PG_DSN="host=localhost dbname=..." python examples/full_data_migration.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Todos os quatro rodam de verdade (não são pseudocódigo) — os três primeiros só precisam do SQLite
|
|
105
|
+
da stdlib; o último precisa de um PostgreSQL acessível via a variável de ambiente indicada.
|
|
106
|
+
|
|
107
|
+
## Arquitetura
|
|
108
|
+
|
|
109
|
+
```txt
|
|
110
|
+
dbbridge/
|
|
111
|
+
cli.py # scan|report|translate|patch|apply|migrate-data|validate|rollback|doctor|dialects
|
|
112
|
+
core/
|
|
113
|
+
ir.py # Schema, Table, Column, ForeignKey, UniqueConstraint, CheckConstraint, Index, View, Trigger, Enum
|
|
114
|
+
types.py # CanonicalType, Ambiguity
|
|
115
|
+
parser.py / renderer.py # contratos + registry (register_parser/get_parser/available_parsers, idem renderer)
|
|
116
|
+
planner.py # translate(sql, source, target) — o pipeline completo numa função
|
|
117
|
+
dialects/
|
|
118
|
+
sqlite.py, postgres.py # implementados
|
|
119
|
+
mysql.py, sqlserver.py # stubs registrados (NotImplementedError com a fase planejada)
|
|
120
|
+
scanner/scanner.py # AST — encontra SQL arriscado por par de dialetos
|
|
121
|
+
codemod/codemod.py # AST — reescreve só literais simples sem ambiguidade
|
|
122
|
+
migration/
|
|
123
|
+
schema_migration.py # traduz + aplica DDL (aditivo, nunca DROP/TRUNCATE)
|
|
124
|
+
data_migration.py # copia dados em lotes
|
|
125
|
+
validator.py # contagens + checksum
|
|
126
|
+
rollback.py # restaura código a partir de backup
|
|
127
|
+
compat/runtime.py # connect() — Compatibility Mode
|
|
128
|
+
reports/report.py # normaliza qualquer resultado pra texto/JSON
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Por que IR em vez de conversores pareados: com N dialetos, um conversor direto por par cresce O(N²)
|
|
132
|
+
e duplica a mesma lógica de tipos/constraints em cada combinação. Com um modelo canônico no meio,
|
|
133
|
+
cada dialeto novo precisa de só 1 parser + 1 renderer (O(N)) pra já converter de/para todos os
|
|
134
|
+
outros já implementados.
|
|
135
|
+
|
|
136
|
+
## Segurança
|
|
137
|
+
|
|
138
|
+
- Toda alteração de código gera backup antes de escrever, e pode ser revertida via `dbbridge rollback`.
|
|
139
|
+
- Migração de schema/dados é sempre aditiva — nunca `DROP`/`TRUNCATE`/`DELETE`.
|
|
140
|
+
- Casos ambíguos (aspas duplas em literal, `INSERT OR REPLACE`, f-strings, expressões dinâmicas)
|
|
141
|
+
nunca são "adivinhados" — viram `REVIEW_REQUIRED` pra revisão manual.
|
|
142
|
+
- `apply_changes` verifica `ast.parse()` do arquivo resultante antes de considerar sucesso; se o
|
|
143
|
+
resultado tiver `SyntaxError`, reverte sozinho a partir do backup.
|
|
144
|
+
|
|
145
|
+
## Fases
|
|
146
|
+
|
|
147
|
+
1. **IR + SQLite ↔ PostgreSQL** — atual, completo: parser/renderer dos dois dialetos, scanner,
|
|
148
|
+
codemod, migração de schema/dados, validação por checksum, rollback, CLI, Compatibility Mode.
|
|
149
|
+
2. MySQL (parser + renderer reais).
|
|
150
|
+
3. Codemod genérico validado contra os 3 dialetos.
|
|
151
|
+
4. Data migration validada nos 4 pares centrais (SQLite/Postgres/MySQL cruzados).
|
|
152
|
+
5. SQL Server.
|
|
153
|
+
6. **Fases futuras (levantadas, ainda não priorizadas):** shadow migration (rodar origem e destino
|
|
154
|
+
em paralelo comparando resultados antes do cutover final), query replay (reproduzir tráfego real
|
|
155
|
+
de produção contra o destino como teste de carga/compatibilidade), dashboard web, relatórios em
|
|
156
|
+
HTML/PDF (hoje só texto/JSON via `reports/report.py`), mecanismo de plugin formal pra dialetos
|
|
157
|
+
externos (hoje a extensão já é possível via `register_parser`/`register_renderer` — falta só
|
|
158
|
+
empacotar como plugin instalável separadamente, ex. entry points do Python), e suporte a bancos
|
|
159
|
+
não-relacionais como MongoDB (exigiria uma IR paralela pra documentos/coleções — ver
|
|
160
|
+
[docs/compatibility-matrix.md](docs/compatibility-matrix.md#roadmap-além-do-sql-relacional)).
|
|
161
|
+
|
|
162
|
+
Cada fase só avança depois da anterior validada — ver [docs/guide.md](docs/guide.md) para detalhes
|
|
163
|
+
de arquitetura e extensão pra novos dialetos, [docs/cli-reference.md](docs/cli-reference.md) pra
|
|
164
|
+
todos os comandos, [docs/compatibility-matrix.md](docs/compatibility-matrix.md) pro que já
|
|
165
|
+
funciona vs. planejado, [docs/migration-tutorial.md](docs/migration-tutorial.md) pro passo a passo
|
|
166
|
+
completo, e [docs/production-checklist.md](docs/production-checklist.md) antes de rodar contra
|
|
167
|
+
dados reais.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""DBBridge — plataforma genérica de conversão/análise/migração entre bancos de dados, baseada numa
|
|
2
|
+
Intermediate Representation (IR) canônica.
|
|
3
|
+
|
|
4
|
+
Banco origem -> Parser do dialeto origem -> IR -> Renderer do dialeto destino -> Banco destino
|
|
5
|
+
|
|
6
|
+
Não é uma evolução ad-hoc pareada (SQLite->Postgres, SQLite->MySQL, ...) — é uma arquitetura N-para-N
|
|
7
|
+
através de UM modelo comum. Ver core/ir.py pra IR, core/planner.py pro pipeline completo.
|
|
8
|
+
|
|
9
|
+
from dbbridge.core.planner import translate
|
|
10
|
+
result = translate(sql, source_dialect="sqlite", target_dialect="postgres")
|
|
11
|
+
print(result.sql)
|
|
12
|
+
|
|
13
|
+
Fase 1 (atual): IR completa + SQLite <-> PostgreSQL, scanner, codemod, CLI, rollback.
|
|
14
|
+
Dialetos MySQL/SQL Server: estrutura registrada, implementação nas Fases 2/5 (ver README.md).
|
|
15
|
+
"""
|
|
16
|
+
from . import dialects # noqa: F401 — registra sqlite/postgres/mysql(stub)/sqlserver(stub)
|
|
17
|
+
from .compat.runtime import connect
|
|
18
|
+
from .core.planner import TranslationResult, translate
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = ["translate", "TranslationResult", "connect"]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""CLI: dbbridge scan|report|translate|patch|apply|migrate-data|validate|rollback|doctor."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import dialects # noqa: F401 — garante que sqlite/postgres/mysql/sqlserver estão registrados
|
|
9
|
+
from .codemod.codemod import apply_changes, plan_file
|
|
10
|
+
from .core.parser import available_parsers
|
|
11
|
+
from .core.planner import translate as translate_ir
|
|
12
|
+
from .core.renderer import available_renderers
|
|
13
|
+
from .migration.rollback import rollback_code_changes
|
|
14
|
+
from .reports.report import render_report
|
|
15
|
+
from .scanner.scanner import scan_project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _add_common(p: argparse.ArgumentParser):
|
|
19
|
+
p.add_argument("--from", dest="source", required=True, choices=available_parsers(), help="Dialeto de origem")
|
|
20
|
+
p.add_argument("--to", dest="target", required=True, choices=available_renderers(), help="Dialeto de destino")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv=None):
|
|
24
|
+
parser = argparse.ArgumentParser(prog="dbbridge", description="Conversão/análise/migração entre bancos de dados via IR canônica")
|
|
25
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
p_scan = sub.add_parser("scan", help="Varre um projeto Python procurando SQL incompatível entre os dois dialetos")
|
|
28
|
+
_add_common(p_scan)
|
|
29
|
+
p_scan.add_argument("path")
|
|
30
|
+
p_scan.add_argument("--json", action="store_true")
|
|
31
|
+
|
|
32
|
+
p_report = sub.add_parser("report", help="Gera relatório do último scan (texto ou JSON)")
|
|
33
|
+
p_report.add_argument("path")
|
|
34
|
+
p_report.add_argument("--from", dest="source", choices=available_parsers())
|
|
35
|
+
p_report.add_argument("--to", dest="target", choices=available_renderers())
|
|
36
|
+
p_report.add_argument("--json", action="store_true")
|
|
37
|
+
p_report.add_argument("--out", default=None)
|
|
38
|
+
|
|
39
|
+
p_translate = sub.add_parser("translate", help="Traduz um arquivo .sql (schema DDL) de um dialeto pro outro")
|
|
40
|
+
_add_common(p_translate)
|
|
41
|
+
p_translate.add_argument("sql_file")
|
|
42
|
+
p_translate.add_argument("--out", default=None)
|
|
43
|
+
|
|
44
|
+
p_patch = sub.add_parser("patch", help="Gera o plano de mudanças de código SEM escrever nada (dry-run)")
|
|
45
|
+
_add_common(p_patch)
|
|
46
|
+
p_patch.add_argument("path")
|
|
47
|
+
|
|
48
|
+
p_apply = sub.add_parser("apply", help="Aplica as mudanças de código seguras (com backup automático)")
|
|
49
|
+
_add_common(p_apply)
|
|
50
|
+
p_apply.add_argument("path")
|
|
51
|
+
|
|
52
|
+
p_migrate = sub.add_parser("migrate-data", help="Copia dados entre uma conexão origem e uma destino")
|
|
53
|
+
_add_common(p_migrate)
|
|
54
|
+
p_migrate.add_argument("--source-dsn", required=True)
|
|
55
|
+
p_migrate.add_argument("--target-dsn", required=True)
|
|
56
|
+
p_migrate.add_argument("--tables", nargs="+", required=True)
|
|
57
|
+
|
|
58
|
+
p_validate = sub.add_parser("validate", help="Valida contagens/checksums entre origem e destino após migrate-data")
|
|
59
|
+
_add_common(p_validate)
|
|
60
|
+
p_validate.add_argument("--source-dsn", required=True)
|
|
61
|
+
p_validate.add_argument("--target-dsn", required=True)
|
|
62
|
+
p_validate.add_argument("--tables", nargs="+", required=True)
|
|
63
|
+
|
|
64
|
+
p_rollback = sub.add_parser("rollback", help="Restaura arquivos de código a partir do backup mais recente")
|
|
65
|
+
p_rollback.add_argument("path")
|
|
66
|
+
|
|
67
|
+
sub.add_parser("doctor", help="Diagnóstico rápido: dialetos disponíveis, drivers instalados")
|
|
68
|
+
sub.add_parser("dialects", help="Lista os dialetos com parser/renderer disponíveis")
|
|
69
|
+
|
|
70
|
+
args = parser.parse_args(argv)
|
|
71
|
+
|
|
72
|
+
if args.command == "scan":
|
|
73
|
+
report = scan_project(Path(args.path), args.source, args.target)
|
|
74
|
+
print(render_report(f"DBBridge scan ({args.source} -> {args.target})", report, fmt="json" if args.json else "text"))
|
|
75
|
+
|
|
76
|
+
elif args.command == "report":
|
|
77
|
+
report = scan_project(Path(args.path), args.source or "sqlite", args.target or "postgres")
|
|
78
|
+
text = render_report(f"DBBridge report ({args.source} -> {args.target})", report, fmt="json" if args.json else "text")
|
|
79
|
+
if args.out:
|
|
80
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
81
|
+
print(f"Relatório salvo em {args.out}")
|
|
82
|
+
else:
|
|
83
|
+
print(text)
|
|
84
|
+
|
|
85
|
+
elif args.command == "translate":
|
|
86
|
+
sql = Path(args.sql_file).read_text(encoding="utf-8")
|
|
87
|
+
result = translate_ir(sql, args.source, args.target)
|
|
88
|
+
if args.out:
|
|
89
|
+
Path(args.out).write_text(result.sql, encoding="utf-8")
|
|
90
|
+
print(f"SQL traduzido salvo em {args.out}")
|
|
91
|
+
else:
|
|
92
|
+
print(result.sql)
|
|
93
|
+
if result.review_required:
|
|
94
|
+
print("\n--- REVIEW_REQUIRED ---", file=sys.stderr)
|
|
95
|
+
for f in result.parse_findings:
|
|
96
|
+
print(f" [parse] {f.statement[:80]!r}: {f.reason}", file=sys.stderr)
|
|
97
|
+
# Nenhum parser atual produz Trigger/View no IR que gere RenderFinding (CREATE TRIGGER já
|
|
98
|
+
# vira ParseFinding antes de chegar aqui) — mantido para dialetos futuros que possam parsear
|
|
99
|
+
# esses casos com sucesso e só falhar na hora de renderizar pro destino.
|
|
100
|
+
for f in result.render_findings:
|
|
101
|
+
print(f" [render] {f.target}: {f.reason}", file=sys.stderr)
|
|
102
|
+
|
|
103
|
+
elif args.command == "patch":
|
|
104
|
+
total = 0
|
|
105
|
+
for path in sorted(Path(args.path).rglob("*.py")):
|
|
106
|
+
if any(part in ("venv", ".venv", "__pycache__") for part in path.parts):
|
|
107
|
+
continue
|
|
108
|
+
changes = plan_file(path, args.source, args.target)
|
|
109
|
+
if changes:
|
|
110
|
+
print(f"\n{path}:")
|
|
111
|
+
for c in changes:
|
|
112
|
+
print(f" [{c.status}] linha {c.line}: {c.reason}")
|
|
113
|
+
total += len(changes)
|
|
114
|
+
print(f"\nTotal planejado: {total} mudança(s). Nada foi escrito — rode 'apply' pra gravar.")
|
|
115
|
+
|
|
116
|
+
elif args.command == "apply":
|
|
117
|
+
applied_total = 0
|
|
118
|
+
touched = []
|
|
119
|
+
backup_dir = Path(args.path) / ".dbbridge_backups"
|
|
120
|
+
for path in sorted(Path(args.path).rglob("*.py")):
|
|
121
|
+
if any(part in ("venv", ".venv", "__pycache__", ".dbbridge_backups") for part in path.parts):
|
|
122
|
+
continue
|
|
123
|
+
changes = plan_file(path, args.source, args.target)
|
|
124
|
+
if not changes:
|
|
125
|
+
continue
|
|
126
|
+
ok = apply_changes(path, changes, backup_dir)
|
|
127
|
+
if ok:
|
|
128
|
+
n = sum(1 for c in changes if c.status == "APPLIED")
|
|
129
|
+
if n:
|
|
130
|
+
applied_total += n
|
|
131
|
+
touched.append(path)
|
|
132
|
+
print(f"{applied_total} mudança(s) aplicada(s) em {len(touched)} arquivo(s).")
|
|
133
|
+
|
|
134
|
+
elif args.command == "migrate-data":
|
|
135
|
+
from .compat.runtime import connect as db_connect
|
|
136
|
+
from .migration.data_migration import migrate_data
|
|
137
|
+
|
|
138
|
+
source_conn = db_connect(args.source, args.source, args.source_dsn)
|
|
139
|
+
target_conn = db_connect(args.source, args.target, args.target_dsn)
|
|
140
|
+
result = migrate_data(source_conn, target_conn, args.tables)
|
|
141
|
+
print(render_report(f"DBBridge migrate-data ({args.source} -> {args.target})", result))
|
|
142
|
+
if not result.all_counts_match:
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
elif args.command == "validate":
|
|
146
|
+
from .compat.runtime import connect as db_connect
|
|
147
|
+
from .migration.validator import validate
|
|
148
|
+
|
|
149
|
+
source_conn = db_connect(args.source, args.source, args.source_dsn)
|
|
150
|
+
target_conn = db_connect(args.source, args.target, args.target_dsn)
|
|
151
|
+
report = validate(source_conn, target_conn, {t: None for t in args.tables})
|
|
152
|
+
print(render_report(f"DBBridge validate ({args.source} -> {args.target})", report))
|
|
153
|
+
if not report.all_ok:
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
elif args.command == "rollback":
|
|
157
|
+
n = rollback_code_changes(Path(args.path))
|
|
158
|
+
print(f"{n} arquivo(s) restaurado(s) a partir do backup.")
|
|
159
|
+
|
|
160
|
+
elif args.command == "doctor":
|
|
161
|
+
print("Dialetos com parser:", available_parsers())
|
|
162
|
+
print("Dialetos com renderer:", available_renderers())
|
|
163
|
+
try:
|
|
164
|
+
import psycopg # noqa: F401
|
|
165
|
+
print("psycopg (Postgres driver): instalado")
|
|
166
|
+
except ImportError:
|
|
167
|
+
print("psycopg (Postgres driver): NÃO instalado")
|
|
168
|
+
import sqlite3 # noqa: F401
|
|
169
|
+
print("sqlite3 (stdlib): disponível")
|
|
170
|
+
|
|
171
|
+
elif args.command == "dialects":
|
|
172
|
+
print("Parsers disponíveis:", available_parsers())
|
|
173
|
+
print("Renderers disponíveis:", available_renderers())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
main()
|