maxc-cli 0.9.1__tar.gz → 0.9.3__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.9.1 → maxc_cli-0.9.3}/PKG-INFO +1 -1
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/app.py +12 -24
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/odps.py +22 -5
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/query.py +69 -18
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/cli.py +7 -9
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/exceptions.py +2 -3
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/models.py +2 -3
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/SKILL.md +8 -13
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/command-patterns.md +4 -3
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/json-output-format.md +8 -12
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +16 -28
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/red-lines.md +3 -3
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/utils.py +43 -804
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_ai_native_contract_regressions.py +23 -5
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_cli_mock.py +7 -2
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_effective_hints_contract.py +20 -20
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_phase1_improvements.py +3 -3
- maxc_cli-0.9.3/tests/test_query_namespace_settings.py +381 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_query_result_csv_fallback.py +62 -1
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_setting_parser.py +59 -288
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/MANIFEST.in +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/README.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/pyproject.toml +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/scripts/regression_test.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/setup.cfg +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/setup.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/auth_continuation.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/mcp.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/backend/semantic.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/enterprise_tls.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/function_metadata.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/job_ids.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/mcp_serve.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/oauth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/odps_runtime.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/proxy_auth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/semantic.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/semantic_management.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/registered-functions.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/state_permissions.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_auth_logout.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_auth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_mcp.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_backend_query_cost.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_build_release_archive_compat.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_cache.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_catalog.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_compat.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_config_atomic_write.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_connection_bootstrap.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_enterprise_tls.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_external_auth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_help_format.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_help_version_e2e.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_helpers.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_installer_contracts.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_integration.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_job_inspection.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_job_store_durability.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_kb_commands.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_manifest_runtime_contract.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_masking.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_mcp_serve.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_oauth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_odps_runtime.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_output_action_safety.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_output_format_contract.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_packaging_metadata.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_proxy_auth.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_python39_compat.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_registered_functions.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_semantic_management.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_semantic_scope.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_semantic_transport.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_skill_eval.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_skill_renderer.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_startup_imports.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_state_permissions.py +0 -0
- {maxc_cli-0.9.1 → maxc_cli-0.9.3}/tests/test_state_portability.py +0 -0
|
@@ -76,9 +76,9 @@ from .utils import (
|
|
|
76
76
|
decode_cursor,
|
|
77
77
|
encode_cursor,
|
|
78
78
|
enforce_read_only_sql,
|
|
79
|
-
known_write_operations,
|
|
80
79
|
now_utc_iso,
|
|
81
80
|
sql_has_limit,
|
|
81
|
+
sql_requires_authorization,
|
|
82
82
|
validate_csv_delimiter,
|
|
83
83
|
validate_download_output_path,
|
|
84
84
|
validate_upload_input_path,
|
|
@@ -772,11 +772,10 @@ class MaxCApp:
|
|
|
772
772
|
"the existing job using its persisted execution context."
|
|
773
773
|
),
|
|
774
774
|
)
|
|
775
|
-
|
|
776
|
-
if
|
|
775
|
+
requires_authorization = sql_requires_authorization(sql)
|
|
776
|
+
if requires_authorization and ((retry_on or []) or max_retries):
|
|
777
777
|
raise ValidationError(
|
|
778
|
-
"Automatic retries are not supported for mutating SQL "
|
|
779
|
-
f"({', '.join(write_operations)}).",
|
|
778
|
+
"Automatic retries are not supported for mutating SQL or SQL not proven read-only.",
|
|
780
779
|
suggestion=(
|
|
781
780
|
"Remove --retry-on and --max-retries. Inspect the original "
|
|
782
781
|
"job/result before deciding whether a manual retry is safe."
|
|
@@ -6939,16 +6938,10 @@ class MaxCApp:
|
|
|
6939
6938
|
force: 'bool' = False,
|
|
6940
6939
|
execution_settings: '_McqaExecutionSettings | None' = None,
|
|
6941
6940
|
) -> 'QueryResult':
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
"`@natural` is a roadmap feature and is not available in the current MVP.",
|
|
6945
|
-
suggestion="Use `maxc meta search` or `maxc meta describe` to inspect tables, then submit plain SQL.",
|
|
6946
|
-
)
|
|
6947
|
-
write_operations = known_write_operations(sql)
|
|
6948
|
-
if write_operations and (retry_on or max_retries):
|
|
6941
|
+
requires_authorization = sql_requires_authorization(sql)
|
|
6942
|
+
if requires_authorization and (retry_on or max_retries):
|
|
6949
6943
|
raise ValidationError(
|
|
6950
|
-
"Automatic retries are not supported for mutating SQL "
|
|
6951
|
-
f"({', '.join(write_operations)}).",
|
|
6944
|
+
"Automatic retries are not supported for mutating SQL or SQL not proven read-only.",
|
|
6952
6945
|
suggestion="Remove retry flags and verify the first execution before retrying manually.",
|
|
6953
6946
|
)
|
|
6954
6947
|
|
|
@@ -7018,11 +7011,6 @@ class MaxCApp:
|
|
|
7018
7011
|
explain: 'bool',
|
|
7019
7012
|
force: 'bool' = False,
|
|
7020
7013
|
) -> 'dict[str, Any]':
|
|
7021
|
-
if sql.startswith("@natural"):
|
|
7022
|
-
raise FeatureUnavailableError(
|
|
7023
|
-
"`@natural` is a roadmap feature and is not available in the current MVP.",
|
|
7024
|
-
suggestion="Use `maxc meta search` or `maxc meta describe` to inspect tables, then submit plain SQL.",
|
|
7025
|
-
)
|
|
7026
7014
|
if explain:
|
|
7027
7015
|
return self.backend.explain_query(sql, project=project, force=force)
|
|
7028
7016
|
return self.backend.estimate_query_cost(sql, project=project, force=force)
|
|
@@ -7125,16 +7113,16 @@ class MaxCApp:
|
|
|
7125
7113
|
|
|
7126
7114
|
# Build actions
|
|
7127
7115
|
qe_actions: list[SuggestedAction] = []
|
|
7128
|
-
|
|
7116
|
+
requires_authorization = sql_requires_authorization(result.sql_executed)
|
|
7129
7117
|
if result.tables_used and result.extra_metadata.get("result_kind") != "statement":
|
|
7130
7118
|
qe_actions.append(action("meta.describe", data=data, metadata=metadata))
|
|
7131
7119
|
if result.has_more and next_cursor:
|
|
7132
7120
|
qe_actions.append(action("query.paginate", data=data, metadata=metadata))
|
|
7133
7121
|
elif result.has_more and result.job_id:
|
|
7134
7122
|
qe_actions.append(action("job.result", data=data, metadata=metadata))
|
|
7135
|
-
if dry_run and not
|
|
7123
|
+
if dry_run and not requires_authorization:
|
|
7136
7124
|
qe_actions.append(action("job.submit", data=data, metadata=metadata))
|
|
7137
|
-
elif dry_run and
|
|
7125
|
+
elif dry_run and requires_authorization:
|
|
7138
7126
|
warnings.append(
|
|
7139
7127
|
"No replay action was generated for mutating SQL. A dry-run does not "
|
|
7140
7128
|
"authorize later execution; rerun the exact statement with `--force` "
|
|
@@ -7187,8 +7175,8 @@ class MaxCApp:
|
|
|
7187
7175
|
|
|
7188
7176
|
# Build actions
|
|
7189
7177
|
ae_actions: list[SuggestedAction] = []
|
|
7190
|
-
|
|
7191
|
-
if not
|
|
7178
|
+
requires_authorization = sql_requires_authorization(sql)
|
|
7179
|
+
if not requires_authorization:
|
|
7192
7180
|
if command == "query.cost":
|
|
7193
7181
|
ae_actions.append(action("query.explain", data=analysis, metadata=metadata))
|
|
7194
7182
|
ae_actions.append(action("query", data=analysis, metadata=metadata))
|
|
@@ -13,7 +13,7 @@ from ..helpers import (
|
|
|
13
13
|
translate_odps_error,
|
|
14
14
|
)
|
|
15
15
|
from ..models import QueryResult
|
|
16
|
-
from ..utils import extract_table_names
|
|
16
|
+
from ..utils import detect_operation, extract_table_names
|
|
17
17
|
from .auth import AuthMixin
|
|
18
18
|
from .catalog import CatalogMixin
|
|
19
19
|
from .data import DataMixin
|
|
@@ -124,9 +124,22 @@ class OdpsBackend(
|
|
|
124
124
|
"""Convert ODPS instance to QueryResult."""
|
|
125
125
|
fallback_warnings: list[str] = []
|
|
126
126
|
statement_operation = _write_operation(sql)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
instance_is_select = getattr(instance, "_is_select", None)
|
|
128
|
+
confirmed_task_results = None
|
|
129
|
+
resultless_statement = statement_operation is not None and instance_is_select is not True
|
|
130
|
+
if not resultless_statement and instance_is_select is False:
|
|
131
|
+
try:
|
|
132
|
+
task_results = instance.get_task_results()
|
|
133
|
+
except Exception:
|
|
134
|
+
# A result retrieval failure cannot prove an unknown statement
|
|
135
|
+
# has no output. Keep its reader/error path and original job.
|
|
136
|
+
pass
|
|
137
|
+
else:
|
|
138
|
+
if isinstance(task_results, dict):
|
|
139
|
+
confirmed_task_results = task_results
|
|
140
|
+
resultless_statement = not any(str(value or "").strip() for value in task_results.values())
|
|
141
|
+
if resultless_statement and sql.strip():
|
|
142
|
+
statement_operation = detect_operation(sql)
|
|
130
143
|
if resultless_statement:
|
|
131
144
|
# Successful write statements have no tabular result. In
|
|
132
145
|
# particular, PyODPS's DDL fallback reader may try to iterate a
|
|
@@ -198,7 +211,11 @@ class OdpsBackend(
|
|
|
198
211
|
# PyODPS's CSV reader raise IndexError while casting
|
|
199
212
|
# fields. The job itself succeeded, so degrade to the
|
|
200
213
|
# raw task-result text instead of failing the query.
|
|
201
|
-
task_results =
|
|
214
|
+
task_results = (
|
|
215
|
+
confirmed_task_results
|
|
216
|
+
if confirmed_task_results is not None
|
|
217
|
+
else self._safe_task_results(instance)
|
|
218
|
+
)
|
|
202
219
|
if not any(str(value).strip() for value in task_results.values()):
|
|
203
220
|
# No raw text to degrade to — surface the real error.
|
|
204
221
|
raise
|
|
@@ -117,9 +117,8 @@ def _parse_sql_with_hints(
|
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
# Validate both the executable statement and the leading SET execution
|
|
120
|
-
# context.
|
|
121
|
-
#
|
|
122
|
-
# sees them rather than falling through a mutation denylist.
|
|
120
|
+
# context. Reads are recognized locally; any other exact statement needs
|
|
121
|
+
# explicit authorization before MaxCompute validates its syntax and permissions.
|
|
123
122
|
enforce_read_only_sql(sql, force=force)
|
|
124
123
|
|
|
125
124
|
# Multi-statement SQL needs script mode for MaxCompute to accept it.
|
|
@@ -183,6 +182,47 @@ def _pop_priority(hints: 'dict[str, str]') -> 'int | None':
|
|
|
183
182
|
) from None
|
|
184
183
|
|
|
185
184
|
|
|
185
|
+
def _invoke_sql_with_hints(
|
|
186
|
+
method, sql: 'str', *, hints: 'dict[str, str]', schema_aware: 'bool' = False, **kwargs,
|
|
187
|
+
):
|
|
188
|
+
# PyODPS run_sql resolves self.schema without statement hints, then can
|
|
189
|
+
# overwrite an explicit two-tier hint with the tenant's three-tier default.
|
|
190
|
+
# Keep statement settings scoped while the original hints build each task.
|
|
191
|
+
namespace = next(
|
|
192
|
+
(value for key, value in hints.items() if key.lower() == "odps.namespace.schema"),
|
|
193
|
+
None,
|
|
194
|
+
)
|
|
195
|
+
namespace = namespace.strip().lower() if namespace is not None else None
|
|
196
|
+
if namespace not in {"false", "true"}:
|
|
197
|
+
return method(sql, hints=hints, **kwargs)
|
|
198
|
+
|
|
199
|
+
implicit_namespace = namespace
|
|
200
|
+
if namespace == "true":
|
|
201
|
+
default_schema = next(
|
|
202
|
+
(value for key, value in hints.items() if key.lower() == "odps.default.schema"),
|
|
203
|
+
None,
|
|
204
|
+
)
|
|
205
|
+
if default_schema and schema_aware:
|
|
206
|
+
# run_sql otherwise replaces the explicit schema hint with self.schema.
|
|
207
|
+
# Cost tasks and MCQA v1 accept the original hints, not this SDK kwarg.
|
|
208
|
+
kwargs["default_schema"] = default_schema
|
|
209
|
+
elif default_schema:
|
|
210
|
+
# MCQA v1 cannot accept default_schema, including before its offline
|
|
211
|
+
# fallback. Suppress self.schema's implicit default only in this
|
|
212
|
+
# context; the original true/schema hints still build every task.
|
|
213
|
+
implicit_namespace = "false"
|
|
214
|
+
|
|
215
|
+
from odps.config import option_context
|
|
216
|
+
|
|
217
|
+
with option_context() as options:
|
|
218
|
+
options.sql.settings = {
|
|
219
|
+
**(options.sql.settings or {}),
|
|
220
|
+
**hints,
|
|
221
|
+
"odps.namespace.schema": implicit_namespace,
|
|
222
|
+
}
|
|
223
|
+
return method(sql, hints=hints, **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
186
226
|
class QueryMixin:
|
|
187
227
|
"""Mixin providing query execution methods."""
|
|
188
228
|
|
|
@@ -202,10 +242,9 @@ class QueryMixin:
|
|
|
202
242
|
|
|
203
243
|
Parses any leading ``SET key=value;`` statements from the SQL
|
|
204
244
|
and passes them as execution hints to the MaxCompute backend.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
blocked even when *force* is True.
|
|
245
|
+
Statements not proven read-only require explicit authorization and
|
|
246
|
+
*force*. Approved operation names and SET keys are passed to MaxCompute
|
|
247
|
+
for syntax validation and permission checks.
|
|
209
248
|
|
|
210
249
|
Args:
|
|
211
250
|
sql: SQL query, optionally prefixed with SET statements.
|
|
@@ -215,7 +254,7 @@ class QueryMixin:
|
|
|
215
254
|
``client.execute_sql_cost()``).
|
|
216
255
|
offset: Row offset for cursor-based pagination.
|
|
217
256
|
timeout: Timeout in seconds (default: 300s / 5 minutes).
|
|
218
|
-
force: If True, allow one
|
|
257
|
+
force: If True, allow one explicitly authorized non-read statement.
|
|
219
258
|
|
|
220
259
|
Raises:
|
|
221
260
|
ValidationError: If SET syntax is invalid.
|
|
@@ -229,7 +268,8 @@ class QueryMixin:
|
|
|
229
268
|
|
|
230
269
|
if dry_run:
|
|
231
270
|
try:
|
|
232
|
-
sql_cost =
|
|
271
|
+
sql_cost = _invoke_sql_with_hints(
|
|
272
|
+
self.client.execute_sql_cost,
|
|
233
273
|
actual_sql, project=project, hints=hints,
|
|
234
274
|
)
|
|
235
275
|
except Exception as exc:
|
|
@@ -270,13 +310,16 @@ class QueryMixin:
|
|
|
270
310
|
interactive_kwargs["project"] = project
|
|
271
311
|
interactive_kwargs["use_mcqa_v2"] = True
|
|
272
312
|
interactive_kwargs["quota_name"] = getattr(execution_settings, "quota_name", None)
|
|
273
|
-
instance =
|
|
313
|
+
instance = _invoke_sql_with_hints(
|
|
314
|
+
self.client.execute_sql_interactive,
|
|
274
315
|
actual_sql,
|
|
316
|
+
schema_aware=getattr(execution_settings, "version", "v2") == "v2",
|
|
275
317
|
**interactive_kwargs,
|
|
276
318
|
)
|
|
277
319
|
else:
|
|
278
|
-
instance =
|
|
279
|
-
|
|
320
|
+
instance = _invoke_sql_with_hints(
|
|
321
|
+
self.client.run_sql,
|
|
322
|
+
actual_sql, project=project, hints=hints, schema_aware=True, **priority_kwargs,
|
|
280
323
|
)
|
|
281
324
|
except Exception as exc:
|
|
282
325
|
raise translate_odps_error(exc) from exc
|
|
@@ -334,7 +377,8 @@ class QueryMixin:
|
|
|
334
377
|
actual_sql, hints, _priority = _parse_sql_with_hints(sql, force=force)
|
|
335
378
|
started_monotonic = monotonic()
|
|
336
379
|
try:
|
|
337
|
-
sql_cost =
|
|
380
|
+
sql_cost = _invoke_sql_with_hints(
|
|
381
|
+
self.client.execute_sql_cost,
|
|
338
382
|
actual_sql, project=project, hints=hints,
|
|
339
383
|
)
|
|
340
384
|
except Exception as exc:
|
|
@@ -380,10 +424,12 @@ class QueryMixin:
|
|
|
380
424
|
plan_text: str | None = None
|
|
381
425
|
plan_warning: str | None = None
|
|
382
426
|
try:
|
|
383
|
-
instance =
|
|
427
|
+
instance = _invoke_sql_with_hints(
|
|
428
|
+
self.client.execute_sql,
|
|
384
429
|
f"EXPLAIN {actual_sql}",
|
|
385
430
|
project=project,
|
|
386
431
|
hints=explain_hints,
|
|
432
|
+
schema_aware=True,
|
|
387
433
|
**priority_kwargs,
|
|
388
434
|
)
|
|
389
435
|
try:
|
|
@@ -399,7 +445,8 @@ class QueryMixin:
|
|
|
399
445
|
|
|
400
446
|
# Cost estimate alongside the plan
|
|
401
447
|
try:
|
|
402
|
-
sql_cost =
|
|
448
|
+
sql_cost = _invoke_sql_with_hints(
|
|
449
|
+
self.client.execute_sql_cost,
|
|
403
450
|
actual_sql, project=project, hints=hints,
|
|
404
451
|
)
|
|
405
452
|
except Exception:
|
|
@@ -450,7 +497,7 @@ class QueryMixin:
|
|
|
450
497
|
sql: SQL query, optionally prefixed with SET statements.
|
|
451
498
|
project: ODPS project name.
|
|
452
499
|
idempotency_key: Optional unique ID for deduplication.
|
|
453
|
-
force: If True, allow one
|
|
500
|
+
force: If True, allow one explicitly authorized non-read statement
|
|
454
501
|
after explicit authorization.
|
|
455
502
|
|
|
456
503
|
Returns:
|
|
@@ -473,15 +520,19 @@ class QueryMixin:
|
|
|
473
520
|
interactive_kwargs["project"] = project
|
|
474
521
|
interactive_kwargs["use_mcqa_v2"] = True
|
|
475
522
|
interactive_kwargs["quota_name"] = getattr(execution_settings, "quota_name", None)
|
|
476
|
-
instance =
|
|
523
|
+
instance = _invoke_sql_with_hints(
|
|
524
|
+
self.client.run_sql_interactive,
|
|
477
525
|
actual_sql,
|
|
526
|
+
schema_aware=getattr(execution_settings, "version", "v2") == "v2",
|
|
478
527
|
**interactive_kwargs,
|
|
479
528
|
)
|
|
480
529
|
else:
|
|
481
|
-
instance =
|
|
530
|
+
instance = _invoke_sql_with_hints(
|
|
531
|
+
self.client.run_sql,
|
|
482
532
|
actual_sql,
|
|
483
533
|
project=project,
|
|
484
534
|
hints=hints,
|
|
535
|
+
schema_aware=True,
|
|
485
536
|
**idem_kwargs,
|
|
486
537
|
**priority_kwargs,
|
|
487
538
|
)
|
|
@@ -251,7 +251,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
251
251
|
help="Run or inspect a MaxCompute SQL statement",
|
|
252
252
|
description=(
|
|
253
253
|
"Run, estimate, or explain a MaxCompute SQL statement. "
|
|
254
|
-
"
|
|
254
|
+
"SQL not proven read-only requires explicit --force.\n"
|
|
255
255
|
"Usage:\n"
|
|
256
256
|
f" {cli_name} query \"SELECT 1\" # default: run\n"
|
|
257
257
|
f" {cli_name} query run \"SELECT 1\" # explicit run\n"
|
|
@@ -310,7 +310,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
310
310
|
"--force",
|
|
311
311
|
action="store_true",
|
|
312
312
|
default=False,
|
|
313
|
-
help="Allow one explicitly authorized
|
|
313
|
+
help="Allow one explicitly authorized non-read SQL statement (never inferred from a read request)",
|
|
314
314
|
)
|
|
315
315
|
query_parser.set_defaults(handler=_handle_query)
|
|
316
316
|
|
|
@@ -357,7 +357,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
357
357
|
"--force",
|
|
358
358
|
action="store_true",
|
|
359
359
|
default=False,
|
|
360
|
-
help="Allow one explicitly authorized
|
|
360
|
+
help="Allow one explicitly authorized non-read SQL statement (never inferred from a read request)",
|
|
361
361
|
)
|
|
362
362
|
job_submit.set_defaults(handler=_handle_job_submit, mcqa_fallback=None)
|
|
363
363
|
|
|
@@ -1713,14 +1713,12 @@ def run(
|
|
|
1713
1713
|
"WRITE_OPERATION_REQUIRES_FORCE": AgentHints(
|
|
1714
1714
|
warnings=[
|
|
1715
1715
|
"This write gate is not authorization. Use --force only for "
|
|
1716
|
-
"one exact
|
|
1716
|
+
"one exact non-read SQL statement explicitly authorized by the user."
|
|
1717
1717
|
],
|
|
1718
1718
|
),
|
|
1719
1719
|
"UNSUPPORTED_SQL_OPERATION": AgentHints(
|
|
1720
1720
|
warnings=[
|
|
1721
|
-
"
|
|
1722
|
-
"by the data-plane mutation allowlist; --force does not "
|
|
1723
|
-
"enable unknown or administrative operations."
|
|
1721
|
+
"Inspect the SQL error and the active execution context before retrying."
|
|
1724
1722
|
],
|
|
1725
1723
|
),
|
|
1726
1724
|
}
|
|
@@ -3372,7 +3370,7 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
|
|
|
3372
3370
|
},
|
|
3373
3371
|
agent_allowed=True,
|
|
3374
3372
|
confirmation="--force plus exact user authorization",
|
|
3375
|
-
note="One exact
|
|
3373
|
+
note="One exact non-read SQL statement; verify project, schema, target, and effect.",
|
|
3376
3374
|
),
|
|
3377
3375
|
_manifest_effect(
|
|
3378
3376
|
"local",
|
|
@@ -3433,7 +3431,7 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
|
|
|
3433
3431
|
},
|
|
3434
3432
|
agent_allowed=True,
|
|
3435
3433
|
confirmation="--force plus exact user authorization",
|
|
3436
|
-
note="One exact
|
|
3434
|
+
note="One exact non-read SQL statement; verify project, schema, target, and effect.",
|
|
3437
3435
|
),
|
|
3438
3436
|
_manifest_effect(
|
|
3439
3437
|
"local",
|
|
@@ -164,13 +164,12 @@ class MaxCError(Exception):
|
|
|
164
164
|
],
|
|
165
165
|
"WRITE_OPERATION_REQUIRES_FORCE": [
|
|
166
166
|
"This error is not authorization to write.",
|
|
167
|
-
"If the user explicitly requested the exact
|
|
167
|
+
"If the user explicitly requested the exact non-read SQL, verify its project, schema, target, and effect.",
|
|
168
168
|
"Submit that one statement with --force; never combine writes or infer one from a read request.",
|
|
169
169
|
],
|
|
170
170
|
"UNSUPPORTED_SQL_OPERATION": [
|
|
171
171
|
"Use SELECT, SHOW, DESC, DESCRIBE, EXPLAIN, or a WITH query whose outer statement is read-only.",
|
|
172
|
-
"
|
|
173
|
-
"Check the live command contract and use a dedicated approved workflow when the operation is outside the data-plane allowlist.",
|
|
172
|
+
"Inspect the SQL error and active execution context before requesting a new execution.",
|
|
174
173
|
],
|
|
175
174
|
}
|
|
176
175
|
return steps.get(self.error_code, [])
|
|
@@ -1025,15 +1025,14 @@ def build_safety_block(
|
|
|
1025
1025
|
effective_hints: 'dict[str, str] | None' = None,
|
|
1026
1026
|
) -> 'dict[str, Any]':
|
|
1027
1027
|
"""Build a safety block describing the read-only policy state."""
|
|
1028
|
-
from .utils import executable_operations,
|
|
1028
|
+
from .utils import executable_operations, sql_requires_authorization, statement_operations
|
|
1029
1029
|
|
|
1030
1030
|
if sql:
|
|
1031
1031
|
operations = executable_operations(sql) or statement_operations(sql)
|
|
1032
1032
|
else:
|
|
1033
1033
|
operations = ["SELECT"]
|
|
1034
|
-
write_operations = known_write_operations(sql) if sql else []
|
|
1035
1034
|
operations = list(dict.fromkeys(operations or ["SELECT"]))
|
|
1036
|
-
is_write =
|
|
1035
|
+
is_write = sql_requires_authorization(sql) if sql else False
|
|
1037
1036
|
rendered_hints = dict(effective_hints or {})
|
|
1038
1037
|
|
|
1039
1038
|
if force:
|
|
@@ -177,19 +177,14 @@ examples only when these paths are insufficient.
|
|
|
177
177
|
5. For partitioned tables, inspect `meta partitions` or
|
|
178
178
|
`meta latest-partition`, then include an explicit partition filter.
|
|
179
179
|
6. Cost-check broad or unfamiliar queries before execution.
|
|
180
|
-
7.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
channel: project-security and masking controls are always blocked, and a
|
|
189
|
-
forced mutation accepts only audited statement-local execution hints.
|
|
190
|
-
`data upload`,
|
|
191
|
-
`data download --overwrite`, and `job cancel` are separate mutations and
|
|
192
|
-
require authorization appropriate to their effect.
|
|
180
|
+
7. SELECT, SHOW, DESC, DESCRIBE, EXPLAIN and proven read-only WITH queries
|
|
181
|
+
use the read path. Every other statement requires explicit user approval
|
|
182
|
+
of the exact SQL, leading SET context, project, schema, target and effect.
|
|
183
|
+
Submit that one statement with `--force`; MaxCompute validates its syntax,
|
|
184
|
+
SET parameters and permissions. SET keys do not change read/write classification.
|
|
185
|
+
A write-gate error is not authorization to replay the SQL.
|
|
186
|
+
`data upload`, `data download --overwrite` and `job cancel` are separate
|
|
187
|
+
mutations and require authorization appropriate to their effect.
|
|
193
188
|
8. A failed command is not permission to retry indefinitely. Apply the
|
|
194
189
|
suggested recovery once, then stop or ask when the target or authority is
|
|
195
190
|
still unclear.
|
|
@@ -175,8 +175,9 @@ With SET options (parsed and passed as hints to MaxCompute):
|
|
|
175
175
|
|
|
176
176
|
Leading `SET` options are part of the remote execution context. They do not
|
|
177
177
|
authorize a write or an administrative change. Project-security,
|
|
178
|
-
access-control, and masking
|
|
179
|
-
|
|
178
|
+
access-control, and masking parameter validity and caller permissions are
|
|
179
|
+
validated by MaxCompute. SET keys do not change read/write classification;
|
|
180
|
+
there is no client-side operation or SET key allowlist or denylist.
|
|
180
181
|
|
|
181
182
|
Legacy-compatible syntax still works:
|
|
182
183
|
|
|
@@ -186,7 +187,7 @@ Legacy-compatible syntax still works:
|
|
|
186
187
|
|
|
187
188
|
The command is `query`, not `sql`. There is no `{{cli}} sql` command.
|
|
188
189
|
|
|
189
|
-
For an exact
|
|
190
|
+
For an exact non-read SQL statement the user has explicitly requested, first verify
|
|
190
191
|
the project, namespace, target, and effect, then submit only that statement:
|
|
191
192
|
|
|
192
193
|
```bash
|
|
@@ -137,21 +137,17 @@ issuing a redundant `job result` call.
|
|
|
137
137
|
|
|
138
138
|
## Authorized SQL mutations
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
statement at a time with `--force`. The
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`WRITE_OPERATION_REQUIRES_FORCE` is a safety signal, not authorization;
|
|
146
|
-
|
|
147
|
-
Leading `SET` options are part of the same authorized execution context.
|
|
148
|
-
Project-security and masking controls remain blocked, and forced mutations
|
|
149
|
-
accept only audited statement-local execution hints.
|
|
140
|
+
SQL not proven read-only requires explicit user authorization for its exact
|
|
141
|
+
statement, leading SET context, project, schema, target and effect. Submit one
|
|
142
|
+
statement at a time with `--force`. The client accepts authorized operation
|
|
143
|
+
names and SET keys without a positive allowlist; MaxCompute checks syntax,
|
|
144
|
+
parameter validity and permissions. SET keys do not change read/write classification.
|
|
145
|
+
`WRITE_OPERATION_REQUIRES_FORCE` is a safety signal, not authorization; obtain
|
|
146
|
+
approval before any new submission.
|
|
150
147
|
On successful query or submission responses, `data.safety.effective_hints`
|
|
151
148
|
reports the hints actually sent to MaxCompute. Audited hint values are shown;
|
|
152
149
|
an audited key whose value is outside its documented boolean, numeric, or enum
|
|
153
|
-
domain is still rendered as `<redacted>`. Values of unknown hints
|
|
154
|
-
read-only compatibility are also never echoed.
|
|
150
|
+
domain is still rendered as `<redacted>`. Values of unknown hints are never echoed, including for approved writes.
|
|
155
151
|
|
|
156
152
|
## data upload
|
|
157
153
|
|
|
@@ -22,39 +22,27 @@ Multiple SET statements can be chained:
|
|
|
22
22
|
|
|
23
23
|
For the meaning of each SET option (which switches enable which types / dialect features), see [maxcompute-select-guide.md](maxcompute-select-guide.md) §12.
|
|
24
24
|
|
|
25
|
-
A leading `SET`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
masking.
|
|
31
|
-
|
|
32
|
-
Audited write examples include `odps.sql.bigquery.compatible=true` for
|
|
33
|
-
BigQuery-compatible DDL identifiers and
|
|
34
|
-
`odps.sql.insert.acidtable.deduplicate.enable=true` for the explicitly requested
|
|
35
|
-
Delta-table INSERT deduplication behavior. Their use still belongs to the same
|
|
36
|
-
one-statement authorization; the hint is not separate permission to write.
|
|
25
|
+
A leading `SET` belongs to the same authorized execution context as the SQL.
|
|
26
|
+
Its syntax is parsed locally and its values are passed as hints to MaxCompute.
|
|
27
|
+
There is no client-side SET key allowlist or denylist. SET keys do not change
|
|
28
|
+
read/write classification. MaxCompute validates parameter keys, values and
|
|
29
|
+
caller permissions.
|
|
37
30
|
|
|
38
31
|
## SQL Write Gate
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
that statement. The `odps.sql.read.only` hint is not injected.
|
|
33
|
+
The client recognizes SELECT, SHOW, DESC, DESCRIBE, EXPLAIN and proven read-only
|
|
34
|
+
WITH queries. Every other operation requires explicit user authorization and
|
|
35
|
+
`--force`. New or administrative SQL is treated as a write; its operation name
|
|
36
|
+
is not a client-side reason to reject an authorized statement.
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
Verify the exact SQL, SET context, project, schema, target and effect, then
|
|
39
|
+
submit one executable statement at a time with `--force`. MaxCompute owns
|
|
40
|
+
syntax validation and permission checks. The `odps.sql.read.only` hint is not
|
|
41
|
+
injected. A gate error is not permission to infer a write or automatically
|
|
42
|
+
replay a suggested action.
|
|
50
43
|
|
|
51
|
-
The
|
|
52
|
-
|
|
53
|
-
maintenance or transfer statements. Permission, account, project, system,
|
|
54
|
-
resource, package, tenant, cluster, quota, and unknown administrative shapes
|
|
55
|
-
remain blocked; route those through a dedicated approved workflow.
|
|
56
|
-
|
|
57
|
-
The gate applies to SQL only. **`{{cli}} data upload` is not gated** because it goes through the Tunnel API (a write path by design) — see "Upload semantics" below.
|
|
44
|
+
The gate applies to SQL only. `{{cli}} data upload` is a separate Tunnel write
|
|
45
|
+
path and retains its authorization and overwrite boundaries.
|
|
58
46
|
|
|
59
47
|
## Result Fetch: `--max-rows` and the 10k Tunnel Threshold
|
|
60
48
|
|
|
@@ -20,9 +20,9 @@ Cross-referenced with SKILL.md §Core Principles (which lists the highest-priori
|
|
|
20
20
|
| 8 | Read `error.suggestion` before retrying a failed command | Same input → same error |
|
|
21
21
|
| 9 | Do not replace an existing authentication method without authorization | Keep a working configured provider. For a new public-cloud login, recommend OAuth first; use another approved method only when policy or runtime constraints require it. |
|
|
22
22
|
| 10 | Trust runtime help and actual command output over stale snippets | The CLI evolves; cached knowledge can be wrong |
|
|
23
|
-
| 11 | Execute
|
|
23
|
+
| 11 | Execute non-read SQL only when the user explicitly authorizes the exact statement, target, and effect; use `--force` one statement at a time | The gate is a safety aid, not authorization. Never infer or combine writes. (`data upload` is a separate write path and still needs authorization.) |
|
|
24
24
|
| 12 | Always check `agent_hints.warnings` even when `status=success` | Cache staleness, cost alerts, semantic gaps surface there |
|
|
25
|
-
| 13 | Treat leading `SET` hints as part of the SQL execution context |
|
|
25
|
+
| 13 | Treat leading `SET` hints as part of the SQL execution context | Include SET context in the exact SQL being authorized. SET keys do not change read/write classification; key/value validity and permissions are checked by MaxCompute. |
|
|
26
26
|
|
|
27
27
|
## Common Mistakes
|
|
28
28
|
|
|
@@ -67,7 +67,7 @@ When `status=failure`, inspect `error.code` and follow the recovery action. Alwa
|
|
|
67
67
|
| `SCHEMA_NOT_FOUND` | Schema does not exist | Check `error.context.did_you_mean` and `error.context.available_schemas`; list schemas with `meta list-schemas --json` |
|
|
68
68
|
| `TABLE_NOT_FOUND` | Table does not exist in the schema | Check `error.context.did_you_mean` and `error.context.available_tables`; search with `meta search <name> --json` |
|
|
69
69
|
| `COLUMN_NOT_FOUND` | Column reference does not exist | Check `error.context.available_columns`; run `meta describe <table> --json` |
|
|
70
|
-
| `WRITE_OPERATION_REQUIRES_FORCE` | SQL
|
|
70
|
+
| `WRITE_OPERATION_REQUIRES_FORCE` | SQL not proven read-only blocked by the write gate | This is not authorization. If the user explicitly requested that exact write, recheck the statement, target, and effect, then retry once with `--force`; otherwise stop. This does not apply to the separately authorized Tunnel `data upload` command. |
|
|
71
71
|
| `CSV_PARSE_ERROR` | A CSV cell could not be parsed against the column type during `data upload` | Read `error.context.line` (1-based row number, including header if present) and `error.context.column` (the column **name** as a string, not a position index) to find the bad cell; fix the source CSV and retry. File validation finishes before a Tunnel session is opened, so no rows are written. If `--create-partition` was explicitly requested, treat creation of an empty partition as a separate possible metadata side effect. |
|
|
72
72
|
| `UPLOAD_COMMIT_OUTCOME_UNKNOWN` | The process was interrupted or the backend failed after the Tunnel commit request began, so remote visibility cannot be proven | **Do not retry the upload.** Verify the target table or partition first; retrying append can duplicate rows and retrying overwrite can replace a successful result. This error is non-recoverable and exits with code 130. |
|
|
73
73
|
| `PERMISSION_DENIED` | The checked operation is not allowed for the effective identity/object | Verify identity, project/schema/object, then use `auth can-i` for that exact target |
|