maxc-cli 0.9.3__tar.gz → 0.9.4__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.3 → maxc_cli-0.9.4}/PKG-INFO +1 -1
  2. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/__init__.py +1 -1
  3. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/odps.py +40 -3
  4. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  5. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_query_result_csv_fallback.py +110 -0
  6. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/MANIFEST.in +0 -0
  7. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/README.md +0 -0
  8. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/pyproject.toml +0 -0
  9. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/scripts/pyinstaller_entry.py +0 -0
  10. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/scripts/regression_test.py +0 -0
  11. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/setup.cfg +0 -0
  12. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/setup.py +0 -0
  13. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/__main__.py +0 -0
  14. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/_samples.py +0 -0
  15. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/agent_platforms.py +0 -0
  16. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/app.py +0 -0
  17. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/audit.py +0 -0
  18. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/auth_continuation.py +0 -0
  19. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/auth_providers.py +0 -0
  20. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/__init__.py +0 -0
  21. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/auth.py +0 -0
  22. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/catalog.py +0 -0
  23. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/data.py +0 -0
  24. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/job.py +0 -0
  25. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/mcp.py +0 -0
  26. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/meta.py +0 -0
  27. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/query.py +0 -0
  28. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/backend/semantic.py +0 -0
  29. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/cache.py +0 -0
  30. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/catalog_bootstrap.py +0 -0
  31. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/cli.py +0 -0
  32. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/config.py +0 -0
  33. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/enterprise_tls.py +0 -0
  34. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/exceptions.py +0 -0
  35. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/function_metadata.py +0 -0
  36. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/help_format.py +0 -0
  37. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/helpers.py +0 -0
  38. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/job_ids.py +0 -0
  39. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/masking.py +0 -0
  40. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/mcp_serve.py +0 -0
  41. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/models.py +0 -0
  42. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/oauth.py +0 -0
  43. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/odps_runtime.py +0 -0
  44. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/output.py +0 -0
  45. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/proxy_auth.py +0 -0
  46. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/semantic.py +0 -0
  47. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/semantic_management.py +0 -0
  48. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/setting_parser.py +0 -0
  49. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/SKILL.md +0 -0
  50. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  51. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  52. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  53. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  54. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  55. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  56. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  57. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  58. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/red-lines.md +0 -0
  59. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/registered-functions.md +0 -0
  60. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  61. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/setup-install.md +0 -0
  62. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  63. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  64. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  65. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/state_permissions.py +0 -0
  66. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/store.py +0 -0
  67. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli/utils.py +0 -0
  68. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
  69. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  70. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  71. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/requires.txt +0 -0
  72. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/src/maxc_cli.egg-info/top_level.txt +0 -0
  73. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_agent_hints_and_cli.py +0 -0
  74. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_agent_platforms.py +0 -0
  75. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_agent_skill_commands.py +0 -0
  76. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_agent_skill_commands_context.py +0 -0
  77. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_ai_native_contract_regressions.py +0 -0
  78. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_auth_logout.py +0 -0
  79. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_auth.py +0 -0
  80. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_data.py +0 -0
  81. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_data_serialization.py +0 -0
  82. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_mcp.py +0 -0
  83. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_meta.py +0 -0
  84. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_backend_query_cost.py +0 -0
  85. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_build_release_archive_compat.py +0 -0
  86. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_build_release_script.py +0 -0
  87. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_cache.py +0 -0
  88. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_catalog.py +0 -0
  89. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_catalog_bootstrap.py +0 -0
  90. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_cli_arg_validation.py +0 -0
  91. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_cli_mock.py +0 -0
  92. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  93. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_compat.py +0 -0
  94. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_config_atomic_write.py +0 -0
  95. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_connection_bootstrap.py +0 -0
  96. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_e2e_smoke.py +0 -0
  97. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_effective_hints_contract.py +0 -0
  98. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_enterprise_tls.py +0 -0
  99. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_envelope_shape.py +0 -0
  100. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_error_self_correction.py +0 -0
  101. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_error_translation.py +0 -0
  102. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_exit_codes.py +0 -0
  103. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_external_auth.py +0 -0
  104. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_flag_hoist.py +0 -0
  105. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_help_format.py +0 -0
  106. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_help_version_e2e.py +0 -0
  107. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_helpers.py +0 -0
  108. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_helpers_csv.py +0 -0
  109. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_installer_contracts.py +0 -0
  110. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_integration.py +0 -0
  111. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_integration_real.py +0 -0
  112. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_job_improvements.py +0 -0
  113. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_job_inspection.py +0 -0
  114. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_job_store_durability.py +0 -0
  115. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_kb_commands.py +0 -0
  116. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_manifest_runtime_contract.py +0 -0
  117. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_masking.py +0 -0
  118. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_mcp_serve.py +0 -0
  119. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_meta_schema_and_partition_cols.py +0 -0
  120. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_oauth.py +0 -0
  121. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_odps_runtime.py +0 -0
  122. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_output_action_safety.py +0 -0
  123. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_output_format_contract.py +0 -0
  124. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_packaging_metadata.py +0 -0
  125. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_phase1_improvements.py +0 -0
  126. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_proxy_auth.py +0 -0
  127. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_pyinstaller_bundle.py +0 -0
  128. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_python39_compat.py +0 -0
  129. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_query_auto_promote.py +0 -0
  130. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_query_namespace_settings.py +0 -0
  131. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_registered_functions.py +0 -0
  132. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_semantic_management.py +0 -0
  133. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_semantic_scope.py +0 -0
  134. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_semantic_transport.py +0 -0
  135. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_setting_parser.py +0 -0
  136. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_skill_cli_consistency.py +0 -0
  137. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_skill_eval.py +0 -0
  138. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_skill_renderer.py +0 -0
  139. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_startup_imports.py +0 -0
  140. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/tests/test_state_permissions.py +0 -0
  141. {maxc_cli-0.9.3 → maxc_cli-0.9.4}/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.3
