@ai-devtools/dbspec-linux-x64 1.5.9 → 1.5.12
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.
- package/bin/dbspec/_internal/base_library.zip +0 -0
- package/bin/dbspec/_internal/dbspec.egg-info/PKG-INFO +1 -1
- package/bin/dbspec/_internal/dbspec.egg-info/SOURCES.txt +2 -0
- package/bin/dbspec/_internal/pyproject.toml +1 -1
- package/bin/dbspec/_internal/src/resources/sqlalchemy_templates/README.md +11 -0
- package/bin/dbspec/_internal/src/resources/sqlalchemy_templates/alembic_default_comparator.py.j2 +213 -0
- package/bin/dbspec/dbspec +0 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -126,6 +126,7 @@ src/models/tech.py
|
|
|
126
126
|
src/resources/__init__.py
|
|
127
127
|
src/resources/sqlalchemy_templates/README.md
|
|
128
128
|
src/resources/sqlalchemy_templates/__init__.py
|
|
129
|
+
src/resources/sqlalchemy_templates/alembic_default_comparator.py.j2
|
|
129
130
|
src/resources/sqlalchemy_templates/base.py.j2
|
|
130
131
|
src/resources/sqlalchemy_templates/collation_helper.py.j2
|
|
131
132
|
src/resources/sqlalchemy_templates/domain_init.py.j2
|
|
@@ -133,6 +134,7 @@ src/resources/sqlalchemy_templates/model.py.j2
|
|
|
133
134
|
src/resources/sqlalchemy_templates/package_init.py.j2
|
|
134
135
|
src/reveng/__init__.py
|
|
135
136
|
src/storage/__init__.py
|
|
137
|
+
src/storage/alembic_env_patching.py
|
|
136
138
|
src/storage/atomic.py
|
|
137
139
|
src/storage/config.py
|
|
138
140
|
src/storage/file_utils.py
|
|
@@ -57,3 +57,14 @@
|
|
|
57
57
|
### `domain_init.py.j2`
|
|
58
58
|
Ключи контекста:
|
|
59
59
|
- `domain` - текущий домен с `tables` (module_name, class_name)
|
|
60
|
+
|
|
61
|
+
## Автономный Alembic comparator
|
|
62
|
+
|
|
63
|
+
`alembic_default_comparator.py.j2` содержит канонический Python-код без
|
|
64
|
+
Jinja-параметров, зависящий только от стандартной библиотеки. DBSpec исполняет этот
|
|
65
|
+
же ресурс для собственного сравнения defaults и разбора sequence identifiers;
|
|
66
|
+
инициализация встраивает его в `env.py` потребителя между управляемыми маркерами.
|
|
67
|
+
Так сохраняется единая реализация и не создаётся runtime-зависимость приложения
|
|
68
|
+
от исходников или пакета DBSpec. Ресурс входит в существующее пакетирование `*.j2`
|
|
69
|
+
и сборку frozen CLI; изменения проверяются тестами scanner/comparator и
|
|
70
|
+
изолированным subprocess без site-packages.
|
package/bin/dbspec/_internal/src/resources/sqlalchemy_templates/alembic_default_comparator.py.j2
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Alembic server-default comparison helpers."""
|
|
2
|
+
|
|
3
|
+
import re as _dbspec_re
|
|
4
|
+
from typing import Any as _DBSpecAny
|
|
5
|
+
|
|
6
|
+
from collections.abc import Iterator as _DBSpecIterator
|
|
7
|
+
|
|
8
|
+
_dbspec_NEXTVAL_RE = _dbspec_re.compile(
|
|
9
|
+
r"\bnextval\s*\(\s*'((?:[^']|'')+)'\s*(?:::\s*(?:pg_catalog\.)?regclass)?\s*\)",
|
|
10
|
+
_dbspec_re.IGNORECASE,
|
|
11
|
+
)
|
|
12
|
+
_dbspec_SEQUENCE_IDENTIFIER_RE = _dbspec_re.compile(r'"((?:[^"]|"")+)"|([^\W\d][\w$]*)')
|
|
13
|
+
_dbspec_DOLLAR_QUOTE_RE = _dbspec_re.compile(r"\$(?:[^\W\d]\w*)?\$")
|
|
14
|
+
_dbspec_INVALID_SEQUENCE_IDENTIFIER = "Invalid nextval sequence identifier"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dbspec_sequence_identity(value: str) -> tuple[str, ...]:
|
|
18
|
+
"""Parse SQL identifiers without accepting SQL statement fragments."""
|
|
19
|
+
parts: list[str] = []
|
|
20
|
+
end = 0
|
|
21
|
+
for match in _dbspec_SEQUENCE_IDENTIFIER_RE.finditer(value):
|
|
22
|
+
separator = value[end : match.start()].strip()
|
|
23
|
+
if separator != ("." if parts else ""):
|
|
24
|
+
raise ValueError(f"{_dbspec_INVALID_SEQUENCE_IDENTIFIER}: {value!r}")
|
|
25
|
+
quoted, unquoted = match.groups()
|
|
26
|
+
parts.append(
|
|
27
|
+
quoted.replace('""', '"')
|
|
28
|
+
if quoted is not None
|
|
29
|
+
else "".join(char.lower() if char.isascii() else char for char in unquoted)
|
|
30
|
+
)
|
|
31
|
+
end = match.end()
|
|
32
|
+
if value[end:].strip() or len(parts) not in (1, 2):
|
|
33
|
+
raise ValueError(f"{_dbspec_INVALID_SEQUENCE_IDENTIFIER}: {value!r}")
|
|
34
|
+
return tuple(parts)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dbspec_executed_nextval_arguments(sql: str) -> _DBSpecIterator[str]:
|
|
38
|
+
"""Find calls outside PostgreSQL literals, identifiers and nested comments."""
|
|
39
|
+
position = 0
|
|
40
|
+
while position < len(sql):
|
|
41
|
+
if sql.startswith("--", position):
|
|
42
|
+
newline = sql.find("\n", position)
|
|
43
|
+
position = len(sql) if newline < 0 else newline + 1
|
|
44
|
+
elif sql.startswith("/*", position):
|
|
45
|
+
depth = 1
|
|
46
|
+
position += 2
|
|
47
|
+
while position < len(sql) and depth:
|
|
48
|
+
if sql.startswith("/*", position):
|
|
49
|
+
depth += 1
|
|
50
|
+
position += 2
|
|
51
|
+
elif sql.startswith("*/", position):
|
|
52
|
+
depth -= 1
|
|
53
|
+
position += 2
|
|
54
|
+
else:
|
|
55
|
+
position += 1
|
|
56
|
+
elif sql[position] in ("'", '"'):
|
|
57
|
+
quote = sql[position]
|
|
58
|
+
escaped_string = (
|
|
59
|
+
quote == "'"
|
|
60
|
+
and position > 0
|
|
61
|
+
and sql[position - 1] in "eE"
|
|
62
|
+
and (
|
|
63
|
+
position < 2
|
|
64
|
+
or not (sql[position - 2].isalnum() or sql[position - 2] in "_$")
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
position += 1
|
|
68
|
+
while position < len(sql):
|
|
69
|
+
if sql.startswith(quote * 2, position):
|
|
70
|
+
position += 2
|
|
71
|
+
elif sql[position] == quote:
|
|
72
|
+
position += 1
|
|
73
|
+
break
|
|
74
|
+
elif sql[position] == "\\" and escaped_string:
|
|
75
|
+
position += 2
|
|
76
|
+
else:
|
|
77
|
+
position += 1
|
|
78
|
+
elif match := _dbspec_DOLLAR_QUOTE_RE.match(sql, position):
|
|
79
|
+
closing = sql.find(match[0], match.end())
|
|
80
|
+
position = len(sql) if closing < 0 else closing + len(match[0])
|
|
81
|
+
elif match := _dbspec_NEXTVAL_RE.match(sql, position):
|
|
82
|
+
yield match[1]
|
|
83
|
+
position = match.end()
|
|
84
|
+
else:
|
|
85
|
+
position += 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_dbspec_CAST_RE = _dbspec_re.compile(
|
|
89
|
+
r"::\s*(?:pg_catalog\.)?[a-z_][\w. ]*(?:\(\s*\d+\s*\))?$", _dbspec_re.I
|
|
90
|
+
)
|
|
91
|
+
_dbspec_INTEGER_RE = _dbspec_re.compile(r"[+-]?\d+")
|
|
92
|
+
_dbspec_WHITESPACE_RE = _dbspec_re.compile(r"\s+")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def dbspec_compare_server_default(
|
|
96
|
+
_context: _DBSpecAny,
|
|
97
|
+
inspected_column: _DBSpecAny,
|
|
98
|
+
metadata_column: _DBSpecAny,
|
|
99
|
+
inspected_default: _DBSpecAny,
|
|
100
|
+
metadata_default: _DBSpecAny,
|
|
101
|
+
rendered_metadata_default: _DBSpecAny,
|
|
102
|
+
) -> bool | None:
|
|
103
|
+
"""Compare PostgreSQL defaults without hiding missing sequence defaults."""
|
|
104
|
+
current = _dbspec_normalize_default(inspected_default, inspected_column)
|
|
105
|
+
target = _dbspec_normalize_default(
|
|
106
|
+
(
|
|
107
|
+
rendered_metadata_default
|
|
108
|
+
if rendered_metadata_default is not None
|
|
109
|
+
else metadata_default
|
|
110
|
+
),
|
|
111
|
+
metadata_column,
|
|
112
|
+
)
|
|
113
|
+
if current == target:
|
|
114
|
+
return False
|
|
115
|
+
if current is None or target is None:
|
|
116
|
+
return True
|
|
117
|
+
if current[0] == "nextval" or target[0] == "nextval":
|
|
118
|
+
return True
|
|
119
|
+
if current[0] == "literal" and target[0] == "literal":
|
|
120
|
+
return current[1] != target[1]
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _dbspec_normalize_default(
|
|
125
|
+
value: _DBSpecAny, column: _DBSpecAny
|
|
126
|
+
) -> tuple[str, tuple[str, ...] | str] | None:
|
|
127
|
+
if value is None:
|
|
128
|
+
return None
|
|
129
|
+
text = str(value).strip()
|
|
130
|
+
if not text:
|
|
131
|
+
return None
|
|
132
|
+
sequence = _dbspec_normalize_nextval(text, column)
|
|
133
|
+
if sequence is not None:
|
|
134
|
+
return ("nextval", sequence)
|
|
135
|
+
return ("literal", _dbspec_normalize_literal_default(text))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _dbspec_normalize_nextval(value: str, column: _DBSpecAny) -> tuple[str, ...] | None:
|
|
139
|
+
try:
|
|
140
|
+
arguments = list(_dbspec_executed_nextval_arguments(value))
|
|
141
|
+
if not arguments:
|
|
142
|
+
return None
|
|
143
|
+
identity = _dbspec_sequence_identity(arguments[0].replace("''", "'"))
|
|
144
|
+
except ValueError:
|
|
145
|
+
return None
|
|
146
|
+
if len(identity) == 1:
|
|
147
|
+
schema = _dbspec_column_schema(column)
|
|
148
|
+
if schema:
|
|
149
|
+
return (schema, identity[0])
|
|
150
|
+
return identity
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _dbspec_column_schema(column: _DBSpecAny) -> str | None:
|
|
154
|
+
table = getattr(column, "table", None)
|
|
155
|
+
schema = getattr(table, "schema", None)
|
|
156
|
+
return schema if isinstance(schema, str) and schema else None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _dbspec_normalize_literal_default(value: str) -> str:
|
|
160
|
+
text = value.strip()
|
|
161
|
+
lowered = text.lower()
|
|
162
|
+
if lowered.startswith("default "):
|
|
163
|
+
text = text[8:].strip()
|
|
164
|
+
text = _dbspec_strip_balanced_parentheses(text)
|
|
165
|
+
text = _dbspec_CAST_RE.sub("", text).strip()
|
|
166
|
+
text = _dbspec_strip_balanced_parentheses(text)
|
|
167
|
+
lowered = text.lower()
|
|
168
|
+
if lowered in {"true", "'true'", "1"}:
|
|
169
|
+
return "true"
|
|
170
|
+
if lowered in {"false", "'false'", "0"}:
|
|
171
|
+
return "false"
|
|
172
|
+
if _dbspec_INTEGER_RE.fullmatch(text):
|
|
173
|
+
return str(int(text))
|
|
174
|
+
return _dbspec_WHITESPACE_RE.sub(" ", lowered)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dbspec_strip_balanced_parentheses(value: str) -> str:
|
|
178
|
+
text = value.strip()
|
|
179
|
+
while (
|
|
180
|
+
text.startswith("(")
|
|
181
|
+
and text.endswith(")")
|
|
182
|
+
and _dbspec_has_wrapping_parentheses(text)
|
|
183
|
+
):
|
|
184
|
+
text = text[1:-1].strip()
|
|
185
|
+
return text
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _dbspec_has_wrapping_parentheses(value: str) -> bool:
|
|
189
|
+
depth = 0
|
|
190
|
+
quote: str | None = None
|
|
191
|
+
index = 0
|
|
192
|
+
while index < len(value):
|
|
193
|
+
char = value[index]
|
|
194
|
+
if quote is not None:
|
|
195
|
+
if char == quote:
|
|
196
|
+
if index + 1 < len(value) and value[index + 1] == quote:
|
|
197
|
+
index += 2
|
|
198
|
+
continue
|
|
199
|
+
quote = None
|
|
200
|
+
index += 1
|
|
201
|
+
continue
|
|
202
|
+
if char in {"'", '"'}:
|
|
203
|
+
quote = char
|
|
204
|
+
elif char == "(":
|
|
205
|
+
depth += 1
|
|
206
|
+
elif char == ")":
|
|
207
|
+
depth -= 1
|
|
208
|
+
if depth == 0 and index != len(value) - 1:
|
|
209
|
+
return False
|
|
210
|
+
if depth < 0:
|
|
211
|
+
return False
|
|
212
|
+
index += 1
|
|
213
|
+
return depth == 0
|
package/bin/dbspec/dbspec
CHANGED
|
Binary file
|
package/package.json
CHANGED