maxc-cli 0.9.0__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.
Files changed (141) hide show
  1. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/PKG-INFO +1 -1
  2. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/__init__.py +1 -1
  3. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/app.py +12 -24
  4. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/odps.py +22 -5
  5. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/query.py +72 -19
  6. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/cli.py +7 -9
  7. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/exceptions.py +2 -3
  8. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/models.py +2 -3
  9. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/SKILL.md +8 -13
  10. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/command-patterns.md +4 -3
  11. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/json-output-format.md +8 -12
  12. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +16 -28
  13. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/red-lines.md +3 -3
  14. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/utils.py +43 -804
  15. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  16. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/SOURCES.txt +2 -0
  17. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_ai_native_contract_regressions.py +23 -5
  18. maxc_cli-0.9.3/tests/test_backend_query_cost.py +25 -0
  19. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_cli_mock.py +7 -2
  20. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_effective_hints_contract.py +20 -20
  21. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_phase1_improvements.py +3 -3
  22. maxc_cli-0.9.3/tests/test_query_namespace_settings.py +381 -0
  23. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_query_result_csv_fallback.py +62 -1
  24. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_setting_parser.py +59 -288
  25. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/MANIFEST.in +0 -0
  26. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/README.md +0 -0
  27. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/pyproject.toml +0 -0
  28. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/scripts/pyinstaller_entry.py +0 -0
  29. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/scripts/regression_test.py +0 -0
  30. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/setup.cfg +0 -0
  31. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/setup.py +0 -0
  32. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/__main__.py +0 -0
  33. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/_samples.py +0 -0
  34. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/agent_platforms.py +0 -0
  35. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/audit.py +0 -0
  36. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/auth_continuation.py +0 -0
  37. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/auth_providers.py +0 -0
  38. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/__init__.py +0 -0
  39. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/auth.py +0 -0
  40. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/catalog.py +0 -0
  41. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/data.py +0 -0
  42. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/job.py +0 -0
  43. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/mcp.py +0 -0
  44. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/meta.py +0 -0
  45. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/backend/semantic.py +0 -0
  46. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/cache.py +0 -0
  47. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/catalog_bootstrap.py +0 -0
  48. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/config.py +0 -0
  49. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/enterprise_tls.py +0 -0
  50. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/function_metadata.py +0 -0
  51. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/help_format.py +0 -0
  52. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/helpers.py +0 -0
  53. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/job_ids.py +0 -0
  54. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/masking.py +0 -0
  55. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/mcp_serve.py +0 -0
  56. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/oauth.py +0 -0
  57. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/odps_runtime.py +0 -0
  58. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/output.py +0 -0
  59. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/proxy_auth.py +0 -0
  60. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/semantic.py +0 -0
  61. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/semantic_management.py +0 -0
  62. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/setting_parser.py +0 -0
  63. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  64. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  65. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  66. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  67. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  68. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/registered-functions.md +0 -0
  69. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  70. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/setup-install.md +0 -0
  71. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  72. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  73. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  74. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/state_permissions.py +0 -0
  75. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli/store.py +0 -0
  76. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  77. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  78. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/requires.txt +0 -0
  79. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/src/maxc_cli.egg-info/top_level.txt +0 -0
  80. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_agent_hints_and_cli.py +0 -0
  81. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_agent_platforms.py +0 -0
  82. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_agent_skill_commands.py +0 -0
  83. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_agent_skill_commands_context.py +0 -0
  84. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_auth_logout.py +0 -0
  85. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_backend_auth.py +0 -0
  86. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_backend_data.py +0 -0
  87. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_backend_data_serialization.py +0 -0
  88. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_backend_mcp.py +0 -0
  89. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_backend_meta.py +0 -0
  90. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_build_release_archive_compat.py +0 -0
  91. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_build_release_script.py +0 -0
  92. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_cache.py +0 -0
  93. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_catalog.py +0 -0
  94. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_catalog_bootstrap.py +0 -0
  95. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_cli_arg_validation.py +0 -0
  96. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  97. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_compat.py +0 -0
  98. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_config_atomic_write.py +0 -0
  99. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_connection_bootstrap.py +0 -0
  100. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_e2e_smoke.py +0 -0
  101. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_enterprise_tls.py +0 -0
  102. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_envelope_shape.py +0 -0
  103. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_error_self_correction.py +0 -0
  104. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_error_translation.py +0 -0
  105. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_exit_codes.py +0 -0
  106. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_external_auth.py +0 -0
  107. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_flag_hoist.py +0 -0
  108. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_help_format.py +0 -0
  109. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_help_version_e2e.py +0 -0
  110. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_helpers.py +0 -0
  111. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_helpers_csv.py +0 -0
  112. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_installer_contracts.py +0 -0
  113. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_integration.py +0 -0
  114. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_integration_real.py +0 -0
  115. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_job_improvements.py +0 -0
  116. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_job_inspection.py +0 -0
  117. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_job_store_durability.py +0 -0
  118. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_kb_commands.py +0 -0
  119. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_manifest_runtime_contract.py +0 -0
  120. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_masking.py +0 -0
  121. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_mcp_serve.py +0 -0
  122. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_meta_schema_and_partition_cols.py +0 -0
  123. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_oauth.py +0 -0
  124. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_odps_runtime.py +0 -0
  125. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_output_action_safety.py +0 -0
  126. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_output_format_contract.py +0 -0
  127. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_packaging_metadata.py +0 -0
  128. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_proxy_auth.py +0 -0
  129. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_pyinstaller_bundle.py +0 -0
  130. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_python39_compat.py +0 -0
  131. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_query_auto_promote.py +0 -0
  132. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_registered_functions.py +0 -0
  133. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_semantic_management.py +0 -0
  134. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_semantic_scope.py +0 -0
  135. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_semantic_transport.py +0 -0
  136. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_skill_cli_consistency.py +0 -0
  137. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_skill_eval.py +0 -0
  138. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_skill_renderer.py +0 -0
  139. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_startup_imports.py +0 -0
  140. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_state_permissions.py +0 -0
  141. {maxc_cli-0.9.0 → maxc_cli-0.9.3}/tests/test_state_portability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.9.0