3
+ Version: 0.9.4
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.3"
5
+ __version__ = "0.9.4"
@@ -13,7 +13,7 @@ from ..helpers import (
13
13
  translate_odps_error,
14
14
  )
15
15
  from ..models import QueryResult
16
- from ..utils import detect_operation, extract_table_names
16
+ from ..utils import detect_operation, extract_table_names, sql_keyword_tokens, sql_statements
17
17
  from .auth import AuthMixin
18
18
  from .catalog import CatalogMixin
19
19
  from .data import DataMixin
@@ -75,6 +75,26 @@ def _raw_task_result_rows(task_results: 'dict[str, Any]') -> 'list[dict[str, Any
75
75
  ]
76
76
 
77
77
 
78
+ def _is_show_partitions(sql: 'str') -> 'bool':
79
+ statements = sql_statements(sql)
80
+ return len(statements) == 1 and sql_keyword_tokens(statements[0])[:2] == ["SHOW", "PARTITIONS"]
81
+
82
+
83
+ def _show_partition_rows(task_results: 'Any') -> 'list[dict[str, str]]':
84
+ """SHOW PARTITIONS emits headerless lines, not a CSV table with a header."""
85
+ if not isinstance(task_results, dict):
86
+ raise ValueError("SHOW PARTITIONS returned invalid task results")
87
+ rows = []
88
+ for text in task_results.values():
89
+ if text is None:
90
+ continue
91
+ if not isinstance(text, str):
92
+ raise ValueError("SHOW PARTITIONS returned invalid task result text")
93
+ # Split only protocol line endings; Unicode separators may be values.
94
+ rows.extend({"partition": line.removesuffix("\r")} for line in text.split("\n") if line.removesuffix("\r"))
95
+ return rows
96
+
97
+
78
98
  class OdpsBackend(
79
99
  JobMixin, # JobMixin extends QueryMixin
80
100
  CatalogMixin,
@@ -123,11 +143,12 @@ class OdpsBackend(
123
143
  ) -> 'QueryResult':
124
144
  """Convert ODPS instance to QueryResult."""
125
145
  fallback_warnings: list[str] = []
146
+ show_partitions = _is_show_partitions(sql)
126
147
  statement_operation = _write_operation(sql)
127
148
  instance_is_select = getattr(instance, "_is_select", None)
128
149
  confirmed_task_results = None
129
150
  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:
151
+ if not show_partitions and not resultless_statement and instance_is_select is False:
131
152
  try:
132
153
  task_results = instance.get_task_results()
133
154
  except Exception:
@@ -140,7 +161,23 @@ class OdpsBackend(
140
161
  resultless_statement = not any(str(value or "").strip() for value in task_results.values())
141
162
  if resultless_statement and sql.strip():
142
163
  statement_operation = detect_operation(sql)
143
- if resultless_statement:
164
+ if show_partitions:
165
+ try:
166
+ # The generic CSV reader consumes the first partition as a
167
+ # header and raises on an empty body. Read this known textual
168
+ # result directly, retaining lookup failures as real errors.
169
+ all_rows = _show_partition_rows(instance.get_task_results())
170
+ except Exception as exc:
171
+ error = translate_odps_error(exc)
172
+ error.instance_id = instance.id
173
+ error.logview = self._safe_logview(instance)
174
+ raise error from exc
175
+ schema = [{"name": "partition", "type": "string", "comment": ""}]
176
+ rows = all_rows[offset : offset + max_rows]
177
+ total_rows = len(all_rows)
178
+ degraded_reason = None
179
+ resultless_statement = False
180
+ elif resultless_statement:
144
181
  # Successful write statements have no tabular result. In
145
182
  # particular, PyODPS's DDL fallback reader may try to iterate a
146
183
  # missing task_results value and raise after the server job has
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.9.3
3
+ Version: 0.9.4
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
@@ -688,3 +688,113 @@ def test_select_marker_preserves_results_for_a_known_write_operation() -> None:
688
688
  )
689
689
  assert result.rows == [{"value": "1"}]
690
690
  assert "result_kind" not in result.extra_metadata
691
+
692
+
693
+ @pytest.mark.parametrize("marker", [None, False, True])
694
+ @pytest.mark.parametrize("partitions", [[], ["ds=20261021"], ["ds=20261021", "ds=20261022"]])
695
+ def test_show_partitions_preserves_headerless_rows(marker, partitions):
696
+ raw = "\n".join(partitions) + ("\n" if partitions else "")
697
+ # Use the real SDK reader which caused the regression without making its
698
+ # current empty/headerless parsing bugs part of MaxC's permanent contract.
699
+ instance = _FakeInstance(reader=CsvRecordReader(schema=None, stream=raw), task_results={"SQLTask": raw})
700
+ instance._is_select = marker
701
+ result = _StubBackend()._instance_to_query_result(instance, project="test_project", max_rows=100, sql="SHOW PARTITIONS t", elapsed_ms=1)
702
+ assert result.schema == [{"name": "partition", "type": "string", "comment": ""}]
703
+ assert result.rows == [{"partition": p} for p in partitions]
704
+ assert result.total_rows == result.returned_rows == len(partitions)
705
+ assert result.has_more is False
706
+ assert result.extra_metadata.get("result_kind") != "statement"
707
+
708
+
709
+ @pytest.mark.parametrize("offset, expected, has_more", [(0, ["ds=1"], True), (1, ["ds=2"], False), (2, [], False)])
710
+ def test_show_partitions_pages_each_partition(offset, expected, has_more):
711
+ result = _run(None, sql="SET odps.sql.type.vector.enable=true; /* scope */ SHOW\nPARTITIONS t", task_results={"SQLTask": "ds=1\r\nds=2\r\n"}, offset=offset, max_rows=1)
712
+ assert result.rows == [{"partition": p} for p in expected]
713
+ assert result.total_rows == 2
714
+ assert result.returned_rows == len(expected)
715
+ assert result.has_more is has_more
716
+
717
+
718
+ @pytest.mark.parametrize("task_results", [{}, {"SQLTask": None}, {"SQLTask": ""}])
719
+ def test_show_partitions_successful_empty_task_result_has_schema(task_results):
720
+ result = _run(None, sql="SHOW PARTITIONS t", task_results=task_results)
721
+ assert result.schema[0]["name"] == "partition"
722
+ assert result.rows == []
723
+ assert result.total_rows == 0
724
+
725
+
726
+ def test_show_partitions_lookup_failure_is_not_empty_success():
727
+ with pytest.raises(Exception, match="get_task_results not stubbed"):
728
+ _run(None, sql="SHOW PARTITIONS t", raise_on_task_results=True)
729
+
730
+
731
+ @pytest.mark.parametrize("task_results", [None, [], {"SQLTask": object()}])
732
+ def test_show_partitions_invalid_task_results_are_not_empty_success(task_results):
733
+ instance = _FakeInstance(reader=None)
734
+ instance.get_task_results = lambda: task_results
735
+ with pytest.raises(Exception, match="invalid task result"):
736
+ _StubBackend()._instance_to_query_result(instance, project="test_project", max_rows=100, sql="SHOW PARTITIONS t", elapsed_ms=1)
737
+
738
+
739
+ def test_partition_like_select_header_stays_a_real_csv_header():
740
+ result = _run(CsvRecordReader(schema=None, stream="ds=20261021\nvalue\n"), sql="SELECT value AS `ds=20261021` FROM t")
741
+ assert result.rows == [{"ds=20261021": "value"}]
742
+
743
+
744
+ def test_show_partitions_keeps_unicode_separators_inside_partition_value():
745
+ partition = "ds=before\u2028after"
746
+ result = _run(None, sql="SHOW PARTITIONS t", task_results={"SQLTask": partition + "\n"})
747
+ assert result.rows == [{"partition": partition}]
748
+
749
+
750
+ @pytest.mark.parametrize("partitions", [[], ["ds=1"], ["ds=1", "ds=2"]])
751
+ def test_completed_job_fetch_recovers_show_partitions_sql(partitions):
752
+ from types import SimpleNamespace
753
+
754
+ class _CompletedInstance(_FakeInstance):
755
+ def get_sql_query(self):
756
+ return "SHOW PARTITIONS t"
757
+
758
+ instance = _CompletedInstance(
759
+ reader=None, task_results={"SQLTask": "\n".join(partitions)},
760
+ )
761
+
762
+ class _JobBackend(_StubBackend):
763
+ fetch_job_result = JobMixin.fetch_job_result
764
+ _safe_sql = JobMixin._safe_sql
765
+
766
+ def _get_instance(self, job_id, **kwargs):
767
+ assert job_id == instance.id
768
+ return instance
769
+
770
+ def _instance_to_job_info(self, instance, **kwargs):
771
+ return SimpleNamespace(status="success")
772
+
773
+ def _rehydrate_sqlrt_result_instance(self, instance):
774
+ return instance
775
+
776
+ result = _JobBackend().fetch_job_result(instance.id, max_rows=1, offset=0)
777
+ assert result.sql_executed == "SHOW PARTITIONS t"
778
+ assert result.schema[0]["name"] == "partition"
779
+ assert result.rows == [{"partition": p} for p in partitions[:1]]
780
+ assert result.total_rows == len(partitions)
781
+ assert result.has_more is (len(partitions) > 1)
782
+
783
+
784
+ def test_show_partitions_permission_error_preserves_original_job():
785
+ from odps.errors import NoPermission
786
+
787
+ from maxc_cli.exceptions import PermissionDeniedError
788
+
789
+ class _DeniedInstance(_FakeInstance):
790
+ def get_task_results(self):
791
+ raise NoPermission("Access denied", request_id="show-partitions-request")
792
+
793
+ with pytest.raises(PermissionDeniedError) as caught:
794
+ _StubBackend()._instance_to_query_result(
795
+ _DeniedInstance(reader=None), project="test_project", max_rows=100,
796
+ sql="SHOW PARTITIONS t", elapsed_ms=1,
797
+ )
798
+ assert caught.value.error_code == "PERMISSION_DENIED"
799
+ assert caught.value.instance_id == _DeniedInstance.id
800
+ assert "show-partitions-request" in caught.value.suggestion
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