snowflake-cli 3.11.0__py3-none-any.whl → 3.13.0__py3-none-any.whl
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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +43 -1
- snowflake/cli/_app/commands_registration/builtin_plugins.py +1 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +14 -1
- snowflake/cli/_app/printing.py +153 -19
- snowflake/cli/_app/telemetry.py +25 -10
- snowflake/cli/_plugins/auth/__init__.py +0 -2
- snowflake/cli/_plugins/connection/commands.py +1 -78
- snowflake/cli/_plugins/dbt/commands.py +44 -19
- snowflake/cli/_plugins/dbt/constants.py +1 -1
- snowflake/cli/_plugins/dbt/manager.py +252 -47
- snowflake/cli/_plugins/dcm/commands.py +65 -90
- snowflake/cli/_plugins/dcm/manager.py +137 -50
- snowflake/cli/_plugins/logs/commands.py +7 -0
- snowflake/cli/_plugins/logs/manager.py +21 -1
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
- snowflake/cli/_plugins/object/manager.py +1 -0
- snowflake/cli/_plugins/snowpark/common.py +1 -0
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +29 -5
- snowflake/cli/_plugins/snowpark/package_utils.py +44 -3
- snowflake/cli/_plugins/spcs/services/commands.py +19 -1
- snowflake/cli/_plugins/spcs/services/manager.py +17 -4
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +5 -0
- snowflake/cli/_plugins/sql/lexer/types.py +1 -0
- snowflake/cli/_plugins/sql/repl.py +100 -26
- snowflake/cli/_plugins/sql/repl_commands.py +607 -0
- snowflake/cli/_plugins/sql/statement_reader.py +44 -20
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +28 -2
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +24 -4
- snowflake/cli/api/artifacts/bundle_map.py +32 -2
- snowflake/cli/api/artifacts/regex_resolver.py +54 -0
- snowflake/cli/api/artifacts/upload.py +5 -1
- snowflake/cli/api/artifacts/utils.py +12 -1
- snowflake/cli/api/cli_global_context.py +7 -0
- snowflake/cli/api/commands/decorators.py +7 -0
- snowflake/cli/api/commands/flags.py +24 -1
- snowflake/cli/api/console/abc.py +13 -2
- snowflake/cli/api/console/console.py +20 -0
- snowflake/cli/api/constants.py +9 -0
- snowflake/cli/api/entities/utils.py +10 -6
- snowflake/cli/api/feature_flags.py +3 -2
- snowflake/cli/api/identifiers.py +18 -1
- snowflake/cli/api/project/schemas/entities/entities.py +0 -6
- snowflake/cli/api/rendering/sql_templates.py +2 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/METADATA +7 -7
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/RECORD +51 -54
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +0 -153
- snowflake/cli/_plugins/auth/keypair/manager.py +0 -331
- snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
- snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
- /snowflake/cli/_plugins/auth/{keypair/plugin_spec.py → plugin_spec.py} +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Dict, Generator, Iterable, List, Tuple, Type
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from snowflake.cli._app.printing import print_result
|
|
13
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
14
|
+
from snowflake.cli.api.console import cli_console
|
|
15
|
+
from snowflake.cli.api.exceptions import CliError
|
|
16
|
+
from snowflake.cli.api.output.types import CollectionResult, QueryResult
|
|
17
|
+
from snowflake.connector import SnowflakeConnection
|
|
18
|
+
|
|
19
|
+
# Command pattern to detect REPL commands
|
|
20
|
+
COMMAND_PATTERN = re.compile(r"^(![\w]+)(?:\s+(.*))?$")
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
VALID_UUID_RE = re.compile(
|
|
25
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Registry for auto-discovery of commands
|
|
29
|
+
_COMMAND_REGISTRY: Dict[str, Type["ReplCommand"]] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnknownCommandError(CliError):
|
|
33
|
+
"""Raised when a command pattern matches but no registered command is found."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, command_name: str):
|
|
36
|
+
self.command_name = command_name
|
|
37
|
+
super().__init__(f"Unknown command '{command_name}'")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_command(command_name: str | List[str]):
|
|
41
|
+
"""Decorator to register a command class."""
|
|
42
|
+
|
|
43
|
+
def decorator(cls):
|
|
44
|
+
command_names = (
|
|
45
|
+
command_name if isinstance(command_name, list) else [command_name]
|
|
46
|
+
)
|
|
47
|
+
for cmd_name in command_names:
|
|
48
|
+
_COMMAND_REGISTRY[cmd_name.lower()] = cls
|
|
49
|
+
return cls
|
|
50
|
+
|
|
51
|
+
return decorator
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ReplCommand(abc.ABC):
|
|
55
|
+
"""Base class for REPL commands."""
|
|
56
|
+
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
def execute(self, connection: SnowflakeConnection):
|
|
59
|
+
"""Executes command and prints the result."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def from_args(cls, raw_args, kwargs=None) -> "CompileCommandResult":
|
|
65
|
+
"""Parses raw argument string and creates command ready for execution."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _parse_args(cls, raw_args: str) -> tuple[List[str], Dict[str, Any]]:
|
|
70
|
+
"""Parse raw argument string into positional args and keyword arguments.
|
|
71
|
+
|
|
72
|
+
This is a helper method that commands can use for standard argument parsing.
|
|
73
|
+
Commands can override this if they need custom parsing logic.
|
|
74
|
+
"""
|
|
75
|
+
if not raw_args:
|
|
76
|
+
return [], {}
|
|
77
|
+
|
|
78
|
+
args = []
|
|
79
|
+
kwargs = {}
|
|
80
|
+
cmd_args = raw_args.split()
|
|
81
|
+
|
|
82
|
+
for cmd_arg in cmd_args:
|
|
83
|
+
if "=" not in cmd_arg:
|
|
84
|
+
args.append(cmd_arg)
|
|
85
|
+
else:
|
|
86
|
+
key, val = cmd_arg.split("=", maxsplit=1)
|
|
87
|
+
if key in kwargs:
|
|
88
|
+
raise ValueError(f"Duplicated argument '{key}'")
|
|
89
|
+
kwargs[key] = val
|
|
90
|
+
|
|
91
|
+
return args, kwargs
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _print_result_to_stdout(headers: Iterable[str], rows: Iterable[Iterable[Any]]):
|
|
95
|
+
formatted_rows: Generator[Dict[str, Any], None, None] = (
|
|
96
|
+
{key: value for key, value in zip(headers, row)} for row in rows
|
|
97
|
+
)
|
|
98
|
+
print_result(CollectionResult(formatted_rows))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class CompileCommandResult:
|
|
103
|
+
command: ReplCommand | None = None
|
|
104
|
+
error_message: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@register_command("!queries")
|
|
108
|
+
@dataclass
|
|
109
|
+
class QueriesCommand(ReplCommand):
|
|
110
|
+
"""Command to query and display query history."""
|
|
111
|
+
|
|
112
|
+
help_mode: bool = False
|
|
113
|
+
from_current_session: bool = False
|
|
114
|
+
amount: int = 25
|
|
115
|
+
user: str | None = None
|
|
116
|
+
warehouse: str | None = None
|
|
117
|
+
start_timestamp_ms: int | None = None
|
|
118
|
+
end_timestamp_ms: int | None = None
|
|
119
|
+
duration: str | None = None
|
|
120
|
+
stmt_type: str | None = None
|
|
121
|
+
status: str | None = None
|
|
122
|
+
|
|
123
|
+
def execute(self, connection: SnowflakeConnection) -> None:
|
|
124
|
+
if self.help_mode:
|
|
125
|
+
self._execute_help()
|
|
126
|
+
else:
|
|
127
|
+
self._execute_queries(connection)
|
|
128
|
+
|
|
129
|
+
def _execute_help(self):
|
|
130
|
+
headers = ["FILTER", "ARGUMENT", "DEFAULT"]
|
|
131
|
+
filters = [
|
|
132
|
+
["amount", "integer", "25"],
|
|
133
|
+
["status", "string", "any"],
|
|
134
|
+
["warehouse", "string", "any"],
|
|
135
|
+
["user", "string", "any"],
|
|
136
|
+
[
|
|
137
|
+
"start_date",
|
|
138
|
+
"datetime in ISO format (for example YYYY-MM-DDTHH:mm:ss.sss)",
|
|
139
|
+
"any",
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"end_date",
|
|
143
|
+
"datetime in ISO format (for example YYYY-MM-DDTHH:mm:ss.sss)",
|
|
144
|
+
"any",
|
|
145
|
+
],
|
|
146
|
+
["start", "timestamp in milliseconds (integer)", "any"],
|
|
147
|
+
["end", "timestamp in milliseconds (integer)", "any"],
|
|
148
|
+
["type", "string", "any"],
|
|
149
|
+
["duration", "time in milliseconds", "any"],
|
|
150
|
+
["session", "No arguments", "any"],
|
|
151
|
+
]
|
|
152
|
+
_print_result_to_stdout(headers, filters)
|
|
153
|
+
|
|
154
|
+
def _execute_queries(self, connection: SnowflakeConnection) -> None:
|
|
155
|
+
url_parameters = {
|
|
156
|
+
"_dc": f"{time.time()}",
|
|
157
|
+
"includeDDL": "false",
|
|
158
|
+
"max": self.amount,
|
|
159
|
+
}
|
|
160
|
+
if self.user:
|
|
161
|
+
url_parameters["user"] = self.user
|
|
162
|
+
if self.warehouse:
|
|
163
|
+
url_parameters["wh"] = self.warehouse
|
|
164
|
+
if self.start_timestamp_ms:
|
|
165
|
+
url_parameters["start"] = self.start_timestamp_ms
|
|
166
|
+
if self.end_timestamp_ms:
|
|
167
|
+
url_parameters["end"] = self.end_timestamp_ms
|
|
168
|
+
if self.duration:
|
|
169
|
+
url_parameters["min_duration"] = self.duration
|
|
170
|
+
if self.from_current_session:
|
|
171
|
+
url_parameters["session_id"] = connection.session_id
|
|
172
|
+
if self.status:
|
|
173
|
+
url_parameters["subset"] = self.status
|
|
174
|
+
if self.stmt_type:
|
|
175
|
+
url_parameters["stmt_type"] = self.stmt_type
|
|
176
|
+
|
|
177
|
+
url = "/monitoring/queries?" + urlencode(url_parameters)
|
|
178
|
+
ret = connection.rest.request(url=url, method="get", client="rest")
|
|
179
|
+
if ret.get("data") and ret["data"].get("queries"):
|
|
180
|
+
_result: Generator[Tuple[str, str, str, str], None, None] = (
|
|
181
|
+
(
|
|
182
|
+
query["id"],
|
|
183
|
+
query["sqlText"],
|
|
184
|
+
query["state"],
|
|
185
|
+
query["totalDuration"],
|
|
186
|
+
)
|
|
187
|
+
for query in ret["data"]["queries"]
|
|
188
|
+
)
|
|
189
|
+
_print_result_to_stdout(
|
|
190
|
+
["QUERY ID", "SQL TEXT", "STATUS", "DURATION_MS"], _result
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_args(cls, raw_args, kwargs=None) -> CompileCommandResult:
|
|
195
|
+
"""Parse arguments and create QueriesCommand instance.
|
|
196
|
+
|
|
197
|
+
Supports both calling patterns:
|
|
198
|
+
- New pattern: from_args("amount=3 user=jdoe")
|
|
199
|
+
- Old pattern: from_args(["session"], {"amount": "3"})
|
|
200
|
+
"""
|
|
201
|
+
if isinstance(raw_args, str):
|
|
202
|
+
try:
|
|
203
|
+
args, kwargs = cls._parse_args(raw_args)
|
|
204
|
+
except ValueError as e:
|
|
205
|
+
return CompileCommandResult(error_message=str(e))
|
|
206
|
+
else:
|
|
207
|
+
args, kwargs = raw_args, kwargs or {}
|
|
208
|
+
|
|
209
|
+
return cls._from_parsed_args(args, kwargs)
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def _from_parsed_args(
|
|
213
|
+
cls, args: List[str], kwargs: Dict[str, Any]
|
|
214
|
+
) -> CompileCommandResult:
|
|
215
|
+
if "help" in args:
|
|
216
|
+
return CompileCommandResult(command=cls(help_mode=True))
|
|
217
|
+
|
|
218
|
+
# "session" is set by default if no other arguments are provided
|
|
219
|
+
from_current_session = "session" in args or not kwargs
|
|
220
|
+
amount = kwargs.pop("amount", "25")
|
|
221
|
+
if not amount.isdigit():
|
|
222
|
+
return CompileCommandResult(
|
|
223
|
+
error_message=f"Invalid argument passed to 'amount' filter: {amount}"
|
|
224
|
+
)
|
|
225
|
+
user = kwargs.pop("user", None)
|
|
226
|
+
warehouse = kwargs.pop("warehouse", None)
|
|
227
|
+
duration = kwargs.pop("duration", None)
|
|
228
|
+
|
|
229
|
+
start_timestamp_ms = kwargs.pop("start", None)
|
|
230
|
+
if start_timestamp_ms:
|
|
231
|
+
try:
|
|
232
|
+
start_timestamp_ms = int(start_timestamp_ms)
|
|
233
|
+
except ValueError:
|
|
234
|
+
return CompileCommandResult(
|
|
235
|
+
error_message=f"Invalid argument passed to 'start' filter: {start_timestamp_ms}"
|
|
236
|
+
)
|
|
237
|
+
end_timestamp_ms = kwargs.pop("end", None)
|
|
238
|
+
if end_timestamp_ms:
|
|
239
|
+
try:
|
|
240
|
+
end_timestamp_ms = int(end_timestamp_ms)
|
|
241
|
+
except ValueError:
|
|
242
|
+
return CompileCommandResult(
|
|
243
|
+
error_message=f"Invalid argument passed to 'end' filter: {end_timestamp_ms}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
start_date = kwargs.pop("start_date", None)
|
|
247
|
+
if start_date:
|
|
248
|
+
if start_timestamp_ms:
|
|
249
|
+
return CompileCommandResult(
|
|
250
|
+
error_message="'start_date' filter cannot be used with 'start' filter"
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
seconds = datetime.fromisoformat(start_date).timestamp()
|
|
254
|
+
start_timestamp_ms = int(seconds * 1000) # convert to milliseconds
|
|
255
|
+
except ValueError:
|
|
256
|
+
return CompileCommandResult(
|
|
257
|
+
error_message=f"Invalid date format passed to 'start_date' filter: {start_date}"
|
|
258
|
+
)
|
|
259
|
+
end_date = kwargs.pop("end_date", None)
|
|
260
|
+
if end_date:
|
|
261
|
+
if end_timestamp_ms:
|
|
262
|
+
return CompileCommandResult(
|
|
263
|
+
error_message="'end_date' filter cannot be used with 'end' filter"
|
|
264
|
+
)
|
|
265
|
+
try:
|
|
266
|
+
seconds = datetime.fromisoformat(end_date).timestamp()
|
|
267
|
+
end_timestamp_ms = int(seconds * 1000) # convert to milliseconds
|
|
268
|
+
except ValueError:
|
|
269
|
+
return CompileCommandResult(
|
|
270
|
+
error_message=f"Invalid date format passed to 'end_date' filter: {end_date}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
stmt_type = kwargs.pop("type", None)
|
|
274
|
+
if stmt_type:
|
|
275
|
+
stmt_type = stmt_type.upper()
|
|
276
|
+
if stmt_type not in [
|
|
277
|
+
"ANY",
|
|
278
|
+
"SELECT",
|
|
279
|
+
"INSERT",
|
|
280
|
+
"UPDATE",
|
|
281
|
+
"DELETE",
|
|
282
|
+
"MERGE",
|
|
283
|
+
"MULTI_TABLE_INSERT",
|
|
284
|
+
"COPY",
|
|
285
|
+
"COMMIT",
|
|
286
|
+
"ROLLBACK",
|
|
287
|
+
"BEGIN_TRANSACTION",
|
|
288
|
+
"SHOW",
|
|
289
|
+
"GRANT",
|
|
290
|
+
"CREATE",
|
|
291
|
+
"ALTER",
|
|
292
|
+
]:
|
|
293
|
+
return CompileCommandResult(
|
|
294
|
+
error_message=f"Invalid argument passed to 'type' filter: {stmt_type}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
status = kwargs.pop("status", None)
|
|
298
|
+
if status:
|
|
299
|
+
status = status.upper()
|
|
300
|
+
if status not in [
|
|
301
|
+
"RUNNING",
|
|
302
|
+
"SUCCEEDED",
|
|
303
|
+
"FAILED",
|
|
304
|
+
"BLOCKED",
|
|
305
|
+
"QUEUED",
|
|
306
|
+
"ABORTED",
|
|
307
|
+
]:
|
|
308
|
+
return CompileCommandResult(
|
|
309
|
+
error_message=f"Invalid argument passed to 'status' filter: {status}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
for arg in args:
|
|
313
|
+
if arg.lower() not in ["session", "help"]:
|
|
314
|
+
return CompileCommandResult(
|
|
315
|
+
error_message=f"Invalid argument passed to 'queries' command: {arg}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
kwargs_error = _validate_kwargs_empty("queries", kwargs)
|
|
319
|
+
if kwargs_error:
|
|
320
|
+
return CompileCommandResult(error_message=kwargs_error)
|
|
321
|
+
|
|
322
|
+
return CompileCommandResult(
|
|
323
|
+
command=cls(
|
|
324
|
+
help_mode=False,
|
|
325
|
+
from_current_session=from_current_session,
|
|
326
|
+
amount=int(amount),
|
|
327
|
+
user=user,
|
|
328
|
+
warehouse=warehouse,
|
|
329
|
+
start_timestamp_ms=start_timestamp_ms,
|
|
330
|
+
end_timestamp_ms=end_timestamp_ms,
|
|
331
|
+
duration=duration,
|
|
332
|
+
stmt_type=stmt_type,
|
|
333
|
+
status=status,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _validate_kwargs_empty(command_name: str, kwargs: Dict[str, Any]) -> str | None:
|
|
339
|
+
"""Validate that kwargs is empty and return comprehensive error message if not."""
|
|
340
|
+
if not kwargs:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
invalid_args = [f"{key}={value}" for key, value in kwargs.items()]
|
|
344
|
+
if len(invalid_args) == 1:
|
|
345
|
+
return f"Invalid argument passed to '{command_name}' command: {invalid_args[0]}"
|
|
346
|
+
else:
|
|
347
|
+
args_str = ", ".join(invalid_args)
|
|
348
|
+
return f"Invalid arguments passed to '{command_name}' command: {args_str}"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _validate_only_arg_is_query_id(
|
|
352
|
+
command_name: str, args: List[str], kwargs: Dict[str, Any]
|
|
353
|
+
) -> str | None:
|
|
354
|
+
kwargs_error = _validate_kwargs_empty(command_name, kwargs)
|
|
355
|
+
if kwargs_error:
|
|
356
|
+
return kwargs_error
|
|
357
|
+
if len(args) != 1:
|
|
358
|
+
amount = "Too many" if args else "No"
|
|
359
|
+
return f"{amount} arguments passed to '{command_name}' command. Usage: `!{command_name} <query id>`"
|
|
360
|
+
|
|
361
|
+
qid = args[0]
|
|
362
|
+
if not VALID_UUID_RE.match(qid):
|
|
363
|
+
return f"Invalid query ID passed to '{command_name}' command: {qid}"
|
|
364
|
+
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@register_command("!result")
|
|
369
|
+
@dataclass
|
|
370
|
+
class ResultCommand(ReplCommand):
|
|
371
|
+
"""Command to retrieve and display query results by ID."""
|
|
372
|
+
|
|
373
|
+
query_id: str
|
|
374
|
+
|
|
375
|
+
def execute(self, connection: SnowflakeConnection):
|
|
376
|
+
cursor = connection.cursor()
|
|
377
|
+
cursor.query_result(self.query_id)
|
|
378
|
+
print_result(QueryResult(cursor=cursor))
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def from_args(cls, raw_args, kwargs=None) -> CompileCommandResult:
|
|
382
|
+
"""Parse arguments and create ResultCommand instance.
|
|
383
|
+
|
|
384
|
+
Supports both calling patterns:
|
|
385
|
+
- New pattern: from_args("00000000-0000-0000-0000-000000000000")
|
|
386
|
+
- Old pattern: from_args(["query_id"], {})
|
|
387
|
+
"""
|
|
388
|
+
if isinstance(raw_args, str):
|
|
389
|
+
try:
|
|
390
|
+
args, kwargs = cls._parse_args(raw_args)
|
|
391
|
+
except ValueError as e:
|
|
392
|
+
return CompileCommandResult(error_message=str(e))
|
|
393
|
+
else:
|
|
394
|
+
args, kwargs = raw_args, kwargs or {}
|
|
395
|
+
|
|
396
|
+
return cls._from_parsed_args(args, kwargs)
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def _from_parsed_args(cls, args, kwargs) -> CompileCommandResult:
|
|
400
|
+
error_msg = _validate_only_arg_is_query_id("result", args, kwargs)
|
|
401
|
+
if error_msg:
|
|
402
|
+
return CompileCommandResult(error_message=error_msg)
|
|
403
|
+
return CompileCommandResult(command=cls(args[0]))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@register_command("!abort")
|
|
407
|
+
@dataclass
|
|
408
|
+
class AbortCommand(ReplCommand):
|
|
409
|
+
"""Command to abort a running query by ID."""
|
|
410
|
+
|
|
411
|
+
query_id: str
|
|
412
|
+
|
|
413
|
+
def execute(self, connection: SnowflakeConnection):
|
|
414
|
+
cursor = connection.cursor()
|
|
415
|
+
cursor.execute("SELECT SYSTEM$CANCEL_QUERY(%s)", (self.query_id,))
|
|
416
|
+
print_result(QueryResult(cursor=cursor))
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
def from_args(cls, raw_args, kwargs=None) -> CompileCommandResult:
|
|
420
|
+
"""Parse arguments and create AbortCommand instance.
|
|
421
|
+
|
|
422
|
+
Supports both calling patterns:
|
|
423
|
+
- New pattern: from_args("00000000-0000-0000-0000-000000000000")
|
|
424
|
+
- Old pattern: from_args(["query_id"], {})
|
|
425
|
+
"""
|
|
426
|
+
if isinstance(raw_args, str):
|
|
427
|
+
try:
|
|
428
|
+
args, kwargs = cls._parse_args(raw_args)
|
|
429
|
+
except ValueError as e:
|
|
430
|
+
return CompileCommandResult(error_message=str(e))
|
|
431
|
+
else:
|
|
432
|
+
args, kwargs = raw_args, kwargs or {}
|
|
433
|
+
|
|
434
|
+
return cls._from_parsed_args(args, kwargs)
|
|
435
|
+
|
|
436
|
+
@classmethod
|
|
437
|
+
def _from_parsed_args(cls, args, kwargs) -> CompileCommandResult:
|
|
438
|
+
error_msg = _validate_only_arg_is_query_id("abort", args, kwargs)
|
|
439
|
+
if error_msg:
|
|
440
|
+
return CompileCommandResult(error_message=error_msg)
|
|
441
|
+
return CompileCommandResult(command=cls(args[0]))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@register_command("!edit")
|
|
445
|
+
@dataclass
|
|
446
|
+
class EditCommand(ReplCommand):
|
|
447
|
+
"""Command to edit SQL statements using an external editor."""
|
|
448
|
+
|
|
449
|
+
sql_content: str = ""
|
|
450
|
+
|
|
451
|
+
def execute(self, connection: SnowflakeConnection):
|
|
452
|
+
"""Execute the edit command.
|
|
453
|
+
|
|
454
|
+
Flow:
|
|
455
|
+
1. Validate REPL mode and EDITOR environment variable
|
|
456
|
+
2. Get content to edit (provided args or last command from history)
|
|
457
|
+
3. Open editor with content using click.edit()
|
|
458
|
+
4. Inject edited content back into REPL prompt for execution
|
|
459
|
+
"""
|
|
460
|
+
if not get_cli_context().is_repl:
|
|
461
|
+
raise CliError("The edit command can only be used in interactive mode.")
|
|
462
|
+
|
|
463
|
+
editor = os.environ.get("EDITOR")
|
|
464
|
+
if not editor:
|
|
465
|
+
raise CliError(
|
|
466
|
+
"No editor is set. Please set the EDITOR environment variable."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
content_to_edit = self.sql_content
|
|
470
|
+
if not content_to_edit:
|
|
471
|
+
content_to_edit = self._get_last_command_from_history()
|
|
472
|
+
|
|
473
|
+
edited_content = click.edit(
|
|
474
|
+
text=content_to_edit, editor=editor, extension=".sql", require_save=False
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if edited_content is None:
|
|
478
|
+
log.debug("Editor closed without changes")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
edited_content = edited_content.strip()
|
|
482
|
+
|
|
483
|
+
if edited_content:
|
|
484
|
+
log.debug("Editor returned content, length: %d", len(edited_content))
|
|
485
|
+
|
|
486
|
+
if repl := get_cli_context().repl:
|
|
487
|
+
repl.set_next_input(edited_content)
|
|
488
|
+
else:
|
|
489
|
+
log.warning("REPL instance not found in context")
|
|
490
|
+
|
|
491
|
+
cli_console.message(
|
|
492
|
+
"[green]✓ Edited SQL loaded into prompt. Modify as needed or press Enter to execute.[/green]"
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
log.debug("Editor returned empty content")
|
|
496
|
+
cli_console.message("[yellow]Editor closed with no content.[/yellow]")
|
|
497
|
+
|
|
498
|
+
def _get_last_command_from_history(self) -> str:
|
|
499
|
+
"""Get the last command from the REPL history."""
|
|
500
|
+
repl = get_cli_context().repl
|
|
501
|
+
if repl and repl.history:
|
|
502
|
+
history_entries = list(repl.history.get_strings())
|
|
503
|
+
for entry in reversed(history_entries):
|
|
504
|
+
entry = entry.strip()
|
|
505
|
+
is_repl_command = entry and entry.startswith("!")
|
|
506
|
+
if not is_repl_command:
|
|
507
|
+
return entry
|
|
508
|
+
|
|
509
|
+
return ""
|
|
510
|
+
|
|
511
|
+
@classmethod
|
|
512
|
+
def from_args(cls, raw_args, kwargs=None) -> CompileCommandResult:
|
|
513
|
+
"""Parse arguments and create EditCommand instance.
|
|
514
|
+
|
|
515
|
+
Supports both calling patterns:
|
|
516
|
+
- New pattern: from_args("SELECT * FROM table WHERE id = 1")
|
|
517
|
+
- Old pattern: from_args(["SELECT", "*", "FROM", "table"], {})
|
|
518
|
+
"""
|
|
519
|
+
if isinstance(raw_args, str):
|
|
520
|
+
try:
|
|
521
|
+
args, kwargs = cls._parse_args(raw_args)
|
|
522
|
+
except ValueError as e:
|
|
523
|
+
return CompileCommandResult(error_message=str(e))
|
|
524
|
+
else:
|
|
525
|
+
args, kwargs = raw_args, kwargs or {}
|
|
526
|
+
|
|
527
|
+
return cls._from_parsed_args(args, kwargs)
|
|
528
|
+
|
|
529
|
+
@classmethod
|
|
530
|
+
def _parse_args(cls, raw_args: str) -> tuple[List[str], Dict[str, Any]]:
|
|
531
|
+
"""Parse raw argument string for EditCommand.
|
|
532
|
+
|
|
533
|
+
Unlike other commands, EditCommand treats all arguments as SQL content,
|
|
534
|
+
not as key-value pairs. This allows SQL with equals signs to work correctly.
|
|
535
|
+
"""
|
|
536
|
+
return [raw_args] if raw_args else [], {}
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def _from_parsed_args(cls, args, kwargs) -> CompileCommandResult:
|
|
540
|
+
"""Create EditCommand from parsed arguments.
|
|
541
|
+
|
|
542
|
+
Since EditCommand's custom _parse_args always returns empty kwargs,
|
|
543
|
+
we only need to handle the args to reconstruct the SQL content.
|
|
544
|
+
"""
|
|
545
|
+
sql_content = " ".join(args) if args else ""
|
|
546
|
+
return CompileCommandResult(command=cls(sql_content=sql_content))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def detect_command(input_text: str) -> tuple[str, str] | None:
|
|
550
|
+
"""Detect if input text matches a command pattern.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
tuple[command_name, raw_args] if command pattern is detected, None otherwise
|
|
554
|
+
"""
|
|
555
|
+
match = COMMAND_PATTERN.match(input_text.strip())
|
|
556
|
+
if match:
|
|
557
|
+
command_name = match.group(1) # The !command part
|
|
558
|
+
raw_args = match.group(2) or "" # Everything after the command
|
|
559
|
+
return command_name, raw_args
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def is_registered_command(command_name: str) -> bool:
|
|
564
|
+
"""Check if a command name is registered."""
|
|
565
|
+
return command_name.lower() in _COMMAND_REGISTRY
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def get_command_class(command_name: str) -> Type[ReplCommand] | None:
|
|
569
|
+
"""Get the command class for a given command name."""
|
|
570
|
+
return _COMMAND_REGISTRY.get(command_name.lower())
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def compile_repl_command(input_text: str) -> CompileCommandResult:
|
|
574
|
+
"""Detect and compile a REPL command from input text.
|
|
575
|
+
|
|
576
|
+
This function handles:
|
|
577
|
+
1. Command pattern detection
|
|
578
|
+
2. Command registration checking
|
|
579
|
+
3. Delegation to command-specific parsing
|
|
580
|
+
"""
|
|
581
|
+
# Step 1: Detect if this is a command
|
|
582
|
+
detection_result = detect_command(input_text)
|
|
583
|
+
if not detection_result:
|
|
584
|
+
log.info("Input does not match command pattern")
|
|
585
|
+
return CompileCommandResult(error_message="Not a command")
|
|
586
|
+
|
|
587
|
+
command_name, raw_args = detection_result
|
|
588
|
+
log.debug("Detected command: %s", command_name)
|
|
589
|
+
|
|
590
|
+
# Step 2: Check if command is registered
|
|
591
|
+
if not is_registered_command(command_name):
|
|
592
|
+
log.info("Unknown command: %s", command_name)
|
|
593
|
+
raise UnknownCommandError(command_name)
|
|
594
|
+
|
|
595
|
+
# Step 3: Get command class and delegate parsing
|
|
596
|
+
command_class = get_command_class(command_name)
|
|
597
|
+
if command_class is None:
|
|
598
|
+
# This should never happen since we already checked registration
|
|
599
|
+
raise RuntimeError(f"Command class not found for {command_name}")
|
|
600
|
+
|
|
601
|
+
log.debug("Found command class: %s", command_class.__name__)
|
|
602
|
+
return command_class.from_args(raw_args)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def get_available_commands() -> List[str]:
|
|
606
|
+
"""Returns a list of all registered command names."""
|
|
607
|
+
return list(_COMMAND_REGISTRY.keys())
|