3
+ Version: 0.9.3
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.9.0"
5
+ __version__ = "0.9.3"
@@ -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
- write_operations = known_write_operations(sql)
776
- if write_operations and ((retry_on or []) or max_retries):
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
- if sql.startswith("@natural"):
6943
- raise FeatureUnavailableError(
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
- write_operations = known_write_operations(result.sql_executed)
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 write_operations:
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 write_operations:
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
- write_operations = known_write_operations(sql)
7191
- if not write_operations:
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
- resultless_statement = statement_operation is not None or (
128
- not sql.strip() and getattr(instance, "_is_select", None) is False
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 = self._safe_task_results(instance)
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. The public path is a positive allowlist: unknown dialect
121
- # extensions and unreviewed mutation hints are blocked before the service
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
- Recognized data-plane write operations (INSERT, CREATE TABLE, DROP
206
- TABLE, etc.) are blocked client-side unless *force* is True. Unknown,
207
- procedural, permission, session-control, and administrative SQL remain
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 recognized data-plane DDL/DML statement.
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 = self.client.execute_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 = self.client.execute_sql_interactive(
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 = self.client.run_sql(
279
- actual_sql, project=project, hints=hints, **priority_kwargs,
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 = self.client.execute_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:
@@ -343,7 +387,9 @@ class QueryMixin:
343
387
  **build_query_outline(actual_sql),
344
388
  "project": project,
345
389
  "cost_model": "maxcompute_native_sql_cost",
346
- "estimated_input_size_bytes": int(sql_cost.input_size or 0),
390
+ "estimated_input_size_bytes": (
391
+ int(sql_cost.input_size) if sql_cost.input_size is not None else None
392
+ ),
347
393
  "task_cost_cpu": None,
348
394
  "task_cost_memory": None,
349
395
  "sql_complexity": sql_cost.complexity,
@@ -378,10 +424,12 @@ class QueryMixin:
378
424
  plan_text: str | None = None
379
425
  plan_warning: str | None = None
380
426
  try:
381
- instance = self.client.execute_sql(
427
+ instance = _invoke_sql_with_hints(
428
+ self.client.execute_sql,
382
429
  f"EXPLAIN {actual_sql}",
383
430
  project=project,
384
431
  hints=explain_hints,
432
+ schema_aware=True,
385
433
  **priority_kwargs,
386
434
  )
387
435
  try:
@@ -397,7 +445,8 @@ class QueryMixin:
397
445
 
398
446
  # Cost estimate alongside the plan
399
447
  try:
400
- sql_cost = self.client.execute_sql_cost(
448
+ sql_cost = _invoke_sql_with_hints(
449
+ self.client.execute_sql_cost,
401
450
  actual_sql, project=project, hints=hints,
402
451
  )
403
452
  except Exception:
@@ -448,7 +497,7 @@ class QueryMixin:
448
497
  sql: SQL query, optionally prefixed with SET statements.
449
498
  project: ODPS project name.
450
499
  idempotency_key: Optional unique ID for deduplication.
451
- force: If True, allow one recognized data-plane DDL/DML statement
500
+ force: If True, allow one explicitly authorized non-read statement
452
501
  after explicit authorization.
453
502
 
454
503
  Returns:
@@ -471,15 +520,19 @@ class QueryMixin:
471
520
  interactive_kwargs["project"] = project
472
521
  interactive_kwargs["use_mcqa_v2"] = True
473
522
  interactive_kwargs["quota_name"] = getattr(execution_settings, "quota_name", None)
474
- instance = self.client.run_sql_interactive(
523
+ instance = _invoke_sql_with_hints(
524
+ self.client.run_sql_interactive,
475
525
  actual_sql,
526
+ schema_aware=getattr(execution_settings, "version", "v2") == "v2",
476
527
  **interactive_kwargs,
477
528
  )
478
529
  else:
479
- instance = self.client.run_sql(
530
+ instance = _invoke_sql_with_hints(
531
+ self.client.run_sql,
480
532
  actual_sql,
481
533
  project=project,
482
534
  hints=hints,
535
+ schema_aware=True,
483
536
  **idem_kwargs,
484
537
  **priority_kwargs,
485
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
- "DDL/DML requires explicit --force.\n"
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 data-plane DDL/DML statement (never inferred from a read request)",
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 data-plane DDL/DML statement (never inferred from a read request)",
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 DDL/DML statement explicitly authorized by the user."
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
- "The SQL shape is neither proven read-only nor recognized "
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 data-plane DDL/DML statement; verify project, schema, target, and effect.",
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 data-plane DDL/DML statement; verify project, schema, target, and effect.",
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 DDL/DML, verify its project, schema, target, and effect.",
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
- "--force does not enable unknown, procedural, permission, session-control, or administrative SQL shapes.",
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, known_write_operations, statement_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 = bool(write_operations)
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. For DDL/DML, require an explicit user request and verify the exact statement,
181
- project, schema, target, and effect. Submit one statement at a time with
182
- `--force`; never infer a write from a read request, combine it with another
183
- statement, or replay a suggested write action automatically. The CLI
184
- positive allowlist accepts recognized data-plane mutations; permission,
185
- account, project, system, resource, package, and unknown administrative SQL
186
- remain blocked and require a dedicated approved workflow. A leading `SET`
187
- is part of the authorized execution context, not a second authorization
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 parameters are blocked; a forced mutation accepts
179
- only audited statement-local execution hints.
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 DDL/DML statement the user has explicitly requested, first verify
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
- DDL/DML is allowed only when the user explicitly requests the exact mutation.
141
- Verify the statement, project, schema, target, and effect, then submit one
142
- statement at a time with `--force`. The positive allowlist accepts recognized
143
- data-plane mutations; unknown, procedural, permission, session-control, and
144
- administrative SQL remain blocked even with `--force`.
145
- `WRITE_OPERATION_REQUIRES_FORCE` is a safety signal, not authorization; never
146
- infer the write or automatically replay a suggested mutation action.
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 retained for
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` is execution context, not authorization. Project-security,
26
- access-control, and masking parameters are blocked even for `SELECT`. For a
27
- forced DDL/DML statement, the CLI additionally accepts only audited
28
- statement-local SQL/runtime hints; an unknown hint fails closed. Never use a
29
- `SET` hint to weaken permissions, project protection, label security, or data
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
- `maxc-cli` checks every executable operation, including operations nested in
41
- script control flow, **client-side, before submission**. Without `--force`, it
42
- submits only SQL shapes proven read-only. With `--force`, it still accepts
43
- exactly one executable statement; audited leading `SET` hints may configure
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
- The gate is a safety aid, not authorization. For DDL/DML, require an explicit
47
- user request, verify the exact statement, project, schema, target, and effect,
48
- then submit one statement at a time with `--force`. Never infer a write from a
49
- read request or combine it with another statement.
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 `--force` path uses a positive data-plane allowlist. It accepts recognized
52
- DML and DDL for tables, views, functions, and schemas plus documented table
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 DDL/DML 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.) |
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 | Never use project-security, access-control, or masking parameters through `query`; forced writes accept only audited statement-local hints. |
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 DDL/DML 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. |
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 |