sovaria-cli 0.2.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.
- apps/cli/README.md +147 -0
- apps/cli/__init__.py +0 -0
- apps/cli/src/__init__.py +3 -0
- apps/cli/src/commands/__init__.py +1 -0
- apps/cli/src/commands/admin.py +160 -0
- apps/cli/src/commands/api.py +48 -0
- apps/cli/src/commands/audit.py +181 -0
- apps/cli/src/commands/auth_product.py +187 -0
- apps/cli/src/commands/billing.py +513 -0
- apps/cli/src/commands/chrono.py +248 -0
- apps/cli/src/commands/context.py +99 -0
- apps/cli/src/commands/data.py +272 -0
- apps/cli/src/commands/db.py +528 -0
- apps/cli/src/commands/deploy.py +1099 -0
- apps/cli/src/commands/governance.py +491 -0
- apps/cli/src/commands/iam.py +436 -0
- apps/cli/src/commands/init_cmd.py +177 -0
- apps/cli/src/commands/keys.py +120 -0
- apps/cli/src/commands/login.py +842 -0
- apps/cli/src/commands/org.py +157 -0
- apps/cli/src/commands/policy.py +317 -0
- apps/cli/src/commands/project.py +130 -0
- apps/cli/src/commands/realtime.py +364 -0
- apps/cli/src/commands/scope.py +42 -0
- apps/cli/src/commands/shell.py +415 -0
- apps/cli/src/commands/sim.py +178 -0
- apps/cli/src/commands/storage.py +405 -0
- apps/cli/src/commands/vector.py +204 -0
- apps/cli/src/main.py +169 -0
- apps/cli/src/utils/__init__.py +1 -0
- apps/cli/src/utils/api_client.py +329 -0
- apps/cli/src/utils/auth_store.py +299 -0
- apps/cli/src/utils/config.py +185 -0
- apps/cli/src/utils/display.py +62 -0
- apps/cli/src/utils/migrations.py +69 -0
- apps/cli/src/utils/project_context.py +124 -0
- apps/cli/tests/test_auth_store_profiles.py +103 -0
- apps/cli/tests/test_cli_security.py +110 -0
- apps/cli/tests/test_cli_smoke_paths.py +279 -0
- apps/cli/tests/test_config_profiles.py +55 -0
- apps/cli/tests/test_main_startup.py +29 -0
- apps/cli/tests/test_project_and_commands.py +196 -0
- sovaria_cli-0.2.0.dist-info/METADATA +170 -0
- sovaria_cli-0.2.0.dist-info/RECORD +46 -0
- sovaria_cli-0.2.0.dist-info/WHEEL +4 -0
- sovaria_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apps.cli.src.commands.db — Database management commands.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
sov db tables List migration log / tables
|
|
6
|
+
sov db create-table Create a new table
|
|
7
|
+
sov db sql Execute SQL (supports --interactive)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
|
|
17
|
+
from apps.cli.src.utils.display import print_json, print_sql_result
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Database (SQL) management")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("tables")
|
|
23
|
+
def list_tables(
|
|
24
|
+
org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
|
|
25
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
26
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
27
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""List relational tables / migration log for the linked project."""
|
|
30
|
+
try:
|
|
31
|
+
result = call_api(
|
|
32
|
+
method="GET",
|
|
33
|
+
path="/api/v1/data/rel/migrations",
|
|
34
|
+
profile=profile,
|
|
35
|
+
org_id=org_id,
|
|
36
|
+
project_id=project_id,
|
|
37
|
+
env_id=env_id,
|
|
38
|
+
require_scope=True,
|
|
39
|
+
)
|
|
40
|
+
except CLIRequestError as exc:
|
|
41
|
+
typer.echo(str(exc), err=True)
|
|
42
|
+
raise typer.Exit(1) from exc
|
|
43
|
+
print_json(result)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("sql")
|
|
47
|
+
def run_sql(
|
|
48
|
+
query: str | None = typer.Argument(None, help="SQL query to execute"),
|
|
49
|
+
interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive SQL REPL"),
|
|
50
|
+
org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
|
|
51
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
52
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
53
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Execute SQL against Atlas-Rel. Use --interactive for a mini psql-style loop."""
|
|
56
|
+
if interactive:
|
|
57
|
+
typer.echo("Interactive SQL (\\q or exit to quit)")
|
|
58
|
+
while True:
|
|
59
|
+
try:
|
|
60
|
+
line = typer.prompt("sql", prompt_suffix="> ")
|
|
61
|
+
except (EOFError, KeyboardInterrupt):
|
|
62
|
+
typer.echo("")
|
|
63
|
+
break
|
|
64
|
+
stripped = line.strip()
|
|
65
|
+
if not stripped:
|
|
66
|
+
continue
|
|
67
|
+
if stripped.lower() in {"\\q", "exit", "quit"}:
|
|
68
|
+
break
|
|
69
|
+
_execute_sql(
|
|
70
|
+
stripped,
|
|
71
|
+
profile=profile,
|
|
72
|
+
org_id=org_id,
|
|
73
|
+
project_id=project_id,
|
|
74
|
+
env_id=env_id,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if not query:
|
|
79
|
+
typer.echo("Provide a SQL query or use --interactive", err=True)
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
_execute_sql(
|
|
83
|
+
query,
|
|
84
|
+
profile=profile,
|
|
85
|
+
org_id=org_id,
|
|
86
|
+
project_id=project_id,
|
|
87
|
+
env_id=env_id,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _execute_sql(
|
|
92
|
+
sql: str,
|
|
93
|
+
*,
|
|
94
|
+
profile: str | None,
|
|
95
|
+
org_id: str | None,
|
|
96
|
+
project_id: str | None,
|
|
97
|
+
env_id: str | None,
|
|
98
|
+
) -> None:
|
|
99
|
+
try:
|
|
100
|
+
result = call_api(
|
|
101
|
+
method="POST",
|
|
102
|
+
path="/api/v1/sql/query",
|
|
103
|
+
profile=profile,
|
|
104
|
+
body={"sql": sql, "txid": 0},
|
|
105
|
+
org_id=org_id,
|
|
106
|
+
project_id=project_id,
|
|
107
|
+
env_id=env_id,
|
|
108
|
+
require_scope=True,
|
|
109
|
+
)
|
|
110
|
+
except CLIRequestError as exc:
|
|
111
|
+
typer.echo(str(exc), err=True)
|
|
112
|
+
raise typer.Exit(1) from exc
|
|
113
|
+
print_sql_result(result)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.command("create-table")
|
|
117
|
+
def create_table(
|
|
118
|
+
name: str = typer.Argument(..., help="Table name"),
|
|
119
|
+
columns_json: str = typer.Argument(
|
|
120
|
+
...,
|
|
121
|
+
help='Columns as JSON array, e.g. \'[{"name":"id","type":"SERIAL","primary_key":true}]\'',
|
|
122
|
+
),
|
|
123
|
+
foreign_keys: str | None = typer.Option(None, "--foreign-keys", help="JSON array of FK defs"),
|
|
124
|
+
org_id: str | None = typer.Option(None, "--org", help="Organization ID"),
|
|
125
|
+
project_id: str | None = typer.Option(None, "--project", help="Project ID"),
|
|
126
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
127
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Create a new relational table."""
|
|
130
|
+
try:
|
|
131
|
+
columns = parse_json_arg(columns_json, label="columns")
|
|
132
|
+
parsed_fk = parse_json_arg(foreign_keys, label="--foreign-keys") if foreign_keys else []
|
|
133
|
+
if not isinstance(columns, list):
|
|
134
|
+
raise CLIRequestError("columns must be a JSON array")
|
|
135
|
+
if parsed_fk is not None and not isinstance(parsed_fk, list):
|
|
136
|
+
raise CLIRequestError("--foreign-keys must be a JSON array")
|
|
137
|
+
result = call_api(
|
|
138
|
+
method="POST",
|
|
139
|
+
path="/api/v1/data/rel/create",
|
|
140
|
+
profile=profile,
|
|
141
|
+
body={
|
|
142
|
+
"table_name": name,
|
|
143
|
+
"columns": columns,
|
|
144
|
+
"foreign_keys": parsed_fk or [],
|
|
145
|
+
},
|
|
146
|
+
org_id=org_id,
|
|
147
|
+
project_id=project_id,
|
|
148
|
+
env_id=env_id,
|
|
149
|
+
require_scope=True,
|
|
150
|
+
)
|
|
151
|
+
except CLIRequestError as exc:
|
|
152
|
+
typer.echo(str(exc), err=True)
|
|
153
|
+
raise typer.Exit(1) from exc
|
|
154
|
+
print_json(result)
|
|
155
|
+
typer.echo(f"Table '{name}' created successfully")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ── Transaction subcommands ───────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
tx_app = typer.Typer(help="SQL transaction control")
|
|
161
|
+
app.add_typer(tx_app, name="tx")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _scope_opts(
|
|
165
|
+
profile: str | None,
|
|
166
|
+
org_id: str | None,
|
|
167
|
+
project_id: str | None,
|
|
168
|
+
env_id: str | None,
|
|
169
|
+
) -> dict[str, str | None]:
|
|
170
|
+
return {
|
|
171
|
+
"profile": profile,
|
|
172
|
+
"org_id": org_id,
|
|
173
|
+
"project_id": project_id,
|
|
174
|
+
"env_id": env_id,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@tx_app.command("begin")
|
|
179
|
+
def tx_begin(
|
|
180
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
181
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
182
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
183
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Begin a relational transaction and return txid."""
|
|
186
|
+
try:
|
|
187
|
+
result = call_api(
|
|
188
|
+
method="POST",
|
|
189
|
+
path="/api/v1/data/rel/transaction/begin",
|
|
190
|
+
require_scope=True,
|
|
191
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
192
|
+
)
|
|
193
|
+
except CLIRequestError as exc:
|
|
194
|
+
typer.echo(str(exc), err=True)
|
|
195
|
+
raise typer.Exit(1) from exc
|
|
196
|
+
print_json(result)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@tx_app.command("commit")
|
|
200
|
+
def tx_commit(
|
|
201
|
+
txid: int = typer.Option(..., "--txid", help="Transaction id from `sov db tx begin`"),
|
|
202
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
203
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
204
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
205
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Commit an open transaction."""
|
|
208
|
+
try:
|
|
209
|
+
result = call_api(
|
|
210
|
+
method="POST",
|
|
211
|
+
path="/api/v1/data/rel/transaction/commit",
|
|
212
|
+
body={"txid": txid},
|
|
213
|
+
require_scope=True,
|
|
214
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
215
|
+
)
|
|
216
|
+
except CLIRequestError as exc:
|
|
217
|
+
typer.echo(str(exc), err=True)
|
|
218
|
+
raise typer.Exit(1) from exc
|
|
219
|
+
print_json(result)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@tx_app.command("rollback")
|
|
223
|
+
def tx_rollback(
|
|
224
|
+
txid: int = typer.Option(..., "--txid", help="Transaction id from `sov db tx begin`"),
|
|
225
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
226
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
227
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
228
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Rollback an open transaction."""
|
|
231
|
+
try:
|
|
232
|
+
result = call_api(
|
|
233
|
+
method="POST",
|
|
234
|
+
path="/api/v1/data/rel/transaction/rollback",
|
|
235
|
+
body={"txid": txid},
|
|
236
|
+
require_scope=True,
|
|
237
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
238
|
+
)
|
|
239
|
+
except CLIRequestError as exc:
|
|
240
|
+
typer.echo(str(exc), err=True)
|
|
241
|
+
raise typer.Exit(1) from exc
|
|
242
|
+
print_json(result)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── Column / FK DDL ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
column_app = typer.Typer(help="Column DDL operations")
|
|
248
|
+
app.add_typer(column_app, name="column")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@column_app.command("add")
|
|
252
|
+
def column_add(
|
|
253
|
+
table: str = typer.Argument(..., help="Table name"),
|
|
254
|
+
name: str = typer.Argument(..., help="Column name"),
|
|
255
|
+
data_type: str = typer.Option(..., "--type", help="SQL data type, e.g. TEXT or INTEGER"),
|
|
256
|
+
nullable: bool = typer.Option(True, "--nullable/--not-null"),
|
|
257
|
+
unique: bool = typer.Option(False, "--unique"),
|
|
258
|
+
default: str | None = typer.Option(None, "--default"),
|
|
259
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
260
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
261
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
262
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Add a column to an existing table."""
|
|
265
|
+
body: dict[str, object] = {
|
|
266
|
+
"table_name": table,
|
|
267
|
+
"column_name": name,
|
|
268
|
+
"data_type": data_type,
|
|
269
|
+
"is_nullable": nullable,
|
|
270
|
+
"is_unique": unique,
|
|
271
|
+
}
|
|
272
|
+
if default is not None:
|
|
273
|
+
body["default_value"] = default
|
|
274
|
+
try:
|
|
275
|
+
result = call_api(
|
|
276
|
+
method="POST",
|
|
277
|
+
path="/api/v1/data/rel/column/add",
|
|
278
|
+
body=body,
|
|
279
|
+
require_scope=True,
|
|
280
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
281
|
+
)
|
|
282
|
+
except CLIRequestError as exc:
|
|
283
|
+
typer.echo(str(exc), err=True)
|
|
284
|
+
raise typer.Exit(1) from exc
|
|
285
|
+
print_json(result)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@column_app.command("drop")
|
|
289
|
+
def column_drop(
|
|
290
|
+
table: str = typer.Argument(...),
|
|
291
|
+
name: str = typer.Argument(..., help="Column name"),
|
|
292
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
293
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
294
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
295
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Drop a column from a table."""
|
|
298
|
+
try:
|
|
299
|
+
result = call_api(
|
|
300
|
+
method="POST",
|
|
301
|
+
path="/api/v1/data/rel/column/drop",
|
|
302
|
+
body={"table_name": table, "column_name": name},
|
|
303
|
+
require_scope=True,
|
|
304
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
305
|
+
)
|
|
306
|
+
except CLIRequestError as exc:
|
|
307
|
+
typer.echo(str(exc), err=True)
|
|
308
|
+
raise typer.Exit(1) from exc
|
|
309
|
+
print_json(result)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
fk_app = typer.Typer(help="Foreign key DDL operations")
|
|
313
|
+
app.add_typer(fk_app, name="fk")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@fk_app.command("add")
|
|
317
|
+
def fk_add(
|
|
318
|
+
table: str = typer.Argument(...),
|
|
319
|
+
column: str = typer.Argument(...),
|
|
320
|
+
ref_table: str = typer.Option(..., "--ref-table"),
|
|
321
|
+
ref_column: str = typer.Option(..., "--ref-column"),
|
|
322
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
323
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
324
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
325
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Add a foreign key between tables."""
|
|
328
|
+
try:
|
|
329
|
+
result = call_api(
|
|
330
|
+
method="POST",
|
|
331
|
+
path="/api/v1/data/rel/foreign-key/add",
|
|
332
|
+
body={
|
|
333
|
+
"table_name": table,
|
|
334
|
+
"column_name": column,
|
|
335
|
+
"referenced_table": ref_table,
|
|
336
|
+
"referenced_column": ref_column,
|
|
337
|
+
},
|
|
338
|
+
require_scope=True,
|
|
339
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
340
|
+
)
|
|
341
|
+
except CLIRequestError as exc:
|
|
342
|
+
typer.echo(str(exc), err=True)
|
|
343
|
+
raise typer.Exit(1) from exc
|
|
344
|
+
print_json(result)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@fk_app.command("drop")
|
|
348
|
+
def fk_drop(
|
|
349
|
+
table: str = typer.Argument(...),
|
|
350
|
+
column: str = typer.Argument(...),
|
|
351
|
+
ref_table: str = typer.Option(..., "--ref-table"),
|
|
352
|
+
ref_column: str = typer.Option(..., "--ref-column"),
|
|
353
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
354
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
355
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
356
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Drop a foreign key."""
|
|
359
|
+
try:
|
|
360
|
+
result = call_api(
|
|
361
|
+
method="POST",
|
|
362
|
+
path="/api/v1/data/rel/foreign-key/drop",
|
|
363
|
+
body={
|
|
364
|
+
"table_name": table,
|
|
365
|
+
"column_name": column,
|
|
366
|
+
"referenced_table": ref_table,
|
|
367
|
+
"referenced_column": ref_column,
|
|
368
|
+
},
|
|
369
|
+
require_scope=True,
|
|
370
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
371
|
+
)
|
|
372
|
+
except CLIRequestError as exc:
|
|
373
|
+
typer.echo(str(exc), err=True)
|
|
374
|
+
raise typer.Exit(1) from exc
|
|
375
|
+
print_json(result)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ── Migrations push / pull ────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
migrations_app = typer.Typer(help="Local SQL migration files")
|
|
381
|
+
app.add_typer(migrations_app, name="migrations")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@migrations_app.command("list")
|
|
385
|
+
def migrations_list(
|
|
386
|
+
table: str | None = typer.Option(None, "--table"),
|
|
387
|
+
limit: int = typer.Option(100, "--limit"),
|
|
388
|
+
offset: int = typer.Option(0, "--offset"),
|
|
389
|
+
local: bool = typer.Option(False, "--local", help="List local .sql files only"),
|
|
390
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
391
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
392
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
393
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
394
|
+
) -> None:
|
|
395
|
+
"""List remote migration log entries or local migration files."""
|
|
396
|
+
from apps.cli.src.utils.migrations import list_local_migrations, load_applied_state
|
|
397
|
+
|
|
398
|
+
if local:
|
|
399
|
+
files = list_local_migrations()
|
|
400
|
+
applied = load_applied_state()
|
|
401
|
+
for path in files:
|
|
402
|
+
marker = "✓" if path.name in applied else " "
|
|
403
|
+
typer.echo(f" [{marker}] {path.name}")
|
|
404
|
+
typer.echo(f"\n{len(files)} local file(s), {len(applied)} applied locally")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
params: dict[str, str] = {"limit": str(limit), "offset": str(offset)}
|
|
408
|
+
if table:
|
|
409
|
+
params["table_name"] = table
|
|
410
|
+
try:
|
|
411
|
+
result = call_api(
|
|
412
|
+
method="GET",
|
|
413
|
+
path="/api/v1/data/rel/migrations",
|
|
414
|
+
params=params,
|
|
415
|
+
require_scope=True,
|
|
416
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
417
|
+
)
|
|
418
|
+
except CLIRequestError as exc:
|
|
419
|
+
typer.echo(str(exc), err=True)
|
|
420
|
+
raise typer.Exit(1) from exc
|
|
421
|
+
print_json(result)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@migrations_app.command("push")
|
|
425
|
+
def migrations_push(
|
|
426
|
+
directory: str | None = typer.Option(
|
|
427
|
+
None,
|
|
428
|
+
"--dir",
|
|
429
|
+
help="Migration directory (default: sovaria/migrations under linked project)",
|
|
430
|
+
),
|
|
431
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show pending files without executing"),
|
|
432
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
433
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
434
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
435
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Apply pending local .sql migration files in sorted order."""
|
|
438
|
+
from pathlib import Path
|
|
439
|
+
|
|
440
|
+
from apps.cli.src.utils.display import print_success, print_warning
|
|
441
|
+
from apps.cli.src.utils.migrations import (
|
|
442
|
+
list_local_migrations,
|
|
443
|
+
load_applied_state,
|
|
444
|
+
migration_name,
|
|
445
|
+
migrations_dir,
|
|
446
|
+
save_applied_state,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
folder = Path(directory) if directory else migrations_dir()
|
|
450
|
+
files = list_local_migrations(folder)
|
|
451
|
+
if not files:
|
|
452
|
+
print_warning(f"No .sql files in {folder}. Create migrations there first.")
|
|
453
|
+
raise typer.Exit(0)
|
|
454
|
+
|
|
455
|
+
applied = load_applied_state()
|
|
456
|
+
pending = [path for path in files if migration_name(path) not in applied]
|
|
457
|
+
if not pending:
|
|
458
|
+
print_success("All local migrations already applied.")
|
|
459
|
+
raise typer.Exit(0)
|
|
460
|
+
|
|
461
|
+
typer.echo(f"Pending migrations ({len(pending)}):")
|
|
462
|
+
for path in pending:
|
|
463
|
+
typer.echo(f" • {path.name}")
|
|
464
|
+
|
|
465
|
+
if dry_run:
|
|
466
|
+
raise typer.Exit(0)
|
|
467
|
+
|
|
468
|
+
for path in pending:
|
|
469
|
+
sql = path.read_text(encoding="utf-8").strip()
|
|
470
|
+
if not sql:
|
|
471
|
+
print_warning(f"Skipping empty file: {path.name}")
|
|
472
|
+
applied.add(migration_name(path))
|
|
473
|
+
continue
|
|
474
|
+
typer.echo(f"Applying {path.name} …")
|
|
475
|
+
try:
|
|
476
|
+
call_api(
|
|
477
|
+
method="POST",
|
|
478
|
+
path="/api/v1/sql/query",
|
|
479
|
+
body={"sql": sql, "txid": 0},
|
|
480
|
+
require_scope=True,
|
|
481
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
482
|
+
)
|
|
483
|
+
except CLIRequestError as exc:
|
|
484
|
+
typer.echo(str(exc), err=True)
|
|
485
|
+
raise typer.Exit(1) from exc
|
|
486
|
+
applied.add(migration_name(path))
|
|
487
|
+
|
|
488
|
+
state_file = save_applied_state(applied)
|
|
489
|
+
print_success(f"Applied {len(pending)} migration(s). State: {state_file}")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@migrations_app.command("pull")
|
|
493
|
+
def migrations_pull(
|
|
494
|
+
output: str | None = typer.Option(
|
|
495
|
+
None,
|
|
496
|
+
"--output",
|
|
497
|
+
"-o",
|
|
498
|
+
help="Write remote migration log JSON to this path",
|
|
499
|
+
),
|
|
500
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
501
|
+
org_id: str | None = typer.Option(None, "--org"),
|
|
502
|
+
project_id: str | None = typer.Option(None, "--project"),
|
|
503
|
+
env_id: str | None = typer.Option(None, "--env"),
|
|
504
|
+
) -> None:
|
|
505
|
+
"""Fetch remote migration log (schema change history) as JSON."""
|
|
506
|
+
from pathlib import Path
|
|
507
|
+
|
|
508
|
+
from apps.cli.src.utils.migrations import migrations_dir
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
result = call_api(
|
|
512
|
+
method="GET",
|
|
513
|
+
path="/api/v1/data/rel/migrations",
|
|
514
|
+
params={"limit": "500", "offset": "0"},
|
|
515
|
+
require_scope=True,
|
|
516
|
+
**_scope_opts(profile, org_id, project_id, env_id),
|
|
517
|
+
)
|
|
518
|
+
except CLIRequestError as exc:
|
|
519
|
+
typer.echo(str(exc), err=True)
|
|
520
|
+
raise typer.Exit(1) from exc
|
|
521
|
+
|
|
522
|
+
if output:
|
|
523
|
+
out_path = Path(output)
|
|
524
|
+
else:
|
|
525
|
+
out_path = migrations_dir() / "remote_migrations.json"
|
|
526
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
out_path.write_text(json.dumps(result, indent=2, sort_keys=True, default=str) + "\n")
|
|
528
|
+
typer.echo(f"Wrote migration log to {out_path}")
|