maxc-cli 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.
- maxc_cli/__init__.py +5 -0
- maxc_cli/__main__.py +6 -0
- maxc_cli/app.py +3406 -0
- maxc_cli/audit.py +18 -0
- maxc_cli/auth_providers.py +471 -0
- maxc_cli/backend/__init__.py +8 -0
- maxc_cli/backend/auth.py +144 -0
- maxc_cli/backend/data.py +87 -0
- maxc_cli/backend/job.py +304 -0
- maxc_cli/backend/meta.py +312 -0
- maxc_cli/backend/odps.py +130 -0
- maxc_cli/backend/query.py +148 -0
- maxc_cli/cache.py +662 -0
- maxc_cli/cli.py +1274 -0
- maxc_cli/config.py +406 -0
- maxc_cli/exceptions.py +99 -0
- maxc_cli/helpers.py +964 -0
- maxc_cli/models.py +533 -0
- maxc_cli/output.py +75 -0
- maxc_cli/store.py +123 -0
- maxc_cli/utils.py +136 -0
- maxc_cli-0.1.0.dist-info/METADATA +220 -0
- maxc_cli-0.1.0.dist-info/RECORD +26 -0
- maxc_cli-0.1.0.dist-info/WHEEL +5 -0
- maxc_cli-0.1.0.dist-info/entry_points.txt +2 -0
- maxc_cli-0.1.0.dist-info/top_level.txt +1 -0
maxc_cli/cli.py
ADDED
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Sequence, TextIO
|
|
6
|
+
|
|
7
|
+
from .app import MaxCApp, read_stdin
|
|
8
|
+
from .exceptions import ErrorPayload, MaxCError, ValidationError
|
|
9
|
+
from .models import Envelope
|
|
10
|
+
from .output import emit_json, emit_ndjson, render_error, render_key_values, render_table
|
|
11
|
+
from .utils import read_sql_input
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _add_required_subparsers(
|
|
15
|
+
parser: 'argparse.ArgumentParser',
|
|
16
|
+
*,
|
|
17
|
+
dest: 'str',
|
|
18
|
+
):
|
|
19
|
+
"""Backport-required subparsers for Python 3.6."""
|
|
20
|
+
subparsers = parser.add_subparsers(dest=dest)
|
|
21
|
+
subparsers.required = True
|
|
22
|
+
return subparsers
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
26
|
+
parser = argparse.ArgumentParser(prog="maxc", description="Agent-first MaxCompute CLI MVP")
|
|
27
|
+
parser.add_argument("--config", help="Explicit path to a config file")
|
|
28
|
+
|
|
29
|
+
subparsers = _add_required_subparsers(parser, dest="command_group")
|
|
30
|
+
|
|
31
|
+
query_parser = subparsers.add_parser(
|
|
32
|
+
"query",
|
|
33
|
+
help="Run a SQL query (supports run/cost/explain aliases)",
|
|
34
|
+
description=(
|
|
35
|
+
"Run a SQL query.\n"
|
|
36
|
+
"Recommended usage:\n"
|
|
37
|
+
" maxc query \"SELECT 1\"\n"
|
|
38
|
+
" maxc query cost \"SELECT 1\"\n"
|
|
39
|
+
" maxc query explain \"SELECT 1\"\n"
|
|
40
|
+
"Legacy-compatible usage:\n"
|
|
41
|
+
" maxc query \"SELECT 1\" --mode cost"
|
|
42
|
+
),
|
|
43
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
44
|
+
)
|
|
45
|
+
query_parser.add_argument("sql_parts", nargs="*", help="SQL text; `@natural` placeholders are reserved for future support")
|
|
46
|
+
query_parser.add_argument("--file", help="Read SQL from file")
|
|
47
|
+
query_parser.add_argument("--stdin", action="store_true", help="Read SQL from stdin")
|
|
48
|
+
query_parser.add_argument("--project", help="Target MaxCompute project")
|
|
49
|
+
query_parser.add_argument(
|
|
50
|
+
"--mode",
|
|
51
|
+
choices=["run", "cost", "explain"],
|
|
52
|
+
default="run",
|
|
53
|
+
help="Query mode. Legacy-compatible, but `query run/cost/explain <SQL>` is preferred.",
|
|
54
|
+
)
|
|
55
|
+
query_parser.add_argument("--format", choices=["table", "json", "csv", "ndjson"], help="Output format")
|
|
56
|
+
query_parser.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
57
|
+
query_parser.add_argument("--max-rows", type=int, default=100, help="Maximum rows to return (default: 100)")
|
|
58
|
+
query_parser.add_argument("--page-size", type=int, help="Rows per page for pagination")
|
|
59
|
+
query_parser.add_argument("--cursor", help="Pagination cursor from previous response")
|
|
60
|
+
query_parser.add_argument("--output", help="Write output to file")
|
|
61
|
+
query_parser.add_argument("--output-format", choices=["table", "json", "csv", "ndjson"], help="Output file format")
|
|
62
|
+
query_parser.add_argument("--wait", type=int, default=10,
|
|
63
|
+
help="Seconds to poll before promoting to async (default: 10). --wait 0 returns job_id immediately.")
|
|
64
|
+
query_parser.add_argument("--dry-run", action="store_true", help="Show query plan without executing")
|
|
65
|
+
query_parser.add_argument("--cost-check", type=float, help="Abort if estimated cost exceeds threshold (CU)")
|
|
66
|
+
query_parser.add_argument("--idempotency-key", help="Deduplication key for retries")
|
|
67
|
+
query_parser.add_argument("--retry-on", default="", help="Comma-separated error codes to retry on")
|
|
68
|
+
query_parser.add_argument("--max-retries", type=int, default=0, help="Maximum retry attempts (default: 0)")
|
|
69
|
+
query_parser.add_argument("--retry-backoff", choices=["fixed", "exponential"], default="fixed", help="Retry backoff strategy")
|
|
70
|
+
query_parser.set_defaults(handler=_handle_query)
|
|
71
|
+
|
|
72
|
+
job_parser = subparsers.add_parser("job", help="Manage async jobs")
|
|
73
|
+
job_subparsers = _add_required_subparsers(job_parser, dest="job_command")
|
|
74
|
+
|
|
75
|
+
job_submit = job_subparsers.add_parser("submit", help="Submit an async job")
|
|
76
|
+
job_submit.add_argument("sql_parts", nargs="*", help="SQL text")
|
|
77
|
+
job_submit.add_argument("--file", help="Read SQL from file")
|
|
78
|
+
job_submit.add_argument("--stdin", action="store_true", help="Read SQL from stdin")
|
|
79
|
+
job_submit.add_argument("--project", help="Target MaxCompute project")
|
|
80
|
+
job_submit.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
81
|
+
job_submit.add_argument("--max-rows", type=int, default=100, help="Maximum rows to return (default: 100)")
|
|
82
|
+
job_submit.add_argument("--cost-check", type=float, help="Abort if estimated cost exceeds threshold (CU)")
|
|
83
|
+
job_submit.add_argument("--idempotency-key", help="Deduplication key for retries")
|
|
84
|
+
job_submit.set_defaults(handler=_handle_job_submit)
|
|
85
|
+
|
|
86
|
+
job_status = job_subparsers.add_parser("status", help="Show job status")
|
|
87
|
+
job_status.add_argument("job_id", help="Job ID returned by submit")
|
|
88
|
+
job_status.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
89
|
+
job_status.set_defaults(handler=_handle_job_status)
|
|
90
|
+
|
|
91
|
+
job_wait = job_subparsers.add_parser("wait", help="Wait for a job to finish")
|
|
92
|
+
job_wait.add_argument("job_id", help="Job ID returned by submit")
|
|
93
|
+
job_wait.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
94
|
+
job_wait.add_argument("--stream", action="store_true", help="Stream job progress as NDJSON")
|
|
95
|
+
job_wait.add_argument("--timeout", type=int, default=None, help="Timeout in seconds (default: 300)")
|
|
96
|
+
job_wait.set_defaults(handler=_handle_job_wait)
|
|
97
|
+
|
|
98
|
+
job_diagnose = job_subparsers.add_parser("diagnose", help="Diagnose job status and failure reasons")
|
|
99
|
+
job_diagnose.add_argument("job_id", help="Job ID returned by submit")
|
|
100
|
+
job_diagnose.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
101
|
+
job_diagnose.set_defaults(handler=_handle_job_diagnose)
|
|
102
|
+
|
|
103
|
+
job_result = job_subparsers.add_parser("result", help="Fetch job results")
|
|
104
|
+
job_result.add_argument("job_id", help="Job ID returned by submit")
|
|
105
|
+
job_result.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
106
|
+
job_result.add_argument("--max-rows", type=int, default=100, dest="max_rows", help="Maximum rows to return (default: 100)")
|
|
107
|
+
job_result.add_argument("--cursor", default=None, help="Pagination cursor from previous response")
|
|
108
|
+
job_result.set_defaults(handler=_handle_job_result)
|
|
109
|
+
|
|
110
|
+
job_cancel = job_subparsers.add_parser("cancel", help="Cancel a job")
|
|
111
|
+
job_cancel.add_argument("job_id", help="Job ID returned by submit")
|
|
112
|
+
job_cancel.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
113
|
+
job_cancel.set_defaults(handler=_handle_job_cancel)
|
|
114
|
+
|
|
115
|
+
job_list = job_subparsers.add_parser("list", help="List jobs")
|
|
116
|
+
job_list.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
117
|
+
job_list.add_argument("--limit", type=int, default=20, help="Maximum number of jobs to return (default: 20)")
|
|
118
|
+
job_list.set_defaults(handler=_handle_job_list)
|
|
119
|
+
|
|
120
|
+
meta_parser = subparsers.add_parser("meta", help="Metadata commands")
|
|
121
|
+
meta_subparsers = _add_required_subparsers(meta_parser, dest="meta_command")
|
|
122
|
+
|
|
123
|
+
meta_list = meta_subparsers.add_parser("list-tables", help="List tables")
|
|
124
|
+
meta_list.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
125
|
+
meta_list.set_defaults(handler=_handle_meta_list_tables)
|
|
126
|
+
|
|
127
|
+
meta_describe = meta_subparsers.add_parser("describe", help="Describe a table")
|
|
128
|
+
meta_describe.add_argument("table_name", help="Table name (schema.table or table)")
|
|
129
|
+
meta_describe.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
130
|
+
meta_describe.add_argument("--full", action="store_true", help="Show full column list (default is summary mode)")
|
|
131
|
+
meta_describe.set_defaults(handler=_handle_meta_describe)
|
|
132
|
+
|
|
133
|
+
meta_search = meta_subparsers.add_parser("search", help="Search tables")
|
|
134
|
+
meta_search.add_argument("keyword", help="Search keyword")
|
|
135
|
+
meta_search.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
136
|
+
meta_search.set_defaults(handler=_handle_meta_search)
|
|
137
|
+
|
|
138
|
+
meta_search_columns = meta_subparsers.add_parser("search-columns", help="Search columns")
|
|
139
|
+
meta_search_columns.add_argument("keyword", help="Search keyword")
|
|
140
|
+
meta_search_columns.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
141
|
+
meta_search_columns.set_defaults(handler=_handle_meta_search_columns)
|
|
142
|
+
|
|
143
|
+
meta_latest_partition = meta_subparsers.add_parser("latest-partition", help="Show the latest partition")
|
|
144
|
+
meta_latest_partition.add_argument("table_name", help="Table name (schema.table or table)")
|
|
145
|
+
meta_latest_partition.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
146
|
+
meta_latest_partition.set_defaults(handler=_handle_meta_latest_partition)
|
|
147
|
+
|
|
148
|
+
meta_freshness = meta_subparsers.add_parser("freshness", help="Show table freshness")
|
|
149
|
+
meta_freshness.add_argument("table_name", help="Table name (schema.table or table)")
|
|
150
|
+
meta_freshness.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
151
|
+
meta_freshness.set_defaults(handler=_handle_meta_freshness)
|
|
152
|
+
|
|
153
|
+
meta_lineage = meta_subparsers.add_parser("lineage", help="Show lineage")
|
|
154
|
+
meta_lineage.add_argument("table_name", help="Table name (schema.table or table)")
|
|
155
|
+
meta_lineage.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
156
|
+
meta_lineage.set_defaults(handler=_handle_meta_lineage)
|
|
157
|
+
|
|
158
|
+
meta_partitions = meta_subparsers.add_parser("partitions", help="List partitions")
|
|
159
|
+
meta_partitions.add_argument("table_name", help="Table name (schema.table or table)")
|
|
160
|
+
meta_partitions.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
161
|
+
meta_partitions.set_defaults(handler=_handle_meta_partitions)
|
|
162
|
+
|
|
163
|
+
meta_list_projects = meta_subparsers.add_parser("list-projects", help="List accessible projects")
|
|
164
|
+
meta_list_projects.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
165
|
+
meta_list_projects.set_defaults(handler=_handle_meta_list_projects)
|
|
166
|
+
|
|
167
|
+
meta_list_schemas = meta_subparsers.add_parser("list-schemas", help="List schemas in a project")
|
|
168
|
+
meta_list_schemas.add_argument("--project", help="Target MaxCompute project")
|
|
169
|
+
meta_list_schemas.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
170
|
+
meta_list_schemas.set_defaults(handler=_handle_meta_list_schemas)
|
|
171
|
+
|
|
172
|
+
# Semantic metadata subcommands
|
|
173
|
+
meta_semantic = meta_subparsers.add_parser("semantic", help="Semantic metadata management")
|
|
174
|
+
meta_semantic_subparsers = _add_required_subparsers(
|
|
175
|
+
meta_semantic,
|
|
176
|
+
dest="semantic_command",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# semantic set
|
|
180
|
+
semantic_set = meta_semantic_subparsers.add_parser("set", help="Set semantic metadata for a table")
|
|
181
|
+
semantic_set.add_argument("table_name", help="Table name")
|
|
182
|
+
semantic_set.add_argument("--desc", "--description", dest="semantic_desc", help="Table description")
|
|
183
|
+
semantic_set.add_argument("--use-cases", nargs="*", help="Use cases (space-separated)")
|
|
184
|
+
semantic_set.add_argument("--sample-questions", nargs="*", help="Sample questions (space-separated)")
|
|
185
|
+
semantic_set.add_argument("--column-semantics", type=str, help="Column semantics as JSON string")
|
|
186
|
+
semantic_set.add_argument("--relations", type=str, help="Relations as JSON string")
|
|
187
|
+
semantic_set.add_argument("--stats", type=str, help="Stats as JSON string")
|
|
188
|
+
semantic_set.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
189
|
+
semantic_set.set_defaults(handler=_handle_meta_semantic_set)
|
|
190
|
+
|
|
191
|
+
# semantic get
|
|
192
|
+
semantic_get = meta_semantic_subparsers.add_parser("get", help="Get semantic metadata for a table")
|
|
193
|
+
semantic_get.add_argument("table_name", help="Table name")
|
|
194
|
+
semantic_get.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
195
|
+
semantic_get.set_defaults(handler=_handle_meta_semantic_get)
|
|
196
|
+
|
|
197
|
+
# semantic list-missing
|
|
198
|
+
semantic_list_missing = meta_semantic_subparsers.add_parser("list-missing", help="List tables without semantic metadata")
|
|
199
|
+
semantic_list_missing.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
200
|
+
semantic_list_missing.set_defaults(handler=_handle_meta_semantic_list_missing)
|
|
201
|
+
|
|
202
|
+
session_parser = subparsers.add_parser("session", help="Session management - switch project/schema")
|
|
203
|
+
session_subparsers = _add_required_subparsers(
|
|
204
|
+
session_parser,
|
|
205
|
+
dest="session_command",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
session_set = session_subparsers.add_parser("set", help="Set current project and/or schema for this session")
|
|
209
|
+
session_set.add_argument("--project", help="Project name")
|
|
210
|
+
session_set.add_argument("--schema", help="Schema name")
|
|
211
|
+
session_set.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
212
|
+
session_set.set_defaults(handler=_handle_session_set)
|
|
213
|
+
|
|
214
|
+
session_show = session_subparsers.add_parser("show", help="Show current session settings")
|
|
215
|
+
session_show.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
216
|
+
session_show.set_defaults(handler=_handle_session_show)
|
|
217
|
+
|
|
218
|
+
session_unset = session_subparsers.add_parser("unset", help="Clear session override, revert to env/config")
|
|
219
|
+
session_unset.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
220
|
+
session_unset.set_defaults(handler=_handle_session_unset)
|
|
221
|
+
|
|
222
|
+
data_parser = subparsers.add_parser("data", help="Data exploration commands")
|
|
223
|
+
data_subparsers = _add_required_subparsers(data_parser, dest="data_command")
|
|
224
|
+
|
|
225
|
+
data_sample = data_subparsers.add_parser("sample", help="Sample rows")
|
|
226
|
+
data_sample.add_argument("table_name", help="Table name (schema.table or table)")
|
|
227
|
+
data_sample.add_argument("--rows", type=int, default=5, help="Number of sample rows (default: 5)")
|
|
228
|
+
data_sample.add_argument("--partition", help="Partition specification")
|
|
229
|
+
data_sample.add_argument("--columns", help="Comma-separated column names")
|
|
230
|
+
data_sample.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
231
|
+
data_sample.set_defaults(handler=_handle_data_sample)
|
|
232
|
+
|
|
233
|
+
data_profile = data_subparsers.add_parser("profile", help="Profile table data")
|
|
234
|
+
data_profile.add_argument("table_name", help="Table name (schema.table or table)")
|
|
235
|
+
data_profile.add_argument("--partition", help="Partition specification")
|
|
236
|
+
data_profile.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
237
|
+
data_profile.set_defaults(handler=_handle_data_profile)
|
|
238
|
+
|
|
239
|
+
auth_parser = subparsers.add_parser("auth", help="Authentication and permission checks")
|
|
240
|
+
auth_subparsers = _add_required_subparsers(auth_parser, dest="auth_command")
|
|
241
|
+
|
|
242
|
+
auth_login = auth_subparsers.add_parser("login", help="Save MaxCompute login configuration")
|
|
243
|
+
auth_login.add_argument("--access-id", "--access-key-id", dest="access_id", help="AccessKey ID")
|
|
244
|
+
auth_login.add_argument(
|
|
245
|
+
"--secret-access-key",
|
|
246
|
+
"--access-key-secret",
|
|
247
|
+
dest="secret_access_key",
|
|
248
|
+
help="AccessKey Secret",
|
|
249
|
+
)
|
|
250
|
+
auth_login.add_argument("--security-token", help="STS security token")
|
|
251
|
+
auth_login.add_argument("--project", help="Target MaxCompute project")
|
|
252
|
+
auth_login.add_argument("--endpoint", help="MaxCompute endpoint URL")
|
|
253
|
+
auth_login.add_argument("--region", dest="region_name", help="MaxCompute region name")
|
|
254
|
+
auth_login.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
|
|
255
|
+
auth_login.add_argument("--from-env", action="store_true", help="Import credentials from environment variables")
|
|
256
|
+
auth_login.add_argument("--no-validate", action="store_true", help="Skip credential validation")
|
|
257
|
+
auth_login.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
258
|
+
auth_login.set_defaults(handler=_handle_auth_login)
|
|
259
|
+
|
|
260
|
+
auth_login_ncs = auth_subparsers.add_parser("login-ncs", help="Save ncs-based MaxCompute login configuration")
|
|
261
|
+
auth_login_ncs.add_argument("--account-type", choices=["user", "account", "app"], help="NCS account type")
|
|
262
|
+
auth_login_ncs.add_argument("--employee-id", help="Employee ID for NCS authentication")
|
|
263
|
+
auth_login_ncs.add_argument("--account-name", help="Account name for NCS authentication")
|
|
264
|
+
auth_login_ncs.add_argument("--app-name", help="App name for NCS authentication")
|
|
265
|
+
auth_login_ncs.add_argument("--project", help="Target MaxCompute project")
|
|
266
|
+
auth_login_ncs.add_argument("--endpoint", help="MaxCompute endpoint URL")
|
|
267
|
+
auth_login_ncs.add_argument("--region", dest="region_name", help="MaxCompute region name")
|
|
268
|
+
auth_login_ncs.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
|
|
269
|
+
auth_login_ncs.add_argument("--interactive", action="store_true", help="Interactive NCS login")
|
|
270
|
+
auth_login_ncs.add_argument("--list-accounts", action="store_true", help="List available NCS accounts")
|
|
271
|
+
auth_login_ncs.add_argument("--no-validate", action="store_true", help="Skip credential validation")
|
|
272
|
+
auth_login_ncs.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
273
|
+
auth_login_ncs.set_defaults(handler=_handle_auth_login_ncs)
|
|
274
|
+
|
|
275
|
+
auth_whoami = auth_subparsers.add_parser("whoami", help="Show the current identity")
|
|
276
|
+
auth_whoami.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
277
|
+
auth_whoami.set_defaults(handler=_handle_auth_whoami)
|
|
278
|
+
|
|
279
|
+
auth_can_i = auth_subparsers.add_parser("can-i", help="Check whether an operation is allowed")
|
|
280
|
+
auth_can_i.add_argument("--table", required=True, help="Table name to check")
|
|
281
|
+
auth_can_i.add_argument("--operation", required=True, help="Operation to check (e.g. SELECT, INSERT)")
|
|
282
|
+
auth_can_i.add_argument("--project", help="Target MaxCompute project")
|
|
283
|
+
auth_can_i.add_argument("--brief", action="store_true", help="Show brief result")
|
|
284
|
+
auth_can_i.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
285
|
+
auth_can_i.set_defaults(handler=_handle_auth_can_i)
|
|
286
|
+
|
|
287
|
+
diff_parser = subparsers.add_parser("diff", help="Diff commands")
|
|
288
|
+
diff_subparsers = _add_required_subparsers(diff_parser, dest="diff_command")
|
|
289
|
+
|
|
290
|
+
diff_schema = diff_subparsers.add_parser("schema", help="Compare two table schemas")
|
|
291
|
+
diff_schema.add_argument("left_table", help="Left table for comparison")
|
|
292
|
+
diff_schema.add_argument("right_table", help="Right table for comparison")
|
|
293
|
+
diff_schema.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
294
|
+
diff_schema.set_defaults(handler=_handle_diff_schema)
|
|
295
|
+
|
|
296
|
+
diff_partition = diff_subparsers.add_parser("partition", help="Compare partition lists")
|
|
297
|
+
diff_partition.add_argument("left_table", help="Left table for comparison")
|
|
298
|
+
diff_partition.add_argument("right_table", help="Right table for comparison")
|
|
299
|
+
diff_partition.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
300
|
+
diff_partition.set_defaults(handler=_handle_diff_partition)
|
|
301
|
+
|
|
302
|
+
diff_data = diff_subparsers.add_parser("data", help="Compare read-only table snapshots by key")
|
|
303
|
+
diff_data.add_argument("left_table", help="Left table for comparison")
|
|
304
|
+
diff_data.add_argument("right_table", help="Right table for comparison")
|
|
305
|
+
diff_data.add_argument("--keys", required=True, help="Comma-separated alignment key columns")
|
|
306
|
+
diff_data.add_argument("--columns", help="Comma-separated non-key comparison columns; defaults to shared columns")
|
|
307
|
+
diff_data.add_argument("--rows", type=int, default=100, help="Maximum rows to sample from each side")
|
|
308
|
+
diff_data.add_argument("--partition", help="Partition applied to both tables")
|
|
309
|
+
diff_data.add_argument("--left-partition", help="Partition for left table")
|
|
310
|
+
diff_data.add_argument("--right-partition", help="Partition for right table")
|
|
311
|
+
diff_data.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
312
|
+
diff_data.set_defaults(handler=_handle_diff_data)
|
|
313
|
+
|
|
314
|
+
agent_parser = subparsers.add_parser("agent", help="Agent helper commands")
|
|
315
|
+
agent_subparsers = _add_required_subparsers(agent_parser, dest="agent_command")
|
|
316
|
+
|
|
317
|
+
agent_context = agent_subparsers.add_parser("context", help="Show agent context")
|
|
318
|
+
agent_context.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
319
|
+
agent_context.set_defaults(handler=_handle_agent_context)
|
|
320
|
+
|
|
321
|
+
cache_parser = subparsers.add_parser("cache", help="Metadata cache management")
|
|
322
|
+
cache_subparsers = _add_required_subparsers(cache_parser, dest="cache_command")
|
|
323
|
+
|
|
324
|
+
cache_build = cache_subparsers.add_parser("build", help="Build the metadata cache")
|
|
325
|
+
cache_build.add_argument("--project", help="Target MaxCompute project")
|
|
326
|
+
cache_build.add_argument("--schema", help="Target schema name")
|
|
327
|
+
cache_build.add_argument("--async", dest="async_mode", action="store_true", help="Run the cache build asynchronously")
|
|
328
|
+
cache_build.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
329
|
+
cache_build.set_defaults(handler=_handle_cache_build)
|
|
330
|
+
|
|
331
|
+
cache_build_status = cache_subparsers.add_parser("build-status", help="Show cache build status")
|
|
332
|
+
cache_build_status.add_argument("--project", help="Target MaxCompute project")
|
|
333
|
+
cache_build_status.add_argument("--build-id", help="Build ID")
|
|
334
|
+
cache_build_status.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
335
|
+
cache_build_status.set_defaults(handler=_handle_cache_build_status)
|
|
336
|
+
|
|
337
|
+
cache_status = cache_subparsers.add_parser("status", help="Show cache status")
|
|
338
|
+
cache_status.add_argument("--project", help="Target MaxCompute project")
|
|
339
|
+
cache_status.add_argument("--schema", help="Target schema name")
|
|
340
|
+
cache_status.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
341
|
+
cache_status.set_defaults(handler=_handle_cache_status)
|
|
342
|
+
|
|
343
|
+
cache_clear = cache_subparsers.add_parser("clear", help="Clear cached metadata")
|
|
344
|
+
cache_clear.add_argument("--project", help="Target MaxCompute project")
|
|
345
|
+
cache_clear.add_argument("--schema", help="Target schema name")
|
|
346
|
+
cache_clear.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
347
|
+
cache_clear.set_defaults(handler=_handle_cache_clear)
|
|
348
|
+
|
|
349
|
+
cache_save_semantic = cache_subparsers.add_parser("save-semantic", help="Save semantic metadata")
|
|
350
|
+
cache_save_semantic.add_argument("--table", required=True, help="Table name")
|
|
351
|
+
cache_save_semantic.add_argument("--schema", default="default", help="Schema name (default: default)")
|
|
352
|
+
cache_save_semantic.add_argument("--semantic-desc", required=True, help="One-sentence business description for the table")
|
|
353
|
+
cache_save_semantic.add_argument("--use-cases", default="[]", help="Use cases as a JSON array")
|
|
354
|
+
cache_save_semantic.add_argument("--sample-questions", default="[]", help="Sample questions as a JSON array")
|
|
355
|
+
cache_save_semantic.add_argument("--column-semantics", default="[]", help="Column semantics as a JSON array")
|
|
356
|
+
cache_save_semantic.add_argument("--project", help="Target MaxCompute project")
|
|
357
|
+
cache_save_semantic.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
358
|
+
cache_save_semantic.set_defaults(handler=_handle_cache_save_semantic)
|
|
359
|
+
|
|
360
|
+
cache_get_semantic = cache_subparsers.add_parser("get-semantic", help="Get semantic metadata")
|
|
361
|
+
cache_get_semantic.add_argument("--table", required=True, help="Table name")
|
|
362
|
+
cache_get_semantic.add_argument("--schema", default="default", help="Schema name (default: default)")
|
|
363
|
+
cache_get_semantic.add_argument("--project", help="Target MaxCompute project")
|
|
364
|
+
cache_get_semantic.add_argument("--json", action="store_true", help="Output as JSON envelope")
|
|
365
|
+
cache_get_semantic.set_defaults(handler=_handle_cache_get_semantic)
|
|
366
|
+
|
|
367
|
+
return parser
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def main(argv: 'Sequence[str] | None' = None) -> 'int':
|
|
371
|
+
return run(argv=argv)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def run(
|
|
375
|
+
argv: 'Sequence[str] | None' = None,
|
|
376
|
+
*,
|
|
377
|
+
cwd: 'Path | None' = None,
|
|
378
|
+
stdout: 'TextIO | None' = None,
|
|
379
|
+
stderr: 'TextIO | None' = None,
|
|
380
|
+
) -> 'int':
|
|
381
|
+
stdout = stdout or sys.stdout
|
|
382
|
+
stderr = stderr or sys.stderr
|
|
383
|
+
parser = build_parser()
|
|
384
|
+
args = parser.parse_args(argv)
|
|
385
|
+
working_dir = cwd or Path.cwd()
|
|
386
|
+
requested_config_path = Path(args.config).resolve() if args.config else None
|
|
387
|
+
args.requested_config_path = requested_config_path
|
|
388
|
+
args.stderr = stderr
|
|
389
|
+
config_path = requested_config_path
|
|
390
|
+
if (
|
|
391
|
+
requested_config_path is not None
|
|
392
|
+
and not requested_config_path.exists()
|
|
393
|
+
and _command_name(args) in {"auth.login", "auth.login-ncs"}
|
|
394
|
+
):
|
|
395
|
+
config_path = None
|
|
396
|
+
|
|
397
|
+
app: 'MaxCApp | None' = None
|
|
398
|
+
try:
|
|
399
|
+
command_name = _command_name(args)
|
|
400
|
+
app = MaxCApp(
|
|
401
|
+
cwd=working_dir,
|
|
402
|
+
config_path=config_path,
|
|
403
|
+
load_backend=_should_load_backend(command_name),
|
|
404
|
+
)
|
|
405
|
+
args.handler(app, args, stdout)
|
|
406
|
+
return 0
|
|
407
|
+
except MaxCError as exc:
|
|
408
|
+
if app is not None:
|
|
409
|
+
app.log(
|
|
410
|
+
_command_name(args),
|
|
411
|
+
"failure",
|
|
412
|
+
{},
|
|
413
|
+
error=exc.to_payload().to_dict(),
|
|
414
|
+
)
|
|
415
|
+
if getattr(args, "json", False):
|
|
416
|
+
payload = Envelope(
|
|
417
|
+
command=_command_name(args),
|
|
418
|
+
status="failure",
|
|
419
|
+
error=exc.to_payload(),
|
|
420
|
+
)
|
|
421
|
+
emit_json(payload.to_dict(), stdout)
|
|
422
|
+
else:
|
|
423
|
+
stderr.write(render_error(exc.error_code, exc.message, exc.suggestion) + "\n")
|
|
424
|
+
return exc.exit_code
|
|
425
|
+
except Exception as exc:
|
|
426
|
+
error_payload = ErrorPayload(
|
|
427
|
+
code="INTERNAL_ERROR",
|
|
428
|
+
message=str(exc) or type(exc).__name__,
|
|
429
|
+
suggestion="This is an unexpected error. Please report it with the full message.",
|
|
430
|
+
recoverable=False,
|
|
431
|
+
)
|
|
432
|
+
cmd = _command_name(args) if hasattr(args, "handler") else "unknown"
|
|
433
|
+
if app is not None:
|
|
434
|
+
app.log(cmd, "failure", {}, error=error_payload.to_dict())
|
|
435
|
+
if getattr(args, "json", False):
|
|
436
|
+
envelope = Envelope(
|
|
437
|
+
command=cmd,
|
|
438
|
+
status="failure",
|
|
439
|
+
error=error_payload,
|
|
440
|
+
)
|
|
441
|
+
emit_json(envelope.to_dict(), stdout)
|
|
442
|
+
else:
|
|
443
|
+
stderr.write(render_error(
|
|
444
|
+
error_payload.code,
|
|
445
|
+
error_payload.message,
|
|
446
|
+
error_payload.suggestion,
|
|
447
|
+
) + "\n")
|
|
448
|
+
return 1
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _handle_query(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
452
|
+
mode, sql_parts = _resolve_query_mode(args)
|
|
453
|
+
args.resolved_command = "query" if mode == "run" else f"query.{mode}"
|
|
454
|
+
sql = read_sql_input(
|
|
455
|
+
sql_parts,
|
|
456
|
+
file_path=args.file,
|
|
457
|
+
use_stdin=args.stdin,
|
|
458
|
+
stdin_text=read_stdin() if args.stdin else None,
|
|
459
|
+
)
|
|
460
|
+
if mode == "cost":
|
|
461
|
+
_validate_query_analysis_args(args, mode)
|
|
462
|
+
envelope = app.query_cost(sql=sql, project=args.project)
|
|
463
|
+
elif mode == "explain":
|
|
464
|
+
_validate_query_analysis_args(args, mode)
|
|
465
|
+
envelope = app.query_explain(sql=sql, project=args.project)
|
|
466
|
+
else:
|
|
467
|
+
retry_on = [item.strip() for item in args.retry_on.split(",") if item.strip()]
|
|
468
|
+
envelope = app.query(
|
|
469
|
+
command="query",
|
|
470
|
+
sql=sql,
|
|
471
|
+
project=args.project,
|
|
472
|
+
max_rows=_query_page_size(args),
|
|
473
|
+
cursor=args.cursor,
|
|
474
|
+
dry_run=args.dry_run,
|
|
475
|
+
wait=args.wait,
|
|
476
|
+
cost_check=args.cost_check,
|
|
477
|
+
idempotency_key=args.idempotency_key,
|
|
478
|
+
retry_on=retry_on,
|
|
479
|
+
max_retries=args.max_retries,
|
|
480
|
+
)
|
|
481
|
+
if args.output:
|
|
482
|
+
output_format = _query_output_format(args)
|
|
483
|
+
output_path = _write_output_file(envelope, args.output, output_format)
|
|
484
|
+
envelope.metadata["output_path"] = str(output_path)
|
|
485
|
+
envelope.metadata["output_format"] = output_format
|
|
486
|
+
_emit_envelope(
|
|
487
|
+
envelope,
|
|
488
|
+
args=args,
|
|
489
|
+
stdout=stdout,
|
|
490
|
+
default_format=args.format or _query_default_format(app, mode),
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _handle_job_submit(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
495
|
+
sql = read_sql_input(
|
|
496
|
+
args.sql_parts,
|
|
497
|
+
file_path=args.file,
|
|
498
|
+
use_stdin=args.stdin,
|
|
499
|
+
stdin_text=read_stdin() if args.stdin else None,
|
|
500
|
+
)
|
|
501
|
+
envelope = app.submit_job(
|
|
502
|
+
sql=sql,
|
|
503
|
+
project=args.project,
|
|
504
|
+
max_rows=args.max_rows,
|
|
505
|
+
cost_check=args.cost_check,
|
|
506
|
+
idempotency_key=args.idempotency_key,
|
|
507
|
+
)
|
|
508
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _handle_job_status(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
512
|
+
envelope = app.job_status(args.job_id)
|
|
513
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _handle_job_wait(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
517
|
+
envelope, events = app.job_wait(args.job_id, timeout=args.timeout)
|
|
518
|
+
if args.stream:
|
|
519
|
+
emit_ndjson(events, stdout)
|
|
520
|
+
return
|
|
521
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _handle_job_diagnose(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
525
|
+
envelope = app.job_diagnose(args.job_id)
|
|
526
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _handle_job_result(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
530
|
+
envelope = app.job_result(args.job_id, max_rows=args.max_rows, cursor=args.cursor)
|
|
531
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _handle_job_cancel(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
535
|
+
envelope = app.cancel_job(args.job_id)
|
|
536
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _handle_job_list(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
540
|
+
envelope = app.list_jobs(limit=args.limit)
|
|
541
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _handle_meta_list_tables(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
545
|
+
envelope = app.meta_list_tables()
|
|
546
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _handle_meta_describe(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
550
|
+
envelope = app.meta_describe(args.table_name, full=args.full)
|
|
551
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _handle_meta_search(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
555
|
+
envelope = app.meta_search(args.keyword)
|
|
556
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _handle_meta_search_columns(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
560
|
+
envelope = app.meta_search_columns(args.keyword)
|
|
561
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _handle_meta_latest_partition(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
565
|
+
envelope = app.meta_latest_partition(args.table_name)
|
|
566
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _handle_meta_freshness(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
570
|
+
envelope = app.meta_freshness(args.table_name)
|
|
571
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _handle_meta_lineage(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
575
|
+
envelope = app.meta_lineage(args.table_name)
|
|
576
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _handle_meta_partitions(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
580
|
+
envelope = app.meta_partitions(args.table_name)
|
|
581
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _handle_meta_list_projects(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
585
|
+
envelope = app.meta_list_projects()
|
|
586
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _handle_meta_list_schemas(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
590
|
+
envelope = app.meta_list_schemas(project=args.project)
|
|
591
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _handle_meta_semantic_set(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
595
|
+
"""Handle semantic set command."""
|
|
596
|
+
import json
|
|
597
|
+
|
|
598
|
+
# Parse JSON arguments if provided
|
|
599
|
+
column_semantics = None
|
|
600
|
+
if args.column_semantics:
|
|
601
|
+
try:
|
|
602
|
+
column_semantics = json.loads(args.column_semantics)
|
|
603
|
+
except json.JSONDecodeError as e:
|
|
604
|
+
envelope = Envelope(
|
|
605
|
+
command="meta.semantic.set",
|
|
606
|
+
status="failure",
|
|
607
|
+
data=None,
|
|
608
|
+
metadata={},
|
|
609
|
+
error=ErrorPayload(
|
|
610
|
+
code="INVALID_JSON",
|
|
611
|
+
message=f"Invalid JSON for --column-semantics: {e}",
|
|
612
|
+
recoverable=True,
|
|
613
|
+
suggestion="Provide valid JSON for the --column-semantics argument.",
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
relations = None
|
|
620
|
+
if args.relations:
|
|
621
|
+
try:
|
|
622
|
+
relations = json.loads(args.relations)
|
|
623
|
+
except json.JSONDecodeError as e:
|
|
624
|
+
envelope = Envelope(
|
|
625
|
+
command="meta.semantic.set",
|
|
626
|
+
status="failure",
|
|
627
|
+
data=None,
|
|
628
|
+
metadata={},
|
|
629
|
+
error=ErrorPayload(
|
|
630
|
+
code="INVALID_JSON",
|
|
631
|
+
message=f"Invalid JSON for --relations: {e}",
|
|
632
|
+
recoverable=True,
|
|
633
|
+
suggestion="Provide valid JSON for the --relations argument.",
|
|
634
|
+
),
|
|
635
|
+
)
|
|
636
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
stats = None
|
|
640
|
+
if args.stats:
|
|
641
|
+
try:
|
|
642
|
+
stats = json.loads(args.stats)
|
|
643
|
+
except json.JSONDecodeError as e:
|
|
644
|
+
envelope = Envelope(
|
|
645
|
+
command="meta.semantic.set",
|
|
646
|
+
status="failure",
|
|
647
|
+
data=None,
|
|
648
|
+
metadata={},
|
|
649
|
+
error=ErrorPayload(
|
|
650
|
+
code="INVALID_JSON",
|
|
651
|
+
message=f"Invalid JSON for --stats: {e}",
|
|
652
|
+
recoverable=True,
|
|
653
|
+
suggestion="Provide valid JSON for the --stats argument.",
|
|
654
|
+
),
|
|
655
|
+
)
|
|
656
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
envelope = app.semantic_set(
|
|
660
|
+
table_name=args.table_name,
|
|
661
|
+
semantic_desc=args.semantic_desc,
|
|
662
|
+
use_cases=args.use_cases,
|
|
663
|
+
sample_questions=args.sample_questions,
|
|
664
|
+
column_semantics=column_semantics,
|
|
665
|
+
relations=relations,
|
|
666
|
+
stats=stats,
|
|
667
|
+
)
|
|
668
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _handle_meta_semantic_get(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
672
|
+
"""Handle semantic get command."""
|
|
673
|
+
envelope = app.semantic_get(table_name=args.table_name)
|
|
674
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _handle_meta_semantic_list_missing(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
678
|
+
"""Handle semantic list-missing command."""
|
|
679
|
+
envelope = app.semantic_list_missing()
|
|
680
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _handle_session_set(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
684
|
+
"""Set current project and/or schema for the session."""
|
|
685
|
+
project = args.project
|
|
686
|
+
schema = args.schema
|
|
687
|
+
|
|
688
|
+
if not project and not schema:
|
|
689
|
+
raise ValidationError("At least one of `--project` or `--schema` must be specified.")
|
|
690
|
+
|
|
691
|
+
envelope = app.session_set(project=project, schema=schema)
|
|
692
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _handle_session_show(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
696
|
+
"""Show current session settings."""
|
|
697
|
+
envelope = app.session_show()
|
|
698
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _handle_session_unset(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
702
|
+
"""Clear session override."""
|
|
703
|
+
envelope = app.session_unset()
|
|
704
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _handle_data_sample(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
708
|
+
columns = _csv_arg_list(args.columns)
|
|
709
|
+
envelope = app.data_sample(
|
|
710
|
+
args.table_name,
|
|
711
|
+
rows=args.rows,
|
|
712
|
+
partition=args.partition,
|
|
713
|
+
columns=columns or None,
|
|
714
|
+
)
|
|
715
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="table")
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _handle_data_profile(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
719
|
+
envelope = app.data_profile(args.table_name, partition=args.partition)
|
|
720
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _handle_auth_login(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
724
|
+
envelope = app.auth_login(
|
|
725
|
+
access_id=args.access_id,
|
|
726
|
+
secret_access_key=args.secret_access_key,
|
|
727
|
+
security_token=args.security_token,
|
|
728
|
+
project=args.project,
|
|
729
|
+
endpoint=args.endpoint,
|
|
730
|
+
region_name=args.region_name,
|
|
731
|
+
tunnel_endpoint=args.tunnel_endpoint,
|
|
732
|
+
from_env=args.from_env,
|
|
733
|
+
no_validate=args.no_validate,
|
|
734
|
+
target_config_path=args.requested_config_path,
|
|
735
|
+
)
|
|
736
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _handle_auth_login_ncs(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
740
|
+
envelope = app.auth_login_ncs(
|
|
741
|
+
account_type=args.account_type,
|
|
742
|
+
employee_id=args.employee_id,
|
|
743
|
+
account_name=args.account_name,
|
|
744
|
+
app_name=args.app_name,
|
|
745
|
+
project=args.project,
|
|
746
|
+
endpoint=args.endpoint,
|
|
747
|
+
region_name=args.region_name,
|
|
748
|
+
tunnel_endpoint=args.tunnel_endpoint,
|
|
749
|
+
interactive=args.interactive,
|
|
750
|
+
list_accounts_mode=args.list_accounts,
|
|
751
|
+
no_validate=args.no_validate,
|
|
752
|
+
target_config_path=args.requested_config_path,
|
|
753
|
+
)
|
|
754
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _handle_auth_whoami(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
758
|
+
envelope = app.auth_whoami()
|
|
759
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _handle_auth_can_i(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
763
|
+
envelope = app.auth_can_i(
|
|
764
|
+
table_name=args.table,
|
|
765
|
+
operation=args.operation,
|
|
766
|
+
project=args.project,
|
|
767
|
+
)
|
|
768
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _handle_diff_schema(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
772
|
+
envelope = app.schema_diff(args.left_table, args.right_table)
|
|
773
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _handle_diff_partition(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
777
|
+
envelope = app.partition_diff(args.left_table, args.right_table)
|
|
778
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _handle_diff_data(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
782
|
+
envelope = app.data_diff(
|
|
783
|
+
args.left_table,
|
|
784
|
+
args.right_table,
|
|
785
|
+
keys=_csv_arg_list(args.keys),
|
|
786
|
+
columns=_csv_arg_list(args.columns) or None,
|
|
787
|
+
rows=args.rows,
|
|
788
|
+
partition=args.partition,
|
|
789
|
+
left_partition=args.left_partition,
|
|
790
|
+
right_partition=args.right_partition,
|
|
791
|
+
)
|
|
792
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _handle_agent_context(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
796
|
+
envelope = app.agent_context()
|
|
797
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _handle_cache_build(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
801
|
+
"""Build the metadata cache.
|
|
802
|
+
|
|
803
|
+
JSON or async invocations return the standard envelope contract.
|
|
804
|
+
Human-mode synchronous builds keep the incremental terminal progress output.
|
|
805
|
+
"""
|
|
806
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
807
|
+
import threading
|
|
808
|
+
import time
|
|
809
|
+
import uuid
|
|
810
|
+
|
|
811
|
+
is_json_mode = getattr(args, "json", False)
|
|
812
|
+
is_async_mode = getattr(args, "async_mode", False)
|
|
813
|
+
target_project = args.project or app.config.default_project
|
|
814
|
+
schema_name = getattr(args, "schema", None)
|
|
815
|
+
max_workers = 8
|
|
816
|
+
|
|
817
|
+
if is_async_mode:
|
|
818
|
+
envelope = app.cache_build(
|
|
819
|
+
project=args.project,
|
|
820
|
+
schema_name=schema_name,
|
|
821
|
+
async_mode=is_async_mode,
|
|
822
|
+
)
|
|
823
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
824
|
+
return
|
|
825
|
+
|
|
826
|
+
if is_json_mode:
|
|
827
|
+
progress_stream = getattr(args, "stderr", None) or sys.stderr
|
|
828
|
+
last_progress_emit = 0.0
|
|
829
|
+
|
|
830
|
+
def emit_progress(event: 'dict[str, Any]') -> 'None':
|
|
831
|
+
nonlocal last_progress_emit
|
|
832
|
+
event_type = str(event.get("type", ""))
|
|
833
|
+
now = time.monotonic()
|
|
834
|
+
if event_type == "listing_start":
|
|
835
|
+
progress_stream.write("Fetching table list...\n")
|
|
836
|
+
progress_stream.flush()
|
|
837
|
+
return
|
|
838
|
+
if event_type == "listing_complete":
|
|
839
|
+
total = int(event.get("total_tables", 0))
|
|
840
|
+
progress_stream.write(f"Discovered {total} table(s), starting cache build...\n")
|
|
841
|
+
progress_stream.flush()
|
|
842
|
+
return
|
|
843
|
+
if event_type == "progress":
|
|
844
|
+
if now - last_progress_emit < 0.5:
|
|
845
|
+
return
|
|
846
|
+
last_progress_emit = now
|
|
847
|
+
progress_stream.write(
|
|
848
|
+
"\rProgress: {cached}/{total} tables cached (failed: {failed})".format(
|
|
849
|
+
cached=event.get("cached_tables", 0),
|
|
850
|
+
total=event.get("total_tables", 0),
|
|
851
|
+
failed=event.get("failed_tables", 0),
|
|
852
|
+
)
|
|
853
|
+
)
|
|
854
|
+
progress_stream.flush()
|
|
855
|
+
return
|
|
856
|
+
if event_type == "completed":
|
|
857
|
+
progress_stream.write(
|
|
858
|
+
"\rProgress: {cached}/{total} tables cached (failed: {failed})\n".format(
|
|
859
|
+
cached=event.get("cached_tables", 0),
|
|
860
|
+
total=event.get("total_tables", 0),
|
|
861
|
+
failed=event.get("failed_tables", 0),
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
progress_stream.flush()
|
|
865
|
+
|
|
866
|
+
envelope = app.cache_build(
|
|
867
|
+
project=args.project,
|
|
868
|
+
schema_name=schema_name,
|
|
869
|
+
async_mode=False,
|
|
870
|
+
progress_callback=emit_progress,
|
|
871
|
+
)
|
|
872
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
# Phase 1: Single-threaded list all tables
|
|
876
|
+
stdout.write("Fetching table list...\n")
|
|
877
|
+
stdout.flush()
|
|
878
|
+
|
|
879
|
+
tables = app.backend.list_tables()
|
|
880
|
+
total = len(tables)
|
|
881
|
+
|
|
882
|
+
if total == 0:
|
|
883
|
+
stdout.write("No tables found, cache build completed.\n")
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
build_id = str(uuid.uuid4())[:8]
|
|
887
|
+
app.cache.start_build(target_project, build_id, total)
|
|
888
|
+
|
|
889
|
+
# Output initial state
|
|
890
|
+
stdout.write(f"Target project: {target_project}\n")
|
|
891
|
+
if schema_name:
|
|
892
|
+
stdout.write(f"Target schema: {schema_name}\n")
|
|
893
|
+
stdout.write(f"Discovered {total} table(s), starting cache build...\n\n")
|
|
894
|
+
stdout.flush()
|
|
895
|
+
|
|
896
|
+
# Phase 2: Multi-threaded fetch schema for each table
|
|
897
|
+
cached_count = 0
|
|
898
|
+
errors: 'list[str]' = []
|
|
899
|
+
lock = threading.Lock()
|
|
900
|
+
last_progress_time = time.monotonic()
|
|
901
|
+
progress_interval = 3.0 # seconds
|
|
902
|
+
|
|
903
|
+
def fetch_and_cache(table_name: 'str') -> 'tuple[str, str | None]':
|
|
904
|
+
"""Fetch and cache a table. Returns (table_name, error_or_none)."""
|
|
905
|
+
import concurrent.futures
|
|
906
|
+
|
|
907
|
+
def _do_fetch():
|
|
908
|
+
# Use a simpler approach: only get table metadata without sample rows
|
|
909
|
+
# to avoid potential hangs on table.head() or iterate_partitions()
|
|
910
|
+
table = app.backend._get_table(table_name)
|
|
911
|
+
# Force reload to get full schema info
|
|
912
|
+
if hasattr(table, 'reload'):
|
|
913
|
+
table.reload()
|
|
914
|
+
|
|
915
|
+
columns = [
|
|
916
|
+
{"name": c.name, "type": str(c.type), "comment": getattr(c, 'comment', '') or ''}
|
|
917
|
+
for c in getattr(table.table_schema, 'columns', [])
|
|
918
|
+
]
|
|
919
|
+
|
|
920
|
+
# Get partition columns but don't fetch actual partitions (can be slow)
|
|
921
|
+
partitions = [
|
|
922
|
+
c.name for c in getattr(table.table_schema, 'partitions', [])
|
|
923
|
+
]
|
|
924
|
+
|
|
925
|
+
row_count = int(getattr(table, 'record_num', -1) or -1)
|
|
926
|
+
size_bytes = int(getattr(table, 'size', 0)) if getattr(table, 'size', None) else None
|
|
927
|
+
|
|
928
|
+
app.cache.cache_table(
|
|
929
|
+
project=target_project,
|
|
930
|
+
table_name=table.name,
|
|
931
|
+
description=getattr(table, 'comment', '') or '',
|
|
932
|
+
columns=columns,
|
|
933
|
+
partitions=partitions,
|
|
934
|
+
row_count=row_count if row_count >= 0 else None,
|
|
935
|
+
size_bytes=size_bytes,
|
|
936
|
+
owner=getattr(table, 'owner', None),
|
|
937
|
+
schema_name=schema_name or "default",
|
|
938
|
+
)
|
|
939
|
+
return table_name, None
|
|
940
|
+
|
|
941
|
+
# Run with timeout to prevent hanging
|
|
942
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
943
|
+
future = executor.submit(_do_fetch)
|
|
944
|
+
try:
|
|
945
|
+
return future.result(timeout=30) # 30 second timeout per table
|
|
946
|
+
except concurrent.futures.TimeoutError:
|
|
947
|
+
return table_name, f"{table_name}: timeout after 30s"
|
|
948
|
+
except Exception as exc:
|
|
949
|
+
return table_name, f"{table_name}: {exc}"
|
|
950
|
+
|
|
951
|
+
def emit_progress(force: 'bool' = False) -> 'None':
|
|
952
|
+
nonlocal last_progress_time
|
|
953
|
+
current_time = time.monotonic()
|
|
954
|
+
|
|
955
|
+
_ = force
|
|
956
|
+
if current_time - last_progress_time >= progress_interval:
|
|
957
|
+
last_progress_time = current_time
|
|
958
|
+
stdout.write(f"\rProgress: {cached_count}/{total} tables cached (failed: {len(errors)})")
|
|
959
|
+
stdout.flush()
|
|
960
|
+
|
|
961
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
962
|
+
futures = {executor.submit(fetch_and_cache, t.name): t.name for t in tables}
|
|
963
|
+
|
|
964
|
+
for future in as_completed(futures):
|
|
965
|
+
table_name, error = future.result()
|
|
966
|
+
|
|
967
|
+
with lock:
|
|
968
|
+
if error:
|
|
969
|
+
errors.append(error)
|
|
970
|
+
else:
|
|
971
|
+
cached_count += 1
|
|
972
|
+
|
|
973
|
+
# Move emit_progress outside the lock to avoid holding lock during IO
|
|
974
|
+
emit_progress()
|
|
975
|
+
|
|
976
|
+
# Update build progress in DB (can be done outside lock)
|
|
977
|
+
app.cache.update_build_progress(
|
|
978
|
+
target_project, build_id, cached_count, len(errors)
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Complete build
|
|
982
|
+
if errors:
|
|
983
|
+
app.cache.complete_build(target_project, build_id, error_message=f"{len(errors)} errors")
|
|
984
|
+
else:
|
|
985
|
+
app.cache.complete_build(target_project, build_id)
|
|
986
|
+
|
|
987
|
+
# Final output
|
|
988
|
+
stdout.write("\n\n")
|
|
989
|
+
|
|
990
|
+
if not errors:
|
|
991
|
+
stdout.write("Cache build completed successfully!\n")
|
|
992
|
+
stdout.write(f" Tables cached: {cached_count}/{total}\n")
|
|
993
|
+
else:
|
|
994
|
+
stdout.write("Cache build completed with errors.\n")
|
|
995
|
+
stdout.write(f" Succeeded: {cached_count}\n")
|
|
996
|
+
stdout.write(f" Failed: {len(errors)}\n")
|
|
997
|
+
error_list = errors[:5]
|
|
998
|
+
if error_list:
|
|
999
|
+
stdout.write("\nError details:\n")
|
|
1000
|
+
for error in error_list:
|
|
1001
|
+
stdout.write(f" - {error}\n")
|
|
1002
|
+
if len(errors) > 5:
|
|
1003
|
+
stdout.write(f" ... and {len(errors) - 5} more error(s)\n")
|
|
1004
|
+
|
|
1005
|
+
stats = app.cache.get_cache_stats(target_project)
|
|
1006
|
+
if stats:
|
|
1007
|
+
stdout.write("\nCache stats:\n")
|
|
1008
|
+
stdout.write(f" Total tables: {stats.get('table_count', 0)}\n")
|
|
1009
|
+
if stats.get("oldest"):
|
|
1010
|
+
stdout.write(f" Oldest update: {stats.get('oldest')}\n")
|
|
1011
|
+
if stats.get("newest"):
|
|
1012
|
+
stdout.write(f" Newest update: {stats.get('newest')}\n")
|
|
1013
|
+
|
|
1014
|
+
schemas = app.cache.get_schemas(target_project)
|
|
1015
|
+
if schemas:
|
|
1016
|
+
stdout.write("\nCached schemas:\n")
|
|
1017
|
+
for schema in schemas:
|
|
1018
|
+
stdout.write(f" - {schema}\n")
|
|
1019
|
+
|
|
1020
|
+
stdout.write("\n")
|
|
1021
|
+
stdout.flush()
|
|
1022
|
+
|
|
1023
|
+
app.log("cache.build", "success" if not errors else "partial", {"project": target_project})
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _handle_cache_build_status(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
1028
|
+
envelope = app.cache_build_status(
|
|
1029
|
+
project=args.project,
|
|
1030
|
+
build_id=getattr(args, 'build_id', None),
|
|
1031
|
+
)
|
|
1032
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _handle_cache_status(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
1036
|
+
envelope = app.cache_status(
|
|
1037
|
+
project=args.project,
|
|
1038
|
+
schema_name=getattr(args, 'schema', None),
|
|
1039
|
+
)
|
|
1040
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _handle_cache_clear(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
1044
|
+
envelope = app.cache_clear(
|
|
1045
|
+
project=args.project,
|
|
1046
|
+
schema_name=getattr(args, 'schema', None),
|
|
1047
|
+
)
|
|
1048
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _handle_cache_save_semantic(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
1052
|
+
import json as json_module
|
|
1053
|
+
envelope = app.cache_save_semantic(
|
|
1054
|
+
table_name=args.table,
|
|
1055
|
+
semantic_desc=args.semantic_desc,
|
|
1056
|
+
use_cases=json_module.loads(args.use_cases),
|
|
1057
|
+
sample_questions=json_module.loads(args.sample_questions),
|
|
1058
|
+
column_semantics=json_module.loads(args.column_semantics),
|
|
1059
|
+
project=args.project,
|
|
1060
|
+
schema_name=getattr(args, 'schema', 'default'),
|
|
1061
|
+
)
|
|
1062
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _handle_cache_get_semantic(app: 'MaxCApp', args: 'argparse.Namespace', stdout: 'TextIO') -> 'None':
|
|
1066
|
+
envelope = app.cache_get_semantic(
|
|
1067
|
+
table_name=args.table,
|
|
1068
|
+
project=args.project,
|
|
1069
|
+
schema_name=getattr(args, 'schema', 'default'),
|
|
1070
|
+
)
|
|
1071
|
+
_emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _emit_envelope(
|
|
1075
|
+
envelope: 'Envelope',
|
|
1076
|
+
*,
|
|
1077
|
+
args: 'argparse.Namespace',
|
|
1078
|
+
stdout: 'TextIO',
|
|
1079
|
+
default_format: 'str',
|
|
1080
|
+
) -> 'None':
|
|
1081
|
+
if getattr(args, "brief", False):
|
|
1082
|
+
stdout.write(_render_brief(envelope) + "\n")
|
|
1083
|
+
return
|
|
1084
|
+
if getattr(args, "json", False):
|
|
1085
|
+
emit_json(envelope.to_dict(), stdout)
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
if default_format == "ndjson":
|
|
1089
|
+
rows = envelope.data.get("rows", [])
|
|
1090
|
+
emit_ndjson(rows, stdout)
|
|
1091
|
+
return
|
|
1092
|
+
if default_format == "csv":
|
|
1093
|
+
_emit_csv(envelope.data.get("rows", []), stdout)
|
|
1094
|
+
return
|
|
1095
|
+
if default_format == "json":
|
|
1096
|
+
emit_json(envelope.data, stdout)
|
|
1097
|
+
return
|
|
1098
|
+
|
|
1099
|
+
stdout.write(_render_human(envelope) + "\n")
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _render_brief(envelope: 'Envelope') -> 'str':
|
|
1103
|
+
if envelope.command == "auth.can-i":
|
|
1104
|
+
return "ALLOWED" if envelope.data.get("allowed") else "DENIED"
|
|
1105
|
+
return envelope.status.upper()
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _render_human(envelope: 'Envelope') -> 'str':
|
|
1109
|
+
command = envelope.command
|
|
1110
|
+
data = envelope.data
|
|
1111
|
+
metadata = envelope.metadata
|
|
1112
|
+
|
|
1113
|
+
if command == "query":
|
|
1114
|
+
rows = data.get("rows", [])
|
|
1115
|
+
summary = render_key_values(
|
|
1116
|
+
{
|
|
1117
|
+
"status": envelope.status,
|
|
1118
|
+
"project": metadata.get("project"),
|
|
1119
|
+
"elapsed_ms": metadata.get("elapsed_ms"),
|
|
1120
|
+
"returned_rows": data.get("returned_rows"),
|
|
1121
|
+
"total_rows": data.get("total_rows"),
|
|
1122
|
+
"has_more": data.get("has_more"),
|
|
1123
|
+
"next_cursor": data.get("next_cursor"),
|
|
1124
|
+
"current_offset": metadata.get("current_offset"),
|
|
1125
|
+
"bytes_scanned": metadata.get("bytes_scanned"),
|
|
1126
|
+
"task_cost_cpu": metadata.get("task_cost_cpu"),
|
|
1127
|
+
"task_cost_memory": metadata.get("task_cost_memory"),
|
|
1128
|
+
"tables": metadata.get("tables_used", []),
|
|
1129
|
+
}
|
|
1130
|
+
)
|
|
1131
|
+
body = render_table(rows)
|
|
1132
|
+
return f"{summary}\n\n{body}"
|
|
1133
|
+
|
|
1134
|
+
if command in {"query.cost", "query.explain"}:
|
|
1135
|
+
return render_key_values(data)
|
|
1136
|
+
|
|
1137
|
+
if command == "meta.list-tables":
|
|
1138
|
+
return render_table(data.get("tables", []))
|
|
1139
|
+
|
|
1140
|
+
if command in {"meta.search", "meta.search-columns"}:
|
|
1141
|
+
return render_table(data.get("matches", []))
|
|
1142
|
+
|
|
1143
|
+
if command == "data.sample":
|
|
1144
|
+
return render_table(data.get("rows", []))
|
|
1145
|
+
|
|
1146
|
+
if command == "skill.list":
|
|
1147
|
+
return render_table(data.get("skills", []))
|
|
1148
|
+
|
|
1149
|
+
return render_key_values(data if isinstance(data, dict) else {"value": data})
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _emit_csv(rows: 'list[dict[str, Any]]', stdout: 'TextIO') -> 'None':
|
|
1153
|
+
if not rows:
|
|
1154
|
+
stdout.write("\n")
|
|
1155
|
+
return
|
|
1156
|
+
columns = list(rows[0])
|
|
1157
|
+
stdout.write(",".join(columns) + "\n")
|
|
1158
|
+
for row in rows:
|
|
1159
|
+
stdout.write(",".join(str(row.get(column, "")) for column in columns) + "\n")
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _write_output_file(envelope: 'Envelope', raw_path: 'str', output_format: 'str') -> 'Path':
|
|
1163
|
+
path = Path(raw_path).expanduser().resolve()
|
|
1164
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1165
|
+
with path.open("w", encoding="utf-8") as handle:
|
|
1166
|
+
if output_format == "ndjson":
|
|
1167
|
+
emit_ndjson(envelope.data.get("rows", []), handle)
|
|
1168
|
+
elif output_format == "csv":
|
|
1169
|
+
_emit_csv(envelope.data.get("rows", []), handle)
|
|
1170
|
+
elif output_format == "table":
|
|
1171
|
+
handle.write(render_table(envelope.data.get("rows", [])) + "\n")
|
|
1172
|
+
else:
|
|
1173
|
+
emit_json(envelope.data, handle)
|
|
1174
|
+
return path
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _command_name(args: 'argparse.Namespace') -> 'str':
|
|
1178
|
+
resolved = getattr(args, "resolved_command", None)
|
|
1179
|
+
if resolved:
|
|
1180
|
+
return resolved
|
|
1181
|
+
parts = [args.command_group]
|
|
1182
|
+
for attr in (
|
|
1183
|
+
"job_command",
|
|
1184
|
+
"meta_command",
|
|
1185
|
+
"semantic_command",
|
|
1186
|
+
"session_command",
|
|
1187
|
+
"data_command",
|
|
1188
|
+
"auth_command",
|
|
1189
|
+
"diff_command",
|
|
1190
|
+
"agent_command",
|
|
1191
|
+
"cache_command",
|
|
1192
|
+
"skill_command",
|
|
1193
|
+
):
|
|
1194
|
+
value = getattr(args, attr, None)
|
|
1195
|
+
if value:
|
|
1196
|
+
parts.append(value)
|
|
1197
|
+
return ".".join(parts)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _should_load_backend(command_name: 'str') -> 'bool':
|
|
1201
|
+
return command_name not in {
|
|
1202
|
+
"auth.login",
|
|
1203
|
+
"auth.login-ncs",
|
|
1204
|
+
"auth.whoami",
|
|
1205
|
+
"session.set",
|
|
1206
|
+
"session.show",
|
|
1207
|
+
"session.unset",
|
|
1208
|
+
"agent.context",
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _resolve_query_mode(args: 'argparse.Namespace') -> 'tuple[str, list[str]]':
|
|
1213
|
+
mode = args.mode
|
|
1214
|
+
sql_parts = list(args.sql_parts)
|
|
1215
|
+
alias = sql_parts[0].lower() if sql_parts else ""
|
|
1216
|
+
if mode != "run":
|
|
1217
|
+
if alias in {"run", "cost", "explain"} and (len(sql_parts) > 1 or args.file or args.stdin):
|
|
1218
|
+
raise ValidationError("Do not combine query mode aliases with `--mode`; pick one style.")
|
|
1219
|
+
return mode, sql_parts
|
|
1220
|
+
|
|
1221
|
+
if alias in {"run", "cost", "explain"} and (len(sql_parts) > 1 or args.file or args.stdin):
|
|
1222
|
+
return alias, sql_parts[1:]
|
|
1223
|
+
return mode, sql_parts
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def _validate_query_analysis_args(args: 'argparse.Namespace', mode: 'str') -> 'None':
|
|
1227
|
+
_ = mode
|
|
1228
|
+
unsupported = []
|
|
1229
|
+
if args.dry_run:
|
|
1230
|
+
unsupported.append("--dry-run")
|
|
1231
|
+
if args.cursor:
|
|
1232
|
+
unsupported.append("--cursor")
|
|
1233
|
+
if args.output:
|
|
1234
|
+
unsupported.append("--output")
|
|
1235
|
+
if args.output_format:
|
|
1236
|
+
unsupported.append("--output-format")
|
|
1237
|
+
if getattr(args, "wait", 10) != 10:
|
|
1238
|
+
unsupported.append("--wait")
|
|
1239
|
+
if unsupported:
|
|
1240
|
+
raise ValidationError(
|
|
1241
|
+
f"{', '.join(unsupported)} cannot be combined with `query cost` or `query explain`."
|
|
1242
|
+
)
|
|
1243
|
+
if args.format in {"csv", "ndjson"}:
|
|
1244
|
+
raise ValidationError("`query cost` and `query explain` support only `table` or `json` output.")
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _query_page_size(args: 'argparse.Namespace') -> 'int':
|
|
1248
|
+
return args.page_size if args.page_size is not None else args.max_rows
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _csv_arg_list(value: 'str | None') -> 'list[str]':
|
|
1252
|
+
return [item.strip() for item in (value or "").split(",") if item.strip()]
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _query_output_format(args: 'argparse.Namespace') -> 'str':
|
|
1256
|
+
if args.output_format:
|
|
1257
|
+
return args.output_format
|
|
1258
|
+
if args.output:
|
|
1259
|
+
suffix = Path(args.output).suffix.lower()
|
|
1260
|
+
if suffix == ".csv":
|
|
1261
|
+
return "csv"
|
|
1262
|
+
if suffix == ".ndjson":
|
|
1263
|
+
return "ndjson"
|
|
1264
|
+
if suffix == ".table":
|
|
1265
|
+
return "table"
|
|
1266
|
+
if args.format in {"json", "csv", "ndjson", "table"}:
|
|
1267
|
+
return args.format
|
|
1268
|
+
return "json"
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def _query_default_format(app: 'MaxCApp', mode: 'str') -> 'str':
|
|
1272
|
+
if mode == "run":
|
|
1273
|
+
return app.config.default_format
|
|
1274
|
+
return "table"
|