maxc-cli 0.3.1__tar.gz → 0.3.2__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.3.1 → maxc_cli-0.3.2}/PKG-INFO +1 -1
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/setup.py +1 -1
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/__init__.py +1 -1
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/app.py +1 -1
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/query.py +49 -12
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/PKG-INFO +1 -1
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_external_auth.py +54 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_phase1_improvements.py +5 -5
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_setting_parser.py +40 -4
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/MANIFEST.in +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/README.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/pyproject.toml +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/scripts/pyinstaller_entry.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/scripts/regression_test.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/setup.cfg +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/__main__.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/_samples.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/agent_platforms.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/audit.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/auth_providers.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/__init__.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/auth.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/catalog.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/data.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/job.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/meta.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/backend/odps.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/cache.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/cli.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/config.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/exceptions.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/help_format.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/helpers.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/masking.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/models.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/output.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/setting_parser.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/SKILL.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/agents/openai.yaml +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/command-patterns.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/json-output-format.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/migrate-from-odpscmd.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/partition-guide.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/red-lines.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/setup-install.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/store.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli/utils.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/SOURCES.txt +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/entry_points.txt +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/requires.txt +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/src/maxc_cli.egg-info/top_level.txt +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_hints_and_cli.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_platforms.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_skill_commands.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_agent_skill_commands_context.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_data.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_data_serialization.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_backend_meta.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_build_release_script.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cache.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_catalog.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_catalog_bootstrap.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_arg_validation.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_mock.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_cli_query_parse_and_sanitize.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_compat.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_e2e_smoke.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_envelope_shape.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_error_self_correction.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_error_translation.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_exit_codes.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_flag_hoist.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_help_format.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_helpers.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_helpers_csv.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_integration.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_integration_real.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_job_improvements.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_masking.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_meta_schema_and_partition_cols.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_pyinstaller_bundle.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_query_auto_promote.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_query_result_csv_fallback.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_skill_cli_consistency.py +0 -0
- {maxc_cli-0.3.1 → maxc_cli-0.3.2}/tests/test_skill_renderer.py +0 -0
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="maxc-cli",
|
|
12
|
-
version="0.3.
|
|
12
|
+
version="0.3.2",
|
|
13
13
|
description="Agent-native MaxCompute CLI for external coding agents",
|
|
14
14
|
long_description=README.read_text(encoding="utf-8"),
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -21,6 +21,7 @@ from .backend import OdpsBackend
|
|
|
21
21
|
from .cache import LocalCache
|
|
22
22
|
from .config import (
|
|
23
23
|
AuthConfig,
|
|
24
|
+
ExternalAuthConfig,
|
|
24
25
|
TableDefinition,
|
|
25
26
|
default_global_config_path,
|
|
26
27
|
load_config,
|
|
@@ -2740,7 +2741,6 @@ class MaxCApp:
|
|
|
2740
2741
|
JSON to stdout. See :class:`ExternalCredentialProvider` for the
|
|
2741
2742
|
expected JSON format.
|
|
2742
2743
|
"""
|
|
2743
|
-
from .auth_providers import ExternalAuthConfig
|
|
2744
2744
|
target_path = target_config_path or default_global_config_path()
|
|
2745
2745
|
existing_payload = load_config_mapping(target_path) if target_path.exists() else {}
|
|
2746
2746
|
existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
|
|
@@ -46,15 +46,20 @@ def _count_statements(sql: 'str') -> 'int':
|
|
|
46
46
|
return sum(1 for part in cleaned.split(";") if part.strip())
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def _parse_sql_with_hints(
|
|
49
|
+
def _parse_sql_with_hints(
|
|
50
|
+
sql: 'str', *, force: 'bool' = False,
|
|
51
|
+
) -> 'tuple[str, dict[str, str], int | None]':
|
|
50
52
|
"""Extract SET statements from *sql* and enforce client-side read-only mode.
|
|
51
53
|
|
|
52
|
-
Returns ``(remaining_sql, merged_hints)
|
|
53
|
-
contains
|
|
54
|
-
|
|
54
|
+
Returns ``(remaining_sql, merged_hints, priority)``. ``merged_hints``
|
|
55
|
+
contains user-supplied SET values minus ``odps.instance.priority``,
|
|
56
|
+
which is lifted out into ``priority`` so callers can pass it as the
|
|
57
|
+
``priority=`` kwarg of ``run_sql`` / ``execute_sql``.
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
Write operations (INSERT, CREATE, DROP, etc.) are blocked unless
|
|
60
|
+
*force* is ``True``. Empty SQL raises ``ValidationError``.
|
|
61
|
+
Multi-statement SQL automatically receives
|
|
62
|
+
``odps.sql.submit.mode=script`` unless the user already set it.
|
|
58
63
|
"""
|
|
59
64
|
parsed = SettingParser.parse(sql)
|
|
60
65
|
if parsed.errors:
|
|
@@ -87,7 +92,34 @@ def _parse_sql_with_hints(sql: 'str', *, force: 'bool' = False) -> 'tuple[str, d
|
|
|
87
92
|
if _count_statements(remaining) >= 2:
|
|
88
93
|
hints.setdefault("odps.sql.submit.mode", "script")
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
# odps.instance.priority is not a SQL hint — it's a top-level kwarg on
|
|
96
|
+
# run_sql/execute_sql. Lift it out so the caller can thread it through.
|
|
97
|
+
priority = _pop_priority(hints)
|
|
98
|
+
|
|
99
|
+
return remaining, hints, priority
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _pop_priority(hints: 'dict[str, str]') -> 'int | None':
|
|
103
|
+
"""Pop ``odps.instance.priority`` from *hints* and parse as int.
|
|
104
|
+
|
|
105
|
+
Match is case-insensitive on the key. Returns ``None`` if absent.
|
|
106
|
+
Raises ``ValidationError`` if the value isn't an integer.
|
|
107
|
+
"""
|
|
108
|
+
matched_key: str | None = None
|
|
109
|
+
for k in hints:
|
|
110
|
+
if k.lower() == "odps.instance.priority":
|
|
111
|
+
matched_key = k
|
|
112
|
+
break
|
|
113
|
+
if matched_key is None:
|
|
114
|
+
return None
|
|
115
|
+
raw = hints.pop(matched_key)
|
|
116
|
+
try:
|
|
117
|
+
return int(raw)
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
f"Invalid odps.instance.priority value {raw!r}: must be an integer.",
|
|
121
|
+
suggestion="Use SET odps.instance.priority=N; where N is an integer.",
|
|
122
|
+
) from None
|
|
91
123
|
|
|
92
124
|
|
|
93
125
|
class QueryMixin:
|
|
@@ -125,7 +157,8 @@ class QueryMixin:
|
|
|
125
157
|
ValidationError: If SET syntax is invalid.
|
|
126
158
|
BackendConnectionError: If ODPS connection fails.
|
|
127
159
|
"""
|
|
128
|
-
actual_sql, hints = _parse_sql_with_hints(sql, force=force)
|
|
160
|
+
actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
|
|
161
|
+
priority_kwargs = {"priority": priority} if priority is not None else {}
|
|
129
162
|
|
|
130
163
|
started_at = now_utc_iso()
|
|
131
164
|
started_monotonic = monotonic()
|
|
@@ -162,7 +195,7 @@ class QueryMixin:
|
|
|
162
195
|
|
|
163
196
|
try:
|
|
164
197
|
instance = self.client.run_sql(
|
|
165
|
-
actual_sql, project=project, hints=hints,
|
|
198
|
+
actual_sql, project=project, hints=hints, **priority_kwargs,
|
|
166
199
|
)
|
|
167
200
|
except Exception as exc:
|
|
168
201
|
raise translate_odps_error(exc) from exc
|
|
@@ -204,7 +237,7 @@ class QueryMixin:
|
|
|
204
237
|
Returns:
|
|
205
238
|
Dict with estimated_input_size_bytes, sql_complexity, sql_udf_num, etc.
|
|
206
239
|
"""
|
|
207
|
-
actual_sql, hints = _parse_sql_with_hints(sql, force=force)
|
|
240
|
+
actual_sql, hints, _priority = _parse_sql_with_hints(sql, force=force)
|
|
208
241
|
started_monotonic = monotonic()
|
|
209
242
|
try:
|
|
210
243
|
sql_cost = self.client.execute_sql_cost(
|
|
@@ -240,7 +273,8 @@ class QueryMixin:
|
|
|
240
273
|
Returns:
|
|
241
274
|
Dict with query outline, cost metadata, and ``execution_plan`` text.
|
|
242
275
|
"""
|
|
243
|
-
actual_sql, hints = _parse_sql_with_hints(sql, force=force)
|
|
276
|
+
actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
|
|
277
|
+
priority_kwargs = {"priority": priority} if priority is not None else {}
|
|
244
278
|
# script-mode auto-hint doesn't apply to EXPLAIN itself; remove if present.
|
|
245
279
|
explain_hints = {k: v for k, v in hints.items() if k != "odps.sql.submit.mode"}
|
|
246
280
|
started_monotonic = monotonic()
|
|
@@ -252,6 +286,7 @@ class QueryMixin:
|
|
|
252
286
|
f"EXPLAIN {actual_sql}",
|
|
253
287
|
project=project,
|
|
254
288
|
hints=explain_hints,
|
|
289
|
+
**priority_kwargs,
|
|
255
290
|
)
|
|
256
291
|
try:
|
|
257
292
|
results = instance.get_task_results()
|
|
@@ -319,7 +354,8 @@ class QueryMixin:
|
|
|
319
354
|
"""
|
|
320
355
|
from ..models import JobInfo
|
|
321
356
|
|
|
322
|
-
actual_sql, hints = _parse_sql_with_hints(sql, force=force)
|
|
357
|
+
actual_sql, hints, priority = _parse_sql_with_hints(sql, force=force)
|
|
358
|
+
priority_kwargs = {"priority": priority} if priority is not None else {}
|
|
323
359
|
|
|
324
360
|
try:
|
|
325
361
|
instance = self.client.run_sql(
|
|
@@ -327,6 +363,7 @@ class QueryMixin:
|
|
|
327
363
|
project=project,
|
|
328
364
|
hints=hints,
|
|
329
365
|
unique_identifier_id=idempotency_key,
|
|
366
|
+
**priority_kwargs,
|
|
330
367
|
)
|
|
331
368
|
except Exception as exc:
|
|
332
369
|
raise translate_odps_error(exc) from exc
|
|
@@ -790,3 +790,57 @@ class TestAuthSeemsConfiguredExternal:
|
|
|
790
790
|
self._clear_odps_env(monkeypatch)
|
|
791
791
|
config = _minimal_config()
|
|
792
792
|
assert _auth_seems_configured(self._stub_app(config)) is False
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
# ============================================================
|
|
796
|
+
# MaxCApp.auth_login_external — end-to-end regression
|
|
797
|
+
# ============================================================
|
|
798
|
+
|
|
799
|
+
class TestAuthLoginExternalAppMethod:
|
|
800
|
+
"""Regression: a stale lazy import (``from .auth_providers import
|
|
801
|
+
ExternalAuthConfig``) in ``MaxCApp.auth_login_external`` raised
|
|
802
|
+
``ImportError`` at runtime after a ruff F401 sweep removed the
|
|
803
|
+
transitive re-export from ``auth_providers``. The dataclass lives in
|
|
804
|
+
``config``; the import must come from there. This test exercises the
|
|
805
|
+
method end-to-end so any future re-routing of ``ExternalAuthConfig``
|
|
806
|
+
that breaks the call site is caught immediately.
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
@staticmethod
|
|
810
|
+
def _clear_odps_env(monkeypatch):
|
|
811
|
+
import maxc_cli.backend as backend_module
|
|
812
|
+
for aliases in backend_module.ODPS_ENV_ALIASES.values():
|
|
813
|
+
for alias in aliases:
|
|
814
|
+
monkeypatch.delenv(alias, raising=False)
|
|
815
|
+
|
|
816
|
+
def test_writes_external_provider_config_without_validation(self, tmp_path, monkeypatch):
|
|
817
|
+
import yaml
|
|
818
|
+
|
|
819
|
+
from maxc_cli.app import MaxCApp
|
|
820
|
+
self._clear_odps_env(monkeypatch)
|
|
821
|
+
monkeypatch.chdir(tmp_path)
|
|
822
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
823
|
+
|
|
824
|
+
config_path = tmp_path / "config.yaml"
|
|
825
|
+
app = MaxCApp(cwd=tmp_path, load_backend=False)
|
|
826
|
+
|
|
827
|
+
envelope = app.auth_login_external(
|
|
828
|
+
process_command="/usr/bin/echo '{}'",
|
|
829
|
+
process_timeout=30,
|
|
830
|
+
project="proj_x",
|
|
831
|
+
endpoint="http://service.cn-test.maxcompute.aliyun.com/api",
|
|
832
|
+
region_name="cn-test",
|
|
833
|
+
no_validate=True,
|
|
834
|
+
target_config_path=config_path,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
assert envelope.status == "success"
|
|
838
|
+
assert envelope.command == "auth.login-external"
|
|
839
|
+
assert envelope.data["auth_type"] == "external"
|
|
840
|
+
assert envelope.data["process_command"] == "/usr/bin/echo '{}'"
|
|
841
|
+
|
|
842
|
+
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
843
|
+
assert saved["auth"]["provider"] == "external"
|
|
844
|
+
assert saved["auth"]["external"]["process_command"] == "/usr/bin/echo '{}'"
|
|
845
|
+
assert saved["auth"]["external"]["process_timeout"] == 30
|
|
846
|
+
assert saved["auth"]["project"] == "proj_x"
|
|
@@ -572,19 +572,19 @@ class TestParseSqlWithHints:
|
|
|
572
572
|
def test_multi_statement_injects_script_mode(self):
|
|
573
573
|
from maxc_cli.backend.query import _parse_sql_with_hints
|
|
574
574
|
|
|
575
|
-
_, hints = _parse_sql_with_hints("SELECT 1; SELECT 2")
|
|
575
|
+
_, hints, _ = _parse_sql_with_hints("SELECT 1; SELECT 2")
|
|
576
576
|
assert hints.get("odps.sql.submit.mode") == "script"
|
|
577
577
|
|
|
578
578
|
def test_single_statement_does_not_inject_script_mode(self):
|
|
579
579
|
from maxc_cli.backend.query import _parse_sql_with_hints
|
|
580
580
|
|
|
581
|
-
_, hints = _parse_sql_with_hints("SELECT 1")
|
|
581
|
+
_, hints, _ = _parse_sql_with_hints("SELECT 1")
|
|
582
582
|
assert "odps.sql.submit.mode" not in hints
|
|
583
583
|
|
|
584
584
|
def test_user_provided_script_mode_is_preserved(self):
|
|
585
585
|
from maxc_cli.backend.query import _parse_sql_with_hints
|
|
586
586
|
|
|
587
|
-
_, hints = _parse_sql_with_hints(
|
|
587
|
+
_, hints, _ = _parse_sql_with_hints(
|
|
588
588
|
"SET odps.sql.submit.mode=non_script; SELECT 1; SELECT 2"
|
|
589
589
|
)
|
|
590
590
|
# User's value wins
|
|
@@ -593,13 +593,13 @@ class TestParseSqlWithHints:
|
|
|
593
593
|
def test_trailing_semicolon_is_not_treated_as_multistatement(self):
|
|
594
594
|
from maxc_cli.backend.query import _parse_sql_with_hints
|
|
595
595
|
|
|
596
|
-
_, hints = _parse_sql_with_hints("SELECT 1;")
|
|
596
|
+
_, hints, _ = _parse_sql_with_hints("SELECT 1;")
|
|
597
597
|
assert "odps.sql.submit.mode" not in hints
|
|
598
598
|
|
|
599
599
|
def test_comments_are_not_counted_as_statements(self):
|
|
600
600
|
from maxc_cli.backend.query import _parse_sql_with_hints
|
|
601
601
|
|
|
602
|
-
_, hints = _parse_sql_with_hints(
|
|
602
|
+
_, hints, _ = _parse_sql_with_hints(
|
|
603
603
|
"-- header;\nSELECT 1; -- trailing comment;"
|
|
604
604
|
)
|
|
605
605
|
assert "odps.sql.submit.mode" not in hints
|
|
@@ -91,19 +91,21 @@ def test_set_with_escaped_semicolon():
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def test_parse_sql_with_hints_default_no_extra_hints():
|
|
94
|
-
actual_sql, hints = _parse_sql_with_hints("SELECT 1")
|
|
94
|
+
actual_sql, hints, priority = _parse_sql_with_hints("SELECT 1")
|
|
95
95
|
assert actual_sql == "SELECT 1"
|
|
96
96
|
assert hints == {}
|
|
97
|
+
assert priority is None
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
def test_parse_sql_with_hints_merges_user_set():
|
|
100
|
-
actual_sql, hints = _parse_sql_with_hints(
|
|
101
|
+
actual_sql, hints, priority = _parse_sql_with_hints(
|
|
101
102
|
"SET odps.sql.type.system.odps2=true; SELECT 1"
|
|
102
103
|
)
|
|
103
104
|
assert actual_sql == "SELECT 1"
|
|
104
105
|
assert hints == {
|
|
105
106
|
"odps.sql.type.system.odps2": "true",
|
|
106
107
|
}
|
|
108
|
+
assert priority is None
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
def test_parse_sql_with_hints_blocks_write_without_force():
|
|
@@ -117,13 +119,14 @@ def test_parse_sql_with_hints_blocks_write_without_force():
|
|
|
117
119
|
|
|
118
120
|
|
|
119
121
|
def test_parse_sql_with_hints_force_allows_write():
|
|
120
|
-
actual_sql, hints = _parse_sql_with_hints("CREATE TABLE t (id BIGINT)", force=True)
|
|
122
|
+
actual_sql, hints, priority = _parse_sql_with_hints("CREATE TABLE t (id BIGINT)", force=True)
|
|
121
123
|
assert actual_sql == "CREATE TABLE t (id BIGINT)"
|
|
122
124
|
assert hints == {}
|
|
125
|
+
assert priority is None
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
def test_parse_sql_with_hints_force_preserves_user_sets():
|
|
126
|
-
actual_sql, hints = _parse_sql_with_hints(
|
|
129
|
+
actual_sql, hints, _priority = _parse_sql_with_hints(
|
|
127
130
|
"SET odps.sql.type.system.odps2=true; CREATE TABLE t (id BIGINT)",
|
|
128
131
|
force=True,
|
|
129
132
|
)
|
|
@@ -136,6 +139,39 @@ def test_parse_sql_with_hints_invalid_set_raises():
|
|
|
136
139
|
_parse_sql_with_hints("SET no_semicolon SELECT 1")
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
def test_parse_sql_with_hints_extracts_priority():
|
|
143
|
+
actual_sql, hints, priority = _parse_sql_with_hints(
|
|
144
|
+
"SET odps.instance.priority=3; SELECT 1"
|
|
145
|
+
)
|
|
146
|
+
assert actual_sql == "SELECT 1"
|
|
147
|
+
assert priority == 3
|
|
148
|
+
# priority must be stripped from the hints dict — it's a run_sql kwarg, not a SQL hint.
|
|
149
|
+
assert "odps.instance.priority" not in hints
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_parse_sql_with_hints_priority_case_insensitive_key():
|
|
153
|
+
_, hints, priority = _parse_sql_with_hints(
|
|
154
|
+
"SET ODPS.Instance.Priority=5; SELECT 1"
|
|
155
|
+
)
|
|
156
|
+
assert priority == 5
|
|
157
|
+
assert hints == {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_parse_sql_with_hints_priority_invalid_raises():
|
|
161
|
+
with pytest.raises(ValidationError, match="odps.instance.priority"):
|
|
162
|
+
_parse_sql_with_hints("SET odps.instance.priority=high; SELECT 1")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_parse_sql_with_hints_priority_coexists_with_other_hints():
|
|
166
|
+
_, hints, priority = _parse_sql_with_hints(
|
|
167
|
+
"SET odps.instance.priority=1; "
|
|
168
|
+
"SET odps.sql.type.system.odps2=true; "
|
|
169
|
+
"SELECT 1"
|
|
170
|
+
)
|
|
171
|
+
assert priority == 1
|
|
172
|
+
assert hints == {"odps.sql.type.system.odps2": "true"}
|
|
173
|
+
|
|
174
|
+
|
|
139
175
|
# --- translate_odps_error readonly detection tests ---
|
|
140
176
|
|
|
141
177
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|