buildai-cli 0.3.67__tar.gz → 0.3.68__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.
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/.gitignore +1 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/PKG-INFO +1 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/common.py +136 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/migrate.py +19 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/pyproject.toml +1 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/AGENTS.md +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/CLAUDE.md +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/config.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/console.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/context.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/guard.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/main.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/output.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.68}/cli/pagination.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
@@ -20,6 +21,11 @@ MIGRATIONS_SA_DB_USER = "buildai-migrations-sa@data-470400.iam"
|
|
|
20
21
|
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
21
22
|
MIGRATIONS_DIR = REPO_ROOT / "migrations"
|
|
22
23
|
MIGRATIONS_TABLE = "public._migrations"
|
|
24
|
+
_NO_TRANSACTION_MARKER = "-- requires-no-transaction: true"
|
|
25
|
+
_CONCURRENT_DDL_PATTERN = re.compile(
|
|
26
|
+
r"\b(?:CREATE|DROP|REINDEX|REFRESH)\s+(?:MATERIALIZED\s+VIEW\s+)?(?:INDEX|TABLE|VIEW)?\s*CONCURRENTLY\b",
|
|
27
|
+
flags=re.IGNORECASE,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
@dataclass
|
|
@@ -140,3 +146,133 @@ def migration_requires_system_admin(path: Path) -> bool:
|
|
|
140
146
|
"""Detect migrations that must run with the DB admin connection."""
|
|
141
147
|
|
|
142
148
|
return "requires-system-admin" in path.read_text(encoding="utf-8", errors="ignore")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def migration_requires_manual_apply(path: Path) -> bool:
|
|
152
|
+
"""Detect migrations that must be explicitly selected by an operator."""
|
|
153
|
+
|
|
154
|
+
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines()[:10]:
|
|
155
|
+
if line.strip().lower() == "-- requires-manual-apply: true":
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def migration_requires_no_transaction(path: Path, sql: str) -> bool:
|
|
161
|
+
"""Return whether a migration must be executed statement-by-statement."""
|
|
162
|
+
|
|
163
|
+
header = "\n".join(sql.splitlines()[:10]).lower()
|
|
164
|
+
if _NO_TRANSACTION_MARKER in header:
|
|
165
|
+
return True
|
|
166
|
+
return _CONCURRENT_DDL_PATTERN.search(sql) is not None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def split_sql_statements(sql: str) -> list[str]:
|
|
170
|
+
"""Split SQL into executable statements while respecting quotes and DO blocks."""
|
|
171
|
+
|
|
172
|
+
statements: list[str] = []
|
|
173
|
+
chunk: list[str] = []
|
|
174
|
+
in_single_quote = False
|
|
175
|
+
in_double_quote = False
|
|
176
|
+
in_line_comment = False
|
|
177
|
+
in_block_comment = False
|
|
178
|
+
dollar_tag: str | None = None
|
|
179
|
+
i = 0
|
|
180
|
+
|
|
181
|
+
while i < len(sql):
|
|
182
|
+
char = sql[i]
|
|
183
|
+
next_char = sql[i + 1] if i + 1 < len(sql) else ""
|
|
184
|
+
|
|
185
|
+
if in_line_comment:
|
|
186
|
+
chunk.append(char)
|
|
187
|
+
if char == "\n":
|
|
188
|
+
in_line_comment = False
|
|
189
|
+
i += 1
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if in_block_comment:
|
|
193
|
+
chunk.append(char)
|
|
194
|
+
if char == "*" and next_char == "/":
|
|
195
|
+
chunk.append(next_char)
|
|
196
|
+
in_block_comment = False
|
|
197
|
+
i += 2
|
|
198
|
+
else:
|
|
199
|
+
i += 1
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if dollar_tag is not None:
|
|
203
|
+
if sql.startswith(dollar_tag, i):
|
|
204
|
+
chunk.append(dollar_tag)
|
|
205
|
+
i += len(dollar_tag)
|
|
206
|
+
dollar_tag = None
|
|
207
|
+
else:
|
|
208
|
+
chunk.append(char)
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if in_single_quote:
|
|
213
|
+
chunk.append(char)
|
|
214
|
+
if char == "'" and next_char == "'":
|
|
215
|
+
chunk.append(next_char)
|
|
216
|
+
i += 2
|
|
217
|
+
continue
|
|
218
|
+
if char == "'":
|
|
219
|
+
in_single_quote = False
|
|
220
|
+
i += 1
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
if in_double_quote:
|
|
224
|
+
chunk.append(char)
|
|
225
|
+
if char == '"':
|
|
226
|
+
in_double_quote = False
|
|
227
|
+
i += 1
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if char == "-" and next_char == "-":
|
|
231
|
+
chunk.append(char)
|
|
232
|
+
chunk.append(next_char)
|
|
233
|
+
in_line_comment = True
|
|
234
|
+
i += 2
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if char == "/" and next_char == "*":
|
|
238
|
+
chunk.append(char)
|
|
239
|
+
chunk.append(next_char)
|
|
240
|
+
in_block_comment = True
|
|
241
|
+
i += 2
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if char == "'":
|
|
245
|
+
chunk.append(char)
|
|
246
|
+
in_single_quote = True
|
|
247
|
+
i += 1
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if char == '"':
|
|
251
|
+
chunk.append(char)
|
|
252
|
+
in_double_quote = True
|
|
253
|
+
i += 1
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if char == "$":
|
|
257
|
+
match = re.match(r"\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$", sql[i:])
|
|
258
|
+
if match:
|
|
259
|
+
dollar_tag = match.group(0)
|
|
260
|
+
chunk.append(dollar_tag)
|
|
261
|
+
i += len(dollar_tag)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if char == ";":
|
|
265
|
+
statement = "".join(chunk).strip()
|
|
266
|
+
if statement:
|
|
267
|
+
statements.append(statement)
|
|
268
|
+
chunk = []
|
|
269
|
+
i += 1
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
chunk.append(char)
|
|
273
|
+
i += 1
|
|
274
|
+
|
|
275
|
+
trailing = "".join(chunk).strip()
|
|
276
|
+
if trailing:
|
|
277
|
+
statements.append(trailing)
|
|
278
|
+
return statements
|
|
@@ -20,8 +20,11 @@ from .common import (
|
|
|
20
20
|
get_migration_status,
|
|
21
21
|
get_migrations_connection,
|
|
22
22
|
migration_label,
|
|
23
|
+
migration_requires_manual_apply,
|
|
24
|
+
migration_requires_no_transaction,
|
|
23
25
|
migration_requires_system_admin,
|
|
24
26
|
set_owner_role,
|
|
27
|
+
split_sql_statements,
|
|
25
28
|
)
|
|
26
29
|
|
|
27
30
|
_MIGRATION_WRITE_PROFILES = frozenset(
|
|
@@ -125,6 +128,17 @@ def migrate(
|
|
|
125
128
|
if target == "all"
|
|
126
129
|
else [f for f in migration_files if target and target in f.name]
|
|
127
130
|
)
|
|
131
|
+
if target == "all":
|
|
132
|
+
manual_apply = [f for f in to_run if migration_requires_manual_apply(f)]
|
|
133
|
+
if manual_apply:
|
|
134
|
+
warning(
|
|
135
|
+
"Skipping manual-apply migration(s): "
|
|
136
|
+
+ ", ".join(item.name for item in manual_apply)
|
|
137
|
+
)
|
|
138
|
+
to_run = [f for f in to_run if f not in manual_apply]
|
|
139
|
+
elif any(migration_requires_manual_apply(f) for f in to_run):
|
|
140
|
+
warning("Selected manual-apply migration explicitly; proceeding.")
|
|
141
|
+
|
|
128
142
|
if not to_run:
|
|
129
143
|
success("No migrations to run")
|
|
130
144
|
return
|
|
@@ -157,7 +171,11 @@ def migrate(
|
|
|
157
171
|
await conn.execute("SET statement_timeout = '0'")
|
|
158
172
|
else:
|
|
159
173
|
await conn.execute(f"SET statement_timeout = '{timeout}s'")
|
|
160
|
-
|
|
174
|
+
if migration_requires_no_transaction(migration, sql):
|
|
175
|
+
for statement in split_sql_statements(sql):
|
|
176
|
+
await conn.execute(statement, timeout=None)
|
|
177
|
+
else:
|
|
178
|
+
await conn.execute(sql, timeout=None)
|
|
161
179
|
await conn.execute(
|
|
162
180
|
f"INSERT INTO {MIGRATIONS_TABLE} (filename) VALUES ($1) ON CONFLICT DO NOTHING",
|
|
163
181
|
migration.name,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|