dbbridgekit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbbridge/__init__.py +22 -0
- dbbridge/cli.py +177 -0
- dbbridge/codemod/__init__.py +3 -0
- dbbridge/codemod/codemod.py +194 -0
- dbbridge/compat/__init__.py +3 -0
- dbbridge/compat/runtime.py +193 -0
- dbbridge/core/__init__.py +2 -0
- dbbridge/core/ir.py +106 -0
- dbbridge/core/parser.py +60 -0
- dbbridge/core/planner.py +44 -0
- dbbridge/core/renderer.py +57 -0
- dbbridge/core/sql_lex.py +61 -0
- dbbridge/core/types.py +47 -0
- dbbridge/dialects/__init__.py +6 -0
- dbbridge/dialects/_stub.py +31 -0
- dbbridge/dialects/mysql.py +16 -0
- dbbridge/dialects/postgres.py +292 -0
- dbbridge/dialects/sqlite.py +279 -0
- dbbridge/dialects/sqlserver.py +15 -0
- dbbridge/migration/__init__.py +11 -0
- dbbridge/migration/data_migration.py +76 -0
- dbbridge/migration/rollback.py +57 -0
- dbbridge/migration/schema_migration.py +56 -0
- dbbridge/migration/validator.py +85 -0
- dbbridge/reports/__init__.py +3 -0
- dbbridge/reports/report.py +58 -0
- dbbridge/scanner/__init__.py +3 -0
- dbbridge/scanner/scanner.py +176 -0
- dbbridgekit-0.1.0.dist-info/METADATA +203 -0
- dbbridgekit-0.1.0.dist-info/RECORD +34 -0
- dbbridgekit-0.1.0.dist-info/WHEEL +5 -0
- dbbridgekit-0.1.0.dist-info/entry_points.txt +2 -0
- dbbridgekit-0.1.0.dist-info/licenses/LICENSE +21 -0
- dbbridgekit-0.1.0.dist-info/top_level.txt +1 -0
dbbridge/core/ir.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""IR (Intermediate Representation) canônica — o modelo comum que todo dialeto converge PRA (parser)
|
|
2
|
+
e converge DE (renderer). Nenhuma classe aqui sabe nada sobre SQLite/Postgres/MySQL especificamente;
|
|
3
|
+
esse é o ponto inteiro da arquitetura (ver core/parser.py e core/renderer.py pros pontos de extensão).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .types import CanonicalType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Column:
|
|
14
|
+
name: str
|
|
15
|
+
type: CanonicalType
|
|
16
|
+
nullable: bool = True
|
|
17
|
+
primary_key: bool = False
|
|
18
|
+
auto_increment: bool = False
|
|
19
|
+
unique: bool = False
|
|
20
|
+
default: str | None = None # texto do literal/expressão de default, já em forma canônica
|
|
21
|
+
length: int | None = None # pra VARCHAR
|
|
22
|
+
precision: int | None = None # pra NUMERIC (dígitos totais)
|
|
23
|
+
scale: int | None = None # pra NUMERIC (dígitos após a vírgula)
|
|
24
|
+
enum_values: list[str] = field(default_factory=list) # pra CanonicalType.ENUM
|
|
25
|
+
comment: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ForeignKey:
|
|
30
|
+
name: str | None
|
|
31
|
+
columns: list[str]
|
|
32
|
+
ref_table: str
|
|
33
|
+
ref_columns: list[str]
|
|
34
|
+
on_delete: str | None = None # 'CASCADE' | 'SET NULL' | 'RESTRICT' | None
|
|
35
|
+
on_update: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class UniqueConstraint:
|
|
40
|
+
name: str | None
|
|
41
|
+
columns: list[str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CheckConstraint:
|
|
46
|
+
name: str | None
|
|
47
|
+
expression: str # expressão booleana em forma canônica simples (ex: "price >= 0")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Index:
|
|
52
|
+
name: str
|
|
53
|
+
columns: list[str]
|
|
54
|
+
unique: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Table:
|
|
59
|
+
name: str
|
|
60
|
+
columns: list[Column] = field(default_factory=list)
|
|
61
|
+
foreign_keys: list[ForeignKey] = field(default_factory=list)
|
|
62
|
+
unique_constraints: list[UniqueConstraint] = field(default_factory=list)
|
|
63
|
+
check_constraints: list[CheckConstraint] = field(default_factory=list)
|
|
64
|
+
indexes: list[Index] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def column(self, name: str) -> Column | None:
|
|
67
|
+
return next((c for c in self.columns if c.name == name), None)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def primary_key_columns(self) -> list[str]:
|
|
71
|
+
return [c.name for c in self.columns if c.primary_key]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class View:
|
|
76
|
+
name: str
|
|
77
|
+
query: str # texto do SELECT em forma canônica simples (sem tradução de sintaxe do dialeto origem)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Trigger:
|
|
82
|
+
name: str
|
|
83
|
+
table: str
|
|
84
|
+
timing: str # 'BEFORE' | 'AFTER' | 'INSTEAD OF'
|
|
85
|
+
event: str # 'INSERT' | 'UPDATE' | 'DELETE'
|
|
86
|
+
body: str # corpo bruto — trigger é o caso mais dialect-specific, quase sempre REVIEW_REQUIRED
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Enum:
|
|
91
|
+
name: str
|
|
92
|
+
values: list[str]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Schema:
|
|
97
|
+
"""Container de tudo que um dialeto consegue extrair de um schema — a unidade que passa pela
|
|
98
|
+
pipeline parser -> IR -> renderer inteira de uma vez."""
|
|
99
|
+
|
|
100
|
+
tables: list[Table] = field(default_factory=list)
|
|
101
|
+
views: list[View] = field(default_factory=list)
|
|
102
|
+
triggers: list[Trigger] = field(default_factory=list)
|
|
103
|
+
enums: list[Enum] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
def table(self, name: str) -> Table | None:
|
|
106
|
+
return next((t for t in self.tables if t.name == name), None)
|
dbbridge/core/parser.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Contrato base do parser: todo dialeto implementa `DialectParser.parse(sql) -> ParseResult`,
|
|
2
|
+
convertendo DDL do dialeto pra `Schema` (IR). Não é um parser SQL genérico de propósito — cada
|
|
3
|
+
dialeto reconhece SÓ o subconjunto de sintaxe que ele mesmo produz/aceita, e qualquer coisa que não
|
|
4
|
+
reconheça vira uma `Ambiguity` reportada, nunca uma adivinhação.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from .ir import Schema
|
|
12
|
+
from .types import Ambiguity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ParseFinding:
|
|
17
|
+
kind: Ambiguity
|
|
18
|
+
statement: str
|
|
19
|
+
reason: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ParseResult:
|
|
24
|
+
schema: Schema
|
|
25
|
+
findings: list[ParseFinding] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def ok(self) -> bool:
|
|
29
|
+
return len(self.findings) == 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DialectParser(ABC):
|
|
33
|
+
"""Uma instância por dialeto (ver dialects/sqlite.py, dialects/postgres.py)."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def parse(self, sql: str) -> ParseResult:
|
|
39
|
+
"""Recebe um script SQL (1+ statements) e devolve o Schema (IR) reconhecido, mais qualquer
|
|
40
|
+
finding de statement não reconhecido (nunca lançar exceção pra DDL desconhecido — reportar)."""
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_PARSERS: dict[str, DialectParser] = {}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register_parser(dialect: str, parser: DialectParser) -> None:
|
|
48
|
+
_PARSERS[dialect] = parser
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_parser(dialect: str) -> DialectParser:
|
|
52
|
+
try:
|
|
53
|
+
return _PARSERS[dialect]
|
|
54
|
+
except KeyError:
|
|
55
|
+
raise ValueError(f"Dialeto sem parser registrado: {dialect!r}. "
|
|
56
|
+
f"Disponíveis: {sorted(_PARSERS)}") from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def available_parsers() -> list[str]:
|
|
60
|
+
return sorted(_PARSERS)
|
dbbridge/core/planner.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Orquestra o pipeline inteiro: SQL origem -> parser do dialeto origem -> IR -> renderer do dialeto
|
|
2
|
+
destino -> SQL destino. É o módulo que `cli.py translate`/`scan`/`patch` chamam — a única peça que
|
|
3
|
+
precisa saber que a pipeline tem essas 2 etapas; parsers/renderers continuam sem se conhecerem.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .ir import Schema
|
|
10
|
+
from .parser import ParseFinding, get_parser
|
|
11
|
+
from .renderer import RenderFinding, get_renderer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TranslationResult:
|
|
16
|
+
source_dialect: str
|
|
17
|
+
target_dialect: str
|
|
18
|
+
schema: Schema
|
|
19
|
+
sql: str
|
|
20
|
+
parse_findings: list[ParseFinding] = field(default_factory=list)
|
|
21
|
+
render_findings: list[RenderFinding] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def review_required(self) -> bool:
|
|
25
|
+
return bool(self.parse_findings or self.render_findings)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def translate(sql: str, source_dialect: str, target_dialect: str) -> TranslationResult:
|
|
29
|
+
"""Ponto de entrada único da pipeline completa. Nunca lança por DDL não reconhecido — reporta via
|
|
30
|
+
`parse_findings`/`render_findings` e devolve o que CONSEGUIU converter, marcando o resto."""
|
|
31
|
+
parser = get_parser(source_dialect)
|
|
32
|
+
renderer = get_renderer(target_dialect)
|
|
33
|
+
|
|
34
|
+
parse_result = parser.parse(sql)
|
|
35
|
+
render_result = renderer.render_schema(parse_result.schema)
|
|
36
|
+
|
|
37
|
+
return TranslationResult(
|
|
38
|
+
source_dialect=source_dialect,
|
|
39
|
+
target_dialect=target_dialect,
|
|
40
|
+
schema=parse_result.schema,
|
|
41
|
+
sql=render_result.sql,
|
|
42
|
+
parse_findings=parse_result.findings,
|
|
43
|
+
render_findings=render_result.findings,
|
|
44
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Contrato base do renderer: todo dialeto implementa `DialectRenderer.render_schema(schema) -> str`,
|
|
2
|
+
convertendo a IR de volta pra DDL do dialeto destino. Onde a IR não tem informação suficiente pra
|
|
3
|
+
gerar algo seguro (ex: Trigger — corpo é texto bruto do dialeto ORIGEM, quase sempre incompatível
|
|
4
|
+
com o destino), o renderer devolve isso como uma `RenderFinding` REVIEW_REQUIRED em vez de copiar
|
|
5
|
+
o texto original cegamente (isso seria "inventar tradução perigosa").
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
from .ir import Schema
|
|
13
|
+
from .types import Ambiguity
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RenderFinding:
|
|
18
|
+
kind: Ambiguity
|
|
19
|
+
target: str # nome da tabela/view/trigger afetada
|
|
20
|
+
reason: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RenderResult:
|
|
25
|
+
sql: str
|
|
26
|
+
findings: list[RenderFinding] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def ok(self) -> bool:
|
|
30
|
+
return len(self.findings) == 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DialectRenderer(ABC):
|
|
34
|
+
name: str
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def render_schema(self, schema: Schema) -> RenderResult:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_RENDERERS: dict[str, DialectRenderer] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register_renderer(dialect: str, renderer: DialectRenderer) -> None:
|
|
45
|
+
_RENDERERS[dialect] = renderer
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_renderer(dialect: str) -> DialectRenderer:
|
|
49
|
+
try:
|
|
50
|
+
return _RENDERERS[dialect]
|
|
51
|
+
except KeyError:
|
|
52
|
+
raise ValueError(f"Dialeto sem renderer registrado: {dialect!r}. "
|
|
53
|
+
f"Disponíveis: {sorted(_RENDERERS)}") from None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def available_renderers() -> list[str]:
|
|
57
|
+
return sorted(_RENDERERS)
|
dbbridge/core/sql_lex.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Utilitários de split de texto SQL respeitando parênteses e aspas — usados por TODO dialeto pra
|
|
2
|
+
separar statements (`;`) e itens de coluna dentro de um `CREATE TABLE(...)` (`,`). Não é um
|
|
3
|
+
tokenizer/parser SQL completo (não entende gramática, só profundidade de parênteses e strings) —
|
|
4
|
+
o suficiente pra dividir texto corretamente sem cortar no meio de uma expressão como
|
|
5
|
+
`CHECK(price >= 0 AND price <= 1000)` ou um default `DEFAULT 'a, b'`.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _split_respecting_parens_and_quotes(text: str, sep: str) -> list[str]:
|
|
11
|
+
parts: list[str] = []
|
|
12
|
+
depth = 0
|
|
13
|
+
quote_char: str | None = None
|
|
14
|
+
current: list[str] = []
|
|
15
|
+
i = 0
|
|
16
|
+
while i < len(text):
|
|
17
|
+
ch = text[i]
|
|
18
|
+
if quote_char:
|
|
19
|
+
current.append(ch)
|
|
20
|
+
if ch == quote_char:
|
|
21
|
+
# aspa duplicada dentro de string ('' ou "") = escape, não fecha a string
|
|
22
|
+
if i + 1 < len(text) and text[i + 1] == quote_char:
|
|
23
|
+
current.append(text[i + 1])
|
|
24
|
+
i += 1
|
|
25
|
+
else:
|
|
26
|
+
quote_char = None
|
|
27
|
+
elif ch in ("'", '"'):
|
|
28
|
+
quote_char = ch
|
|
29
|
+
current.append(ch)
|
|
30
|
+
elif ch == "(":
|
|
31
|
+
depth += 1
|
|
32
|
+
current.append(ch)
|
|
33
|
+
elif ch == ")":
|
|
34
|
+
depth -= 1
|
|
35
|
+
current.append(ch)
|
|
36
|
+
elif ch == sep and depth == 0:
|
|
37
|
+
parts.append("".join(current))
|
|
38
|
+
current = []
|
|
39
|
+
else:
|
|
40
|
+
current.append(ch)
|
|
41
|
+
i += 1
|
|
42
|
+
if current:
|
|
43
|
+
parts.append("".join(current))
|
|
44
|
+
return [p.strip() for p in parts if p.strip()]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def split_statements(sql: str) -> list[str]:
|
|
48
|
+
"""Separa um script em statements individuais por `;` no nível 0 de parênteses/aspas."""
|
|
49
|
+
return _split_respecting_parens_and_quotes(sql, ";")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def split_top_level_commas(text: str) -> list[str]:
|
|
53
|
+
"""Separa por `,` no nível 0 — usado pra dividir os itens dentro de `CREATE TABLE nome(...)`."""
|
|
54
|
+
return _split_respecting_parens_and_quotes(text, ",")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def strip_outer_parens(text: str) -> str:
|
|
58
|
+
text = text.strip()
|
|
59
|
+
if text.startswith("(") and text.endswith(")"):
|
|
60
|
+
return text[1:-1].strip()
|
|
61
|
+
return text
|
dbbridge/core/types.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Sistema de tipos canônico — o vocabulário comum que todo dialeto mapeia PARA (no parser) e DE
|
|
2
|
+
(no renderer). Isso é o que permite N dialetos se conectarem através de UM ponto central em vez de
|
|
3
|
+
N×N conversores diretos: cada dialeto só precisa saber conversar com o `CanonicalType`, nunca com
|
|
4
|
+
os outros dialetos diretamente.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CanonicalType(str, Enum):
|
|
12
|
+
"""Vocabulário de tipos que a IR entende. Deliberadamente um conjunto PEQUENO e bem definido —
|
|
13
|
+
cobre o que os dialetos suportados (Fase 1: SQLite/Postgres) realmente precisam representar,
|
|
14
|
+
não uma tentativa de modelar todo tipo de coluna que já existiu em algum banco."""
|
|
15
|
+
|
|
16
|
+
INTEGER = "integer"
|
|
17
|
+
SMALLINT = "smallint"
|
|
18
|
+
BIGINT = "bigint"
|
|
19
|
+
FLOAT = "float"
|
|
20
|
+
NUMERIC = "numeric" # precisão/escala arbitrária (DECIMAL/NUMERIC)
|
|
21
|
+
BOOLEAN = "boolean"
|
|
22
|
+
TEXT = "text"
|
|
23
|
+
VARCHAR = "varchar" # string de tamanho limitado (tem `length`)
|
|
24
|
+
DATE = "date"
|
|
25
|
+
TIME = "time"
|
|
26
|
+
DATETIME = "datetime" # sem timezone
|
|
27
|
+
TIMESTAMP_TZ = "timestamp_tz"
|
|
28
|
+
JSON = "json"
|
|
29
|
+
UUID = "uuid"
|
|
30
|
+
BINARY = "binary" # BLOB/BYTEA
|
|
31
|
+
ENUM = "enum" # valores fixos — ver Column.enum_values
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Tipos que exigem um parâmetro extra pra renderizar direito (tamanho, precisão/escala, valores de enum).
|
|
35
|
+
TYPES_WITH_LENGTH = {CanonicalType.VARCHAR}
|
|
36
|
+
TYPES_WITH_PRECISION = {CanonicalType.NUMERIC}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Ambiguity(str, Enum):
|
|
40
|
+
"""Por que uma tradução NÃO foi feita automaticamente — usado tanto pelo parser (DDL não
|
|
41
|
+
reconhecido) quanto pelo scanner (padrão de código arriscado)."""
|
|
42
|
+
|
|
43
|
+
UNRECOGNIZED_DDL = "unrecognized_ddl"
|
|
44
|
+
UNSUPPORTED_TYPE = "unsupported_type"
|
|
45
|
+
AMBIGUOUS_DEFAULT = "ambiguous_default"
|
|
46
|
+
UNSUPPORTED_EXPRESSION = "unsupported_expression"
|
|
47
|
+
MULTIPLE_STATEMENTS_IN_ONE = "multiple_statements_in_one"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Implementações por dialeto. Importar este pacote registra todos os dialetos disponíveis nos
|
|
2
|
+
registries de core/parser.py e core/renderer.py (via register_parser/register_renderer chamados no
|
|
3
|
+
import de cada módulo de dialeto). Dialetos "estrutura preparada" (mysql, sqlserver — Fases 2/5)
|
|
4
|
+
aparecem em `available_parsers()`/`available_renderers()` mas levantam `NotImplementedError` com
|
|
5
|
+
mensagem clara SE alguém tentar usá-los de verdade — nunca na importação."""
|
|
6
|
+
from . import mysql, postgres, sqlite, sqlserver # noqa: F401 — o import já registra os dialetos
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base compartilhada dos dialetos "estrutura preparada, não implementados ainda" (mysql, sqlserver
|
|
2
|
+
nesta fase — ver README.md/ENTREGA do projeto pra fases). Registrar um stub (em vez de simplesmente
|
|
3
|
+
não registrar nada) permite que `available_parsers()`/`available_renderers()` e a CLI mostrem esses
|
|
4
|
+
dialetos como "conhecidos, ainda não implementados" com uma mensagem clara — melhor experiência que
|
|
5
|
+
um erro genérico de "dialeto desconhecido"."""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..core.parser import DialectParser, ParseResult
|
|
9
|
+
from ..core.renderer import DialectRenderer, RenderResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotImplementedDialectParser(DialectParser):
|
|
13
|
+
def __init__(self, name: str, phase: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
self._phase = phase
|
|
16
|
+
|
|
17
|
+
def parse(self, sql: str) -> ParseResult:
|
|
18
|
+
raise NotImplementedError(
|
|
19
|
+
f"Parser do dialeto '{self.name}' ainda não implementado (planejado pra {self._phase}). "
|
|
20
|
+
f"Estrutura do módulo já existe em dialects/{self.name}.py — ver README.md/ROADMAP.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NotImplementedDialectRenderer(DialectRenderer):
|
|
24
|
+
def __init__(self, name: str, phase: str):
|
|
25
|
+
self.name = name
|
|
26
|
+
self._phase = phase
|
|
27
|
+
|
|
28
|
+
def render_schema(self, schema) -> RenderResult:
|
|
29
|
+
raise NotImplementedError(
|
|
30
|
+
f"Renderer do dialeto '{self.name}' ainda não implementado (planejado pra {self._phase}). "
|
|
31
|
+
f"Estrutura do módulo já existe em dialects/{self.name}.py — ver README.md/ROADMAP.")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Dialeto MySQL — estrutura preparada (Fase 2 do roadmap), ainda não implementado.
|
|
2
|
+
|
|
3
|
+
Quando chegar a hora: mapa de tipos (INT/TINYINT/AUTO_INCREMENT/ENUM nativo/JSON/DATETIME), parser
|
|
4
|
+
de CREATE TABLE no mesmo padrão de dialects/sqlite.py e dialects/postgres.py (reaproveitando
|
|
5
|
+
core/sql_lex.py pra split de statements/colunas), tratamento de `ON UPDATE CURRENT_TIMESTAMP`
|
|
6
|
+
(peculiaridade MySQL sem equivalente direto) e `ENGINE=InnoDB`/charset (ignorados na IR — são
|
|
7
|
+
metadados de armazenamento, não de schema lógico).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ..core.parser import register_parser
|
|
12
|
+
from ..core.renderer import register_renderer
|
|
13
|
+
from ._stub import NotImplementedDialectParser, NotImplementedDialectRenderer
|
|
14
|
+
|
|
15
|
+
register_parser("mysql", NotImplementedDialectParser("mysql", "Fase 2"))
|
|
16
|
+
register_renderer("mysql", NotImplementedDialectRenderer("mysql", "Fase 2"))
|