maxc-cli 0.6.0__tar.gz → 0.6.1__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.
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/PKG-INFO +1 -1
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/odps.py +83 -36
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_query_result_csv_fallback.py +120 -2
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/MANIFEST.in +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/README.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/pyproject.toml +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/scripts/regression_test.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/setup.cfg +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/setup.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/app.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/query.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/cli.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/oauth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_ai_native_contract_regressions.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_cache.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_catalog.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_compat.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_config_atomic_write.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_effective_hints_contract.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_help_format.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_helpers.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_installer_contracts.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_integration.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_job_store_durability.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_masking.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_oauth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_output_format_contract.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_phase1_improvements.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_semantic_management.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_setting_parser.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.6.0 → maxc_cli-0.6.1}/tests/test_state_portability.py +0 -0
|
@@ -56,6 +56,25 @@ def _summarize_fallback_warning(message: 'str') -> 'str | None':
|
|
|
56
56
|
return None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
_RAW_RESULT_COLUMNS = ("task_name", "result")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _raw_task_result_rows(task_results: 'dict[str, Any]') -> 'list[dict[str, Any]]':
|
|
63
|
+
"""Materialize raw task results as single-column rows for degraded reads.
|
|
64
|
+
|
|
65
|
+
Used when the tabular result reader cannot parse the server's CSV text
|
|
66
|
+
(e.g. unquoted commas inside SHOW INDEXES column values). The original
|
|
67
|
+
result strings are returned verbatim without type casting.
|
|
68
|
+
"""
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
"task_name": str(name),
|
|
72
|
+
"result": "" if value is None else str(value),
|
|
73
|
+
}
|
|
74
|
+
for name, value in task_results.items()
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
59
78
|
class OdpsBackend(
|
|
60
79
|
JobMixin, # JobMixin extends QueryMixin
|
|
61
80
|
CatalogMixin,
|
|
@@ -116,7 +135,9 @@ class OdpsBackend(
|
|
|
116
135
|
schema: list[dict[str, Any]] = []
|
|
117
136
|
rows: list[dict[str, Any]] = []
|
|
118
137
|
total_rows = 0
|
|
138
|
+
degraded_reason: str | None = None
|
|
119
139
|
else:
|
|
140
|
+
degraded_reason = None
|
|
120
141
|
try:
|
|
121
142
|
# Capture pyodps's tunnel-fallback UserWarnings so we can surface
|
|
122
143
|
# them via the envelope. catch_warnings(record=True) suppresses
|
|
@@ -124,42 +145,59 @@ class OdpsBackend(
|
|
|
124
145
|
# running without --json still see the original message.
|
|
125
146
|
with warnings.catch_warnings(record=True) as captured:
|
|
126
147
|
warnings.simplefilter("always")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
148
|
+
try:
|
|
149
|
+
with instance.open_reader() as reader:
|
|
150
|
+
# Materialize records first. When pyodps falls back from
|
|
151
|
+
# the instance tunnel to CsvRecordReader (e.g. on tunnel
|
|
152
|
+
# timeout), the column metadata is parsed lazily from the
|
|
153
|
+
# CSV header during the first __next__ call — so we can
|
|
154
|
+
# only inspect it after iteration begins.
|
|
155
|
+
records = list(islice(reader, offset, offset + max_rows))
|
|
156
|
+
|
|
157
|
+
reader_schema = getattr(reader, "schema", None)
|
|
158
|
+
if reader_schema is not None and hasattr(reader_schema, "columns"):
|
|
159
|
+
columns = list(reader_schema.columns)
|
|
160
|
+
else:
|
|
161
|
+
# Fallback path: CsvRecordReader exposes header columns
|
|
162
|
+
# via `_csv_columns` after iteration starts.
|
|
163
|
+
columns = list(getattr(reader, "_csv_columns", None) or [])
|
|
164
|
+
|
|
165
|
+
if not columns:
|
|
166
|
+
# Truly schema-less query-like result.
|
|
167
|
+
schema = []
|
|
168
|
+
rows = []
|
|
169
|
+
total_rows = 0
|
|
170
|
+
else:
|
|
171
|
+
schema = [
|
|
172
|
+
{
|
|
173
|
+
"name": column.name,
|
|
174
|
+
"type": str(column.type),
|
|
175
|
+
"comment": "",
|
|
176
|
+
}
|
|
177
|
+
for column in columns
|
|
178
|
+
]
|
|
179
|
+
column_names = [column["name"] for column in schema]
|
|
180
|
+
rows = [
|
|
181
|
+
record_to_dict(column_names, record.values)
|
|
182
|
+
for record in records
|
|
183
|
+
]
|
|
184
|
+
total_rows = int(getattr(reader, "count", len(rows)) or len(rows))
|
|
185
|
+
except Exception as reader_exc:
|
|
186
|
+
# Some SHOW results (e.g. multi-column SHOW INDEXES
|
|
187
|
+
# col_names) contain unquoted commas that make
|
|
188
|
+
# PyODPS's CSV reader raise IndexError while casting
|
|
189
|
+
# fields. The job itself succeeded, so degrade to the
|
|
190
|
+
# raw task-result text instead of failing the query.
|
|
191
|
+
task_results = self._safe_task_results(instance)
|
|
192
|
+
if not any(str(value).strip() for value in task_results.values()):
|
|
193
|
+
# No raw text to degrade to — surface the real error.
|
|
194
|
+
raise
|
|
195
|
+
degraded_reason = str(reader_exc) or type(reader_exc).__name__
|
|
196
|
+
all_raw_rows = _raw_task_result_rows(task_results)
|
|
197
|
+
rows = all_raw_rows[offset : offset + max_rows]
|
|
198
|
+
schema = [{"name": name, "type": "string", "comment": ""}
|
|
199
|
+
for name in _RAW_RESULT_COLUMNS]
|
|
200
|
+
total_rows = len(rows)
|
|
163
201
|
|
|
164
202
|
for w in captured:
|
|
165
203
|
msg_text = str(w.message)
|
|
@@ -174,10 +212,19 @@ class OdpsBackend(
|
|
|
174
212
|
except Exception as exc:
|
|
175
213
|
raise translate_odps_error(exc) from exc
|
|
176
214
|
|
|
215
|
+
if degraded_reason is not None:
|
|
216
|
+
fallback_warnings.append(
|
|
217
|
+
"Result reader failed to parse the task result "
|
|
218
|
+
f"({degraded_reason}); returning the raw task result text "
|
|
219
|
+
"verbatim in the `result` column without type casting."
|
|
220
|
+
)
|
|
221
|
+
|
|
177
222
|
bytes_scanned, extra_metadata = self._task_cost(instance)
|
|
178
223
|
returned_rows = len(rows)
|
|
179
224
|
has_more = total_rows > (offset + returned_rows)
|
|
180
225
|
extra_metadata["current_offset"] = offset
|
|
226
|
+
if degraded_reason is not None:
|
|
227
|
+
extra_metadata["result_kind"] = "raw_task_result"
|
|
181
228
|
if resultless_statement:
|
|
182
229
|
extra_metadata["result_kind"] = "statement"
|
|
183
230
|
if statement_operation is not None:
|
|
@@ -17,6 +17,7 @@ pytestmark = pytest.mark.unit
|
|
|
17
17
|
|
|
18
18
|
from odps.readers import CsvRecordReader
|
|
19
19
|
|
|
20
|
+
from maxc_cli.backend.job import JobMixin
|
|
20
21
|
from maxc_cli.backend.odps import OdpsBackend
|
|
21
22
|
|
|
22
23
|
|
|
@@ -27,9 +28,18 @@ class _FakeInstance:
|
|
|
27
28
|
start_time = None
|
|
28
29
|
end_time = None
|
|
29
30
|
|
|
30
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
reader,
|
|
34
|
+
*,
|
|
35
|
+
warn_on_open: str | None = None,
|
|
36
|
+
task_results: dict[str, str] | None = None,
|
|
37
|
+
raise_on_task_results: bool = False,
|
|
38
|
+
) -> None:
|
|
31
39
|
self._reader = reader
|
|
32
40
|
self._warn_on_open = warn_on_open
|
|
41
|
+
self._task_results = task_results
|
|
42
|
+
self._raise_on_task_results = raise_on_task_results
|
|
33
43
|
|
|
34
44
|
def open_reader(self):
|
|
35
45
|
if self._warn_on_open:
|
|
@@ -40,12 +50,18 @@ class _FakeInstance:
|
|
|
40
50
|
def get_task_cost(self):
|
|
41
51
|
return None
|
|
42
52
|
|
|
53
|
+
def get_task_results(self):
|
|
54
|
+
if self._raise_on_task_results or self._task_results is None:
|
|
55
|
+
raise RuntimeError("get_task_results not stubbed")
|
|
56
|
+
return dict(self._task_results)
|
|
57
|
+
|
|
43
58
|
|
|
44
59
|
class _StubBackend:
|
|
45
60
|
"""Bind just the methods under test without invoking OdpsBackend.__init__."""
|
|
46
61
|
|
|
47
62
|
project = "test_project"
|
|
48
63
|
_instance_to_query_result = OdpsBackend._instance_to_query_result
|
|
64
|
+
_safe_task_results = JobMixin._safe_task_results
|
|
49
65
|
_task_cost = OdpsBackend._task_cost
|
|
50
66
|
|
|
51
67
|
|
|
@@ -56,8 +72,15 @@ def _run(
|
|
|
56
72
|
max_rows: int = 100,
|
|
57
73
|
offset: int = 0,
|
|
58
74
|
warn_on_open: str | None = None,
|
|
75
|
+
task_results: dict[str, str] | None = None,
|
|
76
|
+
raise_on_task_results: bool = False,
|
|
59
77
|
):
|
|
60
|
-
instance = _FakeInstance(
|
|
78
|
+
instance = _FakeInstance(
|
|
79
|
+
reader,
|
|
80
|
+
warn_on_open=warn_on_open,
|
|
81
|
+
task_results=task_results,
|
|
82
|
+
raise_on_task_results=raise_on_task_results,
|
|
83
|
+
)
|
|
61
84
|
return _StubBackend()._instance_to_query_result(
|
|
62
85
|
instance,
|
|
63
86
|
project="test_project",
|
|
@@ -404,6 +427,101 @@ def test_query_like_statement_still_opens_result_reader() -> None:
|
|
|
404
427
|
)
|
|
405
428
|
|
|
406
429
|
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
# Raw task-result degradation: when the CSV reader cannot parse a successful
|
|
432
|
+
# job's result text (e.g. SHOW INDEXES multi-column col_names contain unquoted
|
|
433
|
+
# commas and PyODPS raises IndexError while casting), fall back to returning
|
|
434
|
+
# the raw task result verbatim instead of failing the query.
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
SHOW_INDEXES_MULTI_COLUMN_HEADER = (
|
|
438
|
+
"idx_name,tab_name,col_names,idx_tab_name,idx_type,comment\n"
|
|
439
|
+
)
|
|
440
|
+
SHOW_INDEXES_MULTI_COLUMN_ROW = (
|
|
441
|
+
"vidx,tbl,c1,c2,vidx_idx,VECTOR,\n"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_csv_index_error_degrades_to_raw_task_result() -> None:
|
|
446
|
+
"""The SHOW INDEXES crash: data rows have more CSV fields than the header,
|
|
447
|
+
so CsvRecordReader raises IndexError mid-iteration. The job succeeded, so
|
|
448
|
+
the envelope must carry the raw result text with an explanatory warning.
|
|
449
|
+
"""
|
|
450
|
+
raw = SHOW_INDEXES_MULTI_COLUMN_HEADER + SHOW_INDEXES_MULTI_COLUMN_ROW
|
|
451
|
+
reader = CsvRecordReader(schema=None, stream=raw)
|
|
452
|
+
result = _run(
|
|
453
|
+
reader,
|
|
454
|
+
sql="SHOW INDEXES ON proj.default.tbl",
|
|
455
|
+
task_results={"AnonymousSQLTask": raw},
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
assert [col["name"] for col in result.schema] == ["task_name", "result"]
|
|
459
|
+
assert result.rows == [{"task_name": "AnonymousSQLTask", "result": raw}]
|
|
460
|
+
assert result.total_rows == 1
|
|
461
|
+
assert result.extra_metadata["result_kind"] == "raw_task_result"
|
|
462
|
+
assert any("raw task result" in w for w in result.warnings), result.warnings
|
|
463
|
+
assert any("IndexError" in w or "index out of range" in w for w in result.warnings), (
|
|
464
|
+
result.warnings
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def test_real_pyodps_indexerror_on_unquoted_comma_row_degrades() -> None:
|
|
469
|
+
"""End-to-end through the real PyODPS reader: a row whose values contain
|
|
470
|
+
unquoted commas must degrade rather than propagate IndexError."""
|
|
471
|
+
raw = (
|
|
472
|
+
"a,b\n"
|
|
473
|
+
'1,"x"\n'
|
|
474
|
+
"2,x,y,z\n" # extra unquoted fields -> IndexError inside _cast_value
|
|
475
|
+
)
|
|
476
|
+
reader = CsvRecordReader(schema=None, stream=raw)
|
|
477
|
+
with pytest.raises(IndexError):
|
|
478
|
+
list(reader) # sanity: the bare reader genuinely fails
|
|
479
|
+
|
|
480
|
+
result = _run(
|
|
481
|
+
CsvRecordReader(schema=None, stream=raw),
|
|
482
|
+
sql="SHOW INDEXES ON t",
|
|
483
|
+
task_results={"SQLTask_1": raw},
|
|
484
|
+
)
|
|
485
|
+
assert result.rows[0]["result"] == raw
|
|
486
|
+
assert result.extra_metadata["result_kind"] == "raw_task_result"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_reader_failure_without_task_result_text_still_raises() -> None:
|
|
490
|
+
"""Degradation requires actual result text; empty or unreadable task
|
|
491
|
+
results keep the original error translation so real failures are not
|
|
492
|
+
masked as success."""
|
|
493
|
+
reader = CsvRecordReader(schema=None, stream="a,b\n1,2,3\n")
|
|
494
|
+
with pytest.raises(Exception, match="list index out of range"):
|
|
495
|
+
_run(reader, sql="SHOW INDEXES ON t", raise_on_task_results=True)
|
|
496
|
+
|
|
497
|
+
# Whitespace-only task results are treated as absent: the header row is
|
|
498
|
+
# consumed by the lazy column load, so iteration yields no records and the
|
|
499
|
+
# bare reader never raises. A genuinely unparseable body must still fail.
|
|
500
|
+
with pytest.raises(Exception, match="list index out of range"):
|
|
501
|
+
_run(
|
|
502
|
+
CsvRecordReader(schema=None, stream="hdr\nx\na,b\n1,2,3\n"),
|
|
503
|
+
sql="SHOW INDEXES ON t",
|
|
504
|
+
task_results={"AnonymousSQLTask": " \n"},
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_degraded_offset_and_max_rows_apply_to_task_rows() -> None:
|
|
509
|
+
raw_a = "a,b\n1,2,3\n"
|
|
510
|
+
raw_b = "a,b\n4,5,6\n"
|
|
511
|
+
reader = CsvRecordReader(schema=None, stream=raw_a)
|
|
512
|
+
result = _run(
|
|
513
|
+
reader,
|
|
514
|
+
sql="SHOW INDEXES ON t",
|
|
515
|
+
task_results={"taskA": raw_a, "taskB": raw_b},
|
|
516
|
+
max_rows=1,
|
|
517
|
+
offset=1,
|
|
518
|
+
)
|
|
519
|
+
assert [row["task_name"] for row in result.rows] == ["taskB"]
|
|
520
|
+
# total_rows counts the materialized window, so pagination terminates.
|
|
521
|
+
assert result.total_rows == 1
|
|
522
|
+
assert result.has_more is False
|
|
523
|
+
|
|
524
|
+
|
|
407
525
|
# ---------------------------------------------------------------------------
|
|
408
526
|
# Fallback-warning capture: surface pyodps's "tunnel fallback" UserWarning
|
|
409
527
|
# into the envelope so agents can see that the result was truncated.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|