@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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbspec
3
- Version: 1.5.9
3
+ Version: 1.5.12
4
4
  Summary: DBSpec: CLI/TUI for database design (Excel as DSL)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbspec"
7
- version = "1.5.9"
7
+ version = "1.5.12"
8
8
  description = "DBSpec: CLI/TUI for database design (Excel as DSL)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.
@@ -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
@@ -24,5 +24,5 @@
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },
27
- "version": "1.5.9"
27
+ "version": "1.5.12"
28
28
  }