snowflake-cli 3.1.0__py3-none-any.whl → 3.2.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.
Files changed (60) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
  3. snowflake/cli/_plugins/connection/commands.py +124 -109
  4. snowflake/cli/_plugins/connection/util.py +54 -9
  5. snowflake/cli/_plugins/cortex/manager.py +1 -1
  6. snowflake/cli/_plugins/git/manager.py +4 -4
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  9. snowflake/cli/_plugins/nativeapp/commands.py +10 -3
  10. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  11. snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
  13. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  14. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  15. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  16. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  17. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  19. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
  20. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
  21. snowflake/cli/_plugins/notebook/manager.py +2 -2
  22. snowflake/cli/_plugins/object/commands.py +10 -1
  23. snowflake/cli/_plugins/object/manager.py +13 -5
  24. snowflake/cli/_plugins/snowpark/common.py +3 -3
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
  26. snowflake/cli/_plugins/spcs/common.py +29 -0
  27. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  28. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  29. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  30. snowflake/cli/_plugins/spcs/services/commands.py +64 -13
  31. snowflake/cli/_plugins/spcs/services/manager.py +75 -15
  32. snowflake/cli/_plugins/sql/commands.py +9 -1
  33. snowflake/cli/_plugins/sql/manager.py +9 -4
  34. snowflake/cli/_plugins/stage/commands.py +20 -16
  35. snowflake/cli/_plugins/stage/diff.py +1 -1
  36. snowflake/cli/_plugins/stage/manager.py +140 -11
  37. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  38. snowflake/cli/_plugins/workspace/commands.py +6 -3
  39. snowflake/cli/api/cli_global_context.py +1 -0
  40. snowflake/cli/api/config.py +23 -5
  41. snowflake/cli/api/console/console.py +4 -19
  42. snowflake/cli/api/entities/utils.py +19 -32
  43. snowflake/cli/api/errno.py +2 -0
  44. snowflake/cli/api/exceptions.py +9 -0
  45. snowflake/cli/api/metrics.py +223 -7
  46. snowflake/cli/api/output/types.py +1 -1
  47. snowflake/cli/api/project/definition_conversion.py +179 -62
  48. snowflake/cli/api/rest_api.py +26 -4
  49. snowflake/cli/api/secure_utils.py +1 -1
  50. snowflake/cli/api/sql_execution.py +35 -22
  51. snowflake/cli/api/stage_path.py +5 -2
  52. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
  53. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
  54. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  55. snowflake/cli/_plugins/nativeapp/manager.py +0 -392
  56. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  57. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  58. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
  59. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  60. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,8 @@ from click import ClickException
21
21
  from snowflake.cli._plugins.object.manager import ObjectManager
22
22
  from snowflake.cli.api.commands.flags import (
23
23
  IdentifierType,
24
+ IfNotExistsOption,
25
+ ReplaceOption,
24
26
  like_option,
25
27
  )
26
28
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
@@ -145,6 +147,8 @@ def create(
145
147
  object_type: str = ObjectArgument,
146
148
  object_attributes: Optional[List[str]] = ObjectAttributesArgument,
147
149
  object_json: str = ObjectDefinitionJsonOption,
150
+ if_not_exists: bool = IfNotExistsOption(),
151
+ replace: bool = ReplaceOption(),
148
152
  **options,
149
153
  ):
150
154
  """
@@ -176,5 +180,10 @@ def create(
176
180
  "Provide either list of object attributes, or object definition in JSON format"
177
181
  )
178
182
 
179
- result = ObjectManager().create(object_type=object_type, object_data=object_data)
183
+ result = ObjectManager().create(
184
+ object_type=object_type,
185
+ object_data=object_data,
186
+ if_not_exists=if_not_exists,
187
+ replace=replace,
188
+ )
180
189
  return MessageResult(result)
@@ -52,11 +52,11 @@ class ObjectManager(SqlExecutionMixin):
52
52
  query += f" like '{like}'"
53
53
  if scope[0] is not None:
54
54
  query += f" in {scope[0].replace('-', ' ')} {scope[1]}"
55
- return self._execute_query(query, **kwargs)
55
+ return self.execute_query(query, **kwargs)
56
56
 
57
57
  def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor:
58
58
  object_name = _get_object_names(object_type).sf_name
59
- return self._execute_query(f"drop {object_name} {fqn.sql_identifier}")
59
+ return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
60
60
 
61
61
  def describe(self, *, object_type: str, fqn: FQN):
62
62
  # Image repository is the only supported object that does not have a DESCRIBE command.
@@ -65,7 +65,7 @@ class ObjectManager(SqlExecutionMixin):
65
65
  f"Describe is currently not supported for object of type image-repository"
66
66
  )
67
67
  object_name = _get_object_names(object_type).sf_name
68
- return self._execute_query(f"describe {object_name} {fqn.sql_identifier}")
68
+ return self.execute_query(f"describe {object_name} {fqn.sql_identifier}")
69
69
 
70
70
  def object_exists(self, *, object_type: str, fqn: FQN):
71
71
  try:
@@ -74,9 +74,17 @@ class ObjectManager(SqlExecutionMixin):
74
74
  except ProgrammingError:
75
75
  return False
76
76
 
77
- def create(self, object_type: str, object_data: Dict[str, Any]) -> str:
77
+ def create(
78
+ self,
79
+ object_type: str,
80
+ object_data: Dict[str, Any],
81
+ replace: bool = False,
82
+ if_not_exists: bool = False,
83
+ ) -> str:
78
84
  rest = RestApi(self._conn)
79
- url = rest.determine_url_for_create_query(object_type=object_type)
85
+ url = rest.determine_url_for_create_query(
86
+ object_type=object_type, replace=replace, if_not_exists=if_not_exists
87
+ )
80
88
  try:
81
89
  response = rest.send_rest_request(url=url, method="post", data=object_data)
82
90
  except Exception as err:
@@ -58,9 +58,9 @@ class SnowparkObjectManager(SqlExecutionMixin):
58
58
  self, execution_identifier: str, object_type: SnowparkObject
59
59
  ) -> SnowflakeCursor:
60
60
  if object_type == SnowparkObject.FUNCTION:
61
- return self._execute_query(f"select {execution_identifier}")
61
+ return self.execute_query(f"select {execution_identifier}")
62
62
  if object_type == SnowparkObject.PROCEDURE:
63
- return self._execute_query(f"call {execution_identifier}")
63
+ return self.execute_query(f"call {execution_identifier}")
64
64
  raise UsageError(f"Unknown object type: {object_type}.")
65
65
 
66
66
  def create_or_replace(
@@ -95,7 +95,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
95
95
  if isinstance(entity, ProcedureEntityModel) and entity.execute_as_caller:
96
96
  query.append("execute as caller")
97
97
 
98
- return self._execute_query("\n".join(query))
98
+ return self.execute_query("\n".join(query))
99
99
 
100
100
  def deploy_entity(
101
101
  self,
@@ -177,7 +177,7 @@ class AnacondaPackagesManager(SqlExecutionMixin):
177
177
  return AnacondaPackages(packages)
178
178
 
179
179
  def _query_snowflake_for_available_packages(self) -> dict[str, AvailablePackage]:
180
- cursor = self._execute_query(
180
+ cursor = self.execute_query(
181
181
  "select package_name, version from snowflake.information_schema.packages where language = 'python'",
182
182
  cursor_class=DictCursor,
183
183
  )
@@ -95,5 +95,34 @@ def handle_object_already_exists(
95
95
  raise error
96
96
 
97
97
 
98
+ def filter_log_timestamp(log: str, include_timestamps: bool) -> str:
99
+ if include_timestamps:
100
+ return log
101
+ else:
102
+ return log.split(" ", 1)[1] if " " in log else log
103
+
104
+
105
+ def new_logs_only(prev_log_records: list[str], new_log_records: list[str]) -> list[str]:
106
+ # Sort the log records, we get time-ordered logs
107
+ # due to ISO 8601 timestamp format in the log content
108
+ # eg: 2024-10-22T01:12:29.873896187Z Count: 1
109
+ new_log_records_sorted = sorted(new_log_records)
110
+
111
+ # Get the first new log record to establish the overlap point
112
+ first_new_log_record = new_log_records_sorted[0]
113
+
114
+ # Traverse previous logs in reverse and remove duplicates from new logs
115
+ for prev_log in reversed(prev_log_records):
116
+ # Stop if the previous log is earlier than the first new log
117
+ if prev_log < first_new_log_record:
118
+ break
119
+
120
+ # Remove matching previous logs from the new logs list
121
+ if prev_log in new_log_records_sorted:
122
+ new_log_records_sorted.remove(prev_log)
123
+
124
+ return new_log_records_sorted
125
+
126
+
98
127
  class NoPropertiesProvidedError(ClickException):
99
128
  pass
@@ -56,18 +56,18 @@ class ComputePoolManager(SqlExecutionMixin):
56
56
  query.append(f"COMMENT = {comment}")
57
57
 
58
58
  try:
59
- return self._execute_query(strip_empty_lines(query))
59
+ return self.execute_query(strip_empty_lines(query))
60
60
  except ProgrammingError as e:
61
61
  handle_object_already_exists(e, ObjectType.COMPUTE_POOL, pool_name)
62
62
 
63
63
  def stop(self, pool_name: str) -> SnowflakeCursor:
64
- return self._execute_query(f"alter compute pool {pool_name} stop all")
64
+ return self.execute_query(f"alter compute pool {pool_name} stop all")
65
65
 
66
66
  def suspend(self, pool_name: str) -> SnowflakeCursor:
67
- return self._execute_query(f"alter compute pool {pool_name} suspend")
67
+ return self.execute_query(f"alter compute pool {pool_name} suspend")
68
68
 
69
69
  def resume(self, pool_name: str) -> SnowflakeCursor:
70
- return self._execute_query(f"alter compute pool {pool_name} resume")
70
+ return self.execute_query(f"alter compute pool {pool_name} resume")
71
71
 
72
72
  def set_property(
73
73
  self,
@@ -95,7 +95,7 @@ class ComputePoolManager(SqlExecutionMixin):
95
95
  for property_name, value in property_pairs:
96
96
  if value is not None:
97
97
  query.append(f"{property_name} = {value}")
98
- return self._execute_query(strip_empty_lines(query))
98
+ return self.execute_query(strip_empty_lines(query))
99
99
 
100
100
  def unset_property(
101
101
  self, pool_name: str, auto_resume: bool, auto_suspend_secs: bool, comment: bool
@@ -113,9 +113,7 @@ class ComputePoolManager(SqlExecutionMixin):
113
113
  )
114
114
  unset_list = [property_name for property_name, value in property_pairs if value]
115
115
  query = f"alter compute pool {pool_name} unset {','.join(unset_list)}"
116
- return self._execute_query(query)
116
+ return self.execute_query(query)
117
117
 
118
118
  def status(self, pool_name: str):
119
- return self._execute_query(
120
- f"call system$get_compute_pool_status('{pool_name}')"
121
- )
119
+ return self.execute_query(f"call system$get_compute_pool_status('{pool_name}')")
@@ -36,7 +36,7 @@ class RegistryManager(SqlExecutionMixin):
36
36
  """
37
37
  Get token to authenticate with registry.
38
38
  """
39
- self._execute_query(
39
+ self.execute_query(
40
40
  "alter session set PYTHON_CONNECTOR_QUERY_RESULT_FORMAT = 'json'"
41
41
  ).fetchall()
42
42
  # disable session deletion
@@ -75,7 +75,7 @@ class RegistryManager(SqlExecutionMixin):
75
75
 
76
76
  def get_registry_url(self) -> str:
77
77
  repositories_query = "show image repositories in account"
78
- result_set = self._execute_query(repositories_query, cursor_class=DictCursor)
78
+ result_set = self.execute_query(repositories_query, cursor_class=DictCursor)
79
79
  results = result_set.fetchall()
80
80
  if len(results) == 0:
81
81
  raise NoImageRepositoriesFoundError()
@@ -84,4 +84,4 @@ class ImageRepositoryManager(SqlExecutionMixin):
84
84
  )
85
85
 
86
86
  def list_images(self, repo_name: str) -> SnowflakeCursor:
87
- return self._execute_query(f"show images in image repository {repo_name}")
87
+ return self.execute_query(f"show images in image repository {repo_name}")
@@ -14,9 +14,9 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import sys
17
+ import itertools
18
18
  from pathlib import Path
19
- from typing import List, Optional
19
+ from typing import Generator, Iterable, List, Optional, cast
20
20
 
21
21
  import typer
22
22
  from click import ClickException
@@ -26,7 +26,6 @@ from snowflake.cli._plugins.object.command_aliases import (
26
26
  )
27
27
  from snowflake.cli._plugins.object.common import CommentOption, Tag, TagOption
28
28
  from snowflake.cli._plugins.spcs.common import (
29
- print_log_lines,
30
29
  validate_and_set_instances,
31
30
  )
32
31
  from snowflake.cli._plugins.spcs.services.manager import ServiceManager
@@ -38,12 +37,15 @@ from snowflake.cli.api.commands.flags import (
38
37
  )
39
38
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
40
39
  from snowflake.cli.api.constants import ObjectType
40
+ from snowflake.cli.api.exceptions import IncompatibleParametersError
41
41
  from snowflake.cli.api.identifiers import FQN
42
42
  from snowflake.cli.api.output.types import (
43
43
  CommandResult,
44
+ MessageResult,
44
45
  QueryJsonValueResult,
45
46
  QueryResult,
46
47
  SingleQueryResult,
48
+ StreamResult,
47
49
  )
48
50
  from snowflake.cli.api.project.util import is_valid_object_name
49
51
 
@@ -77,6 +79,7 @@ SpecPathOption = typer.Option(
77
79
  exists=True,
78
80
  show_default=False,
79
81
  )
82
+ DEFAULT_NUM_LINES = 500
80
83
 
81
84
  _MIN_INSTANCES_HELP = "Minimum number of service instances to run."
82
85
  MinInstancesOption = OverrideableOption(
@@ -219,22 +222,70 @@ def logs(
219
222
  show_default=False,
220
223
  ),
221
224
  num_lines: int = typer.Option(
222
- 500, "--num-lines", help="Number of lines to retrieve."
225
+ DEFAULT_NUM_LINES, "--num-lines", help="Number of lines to retrieve."
226
+ ),
227
+ previous_logs: bool = typer.Option(
228
+ False,
229
+ "--previous-logs",
230
+ help="Retrieve logs from the last terminated container.",
231
+ is_flag=True,
232
+ ),
233
+ since_timestamp: Optional[str] = typer.Option(
234
+ "", "--since", help="Start log retrieval from a specified UTC timestamp."
235
+ ),
236
+ include_timestamps: bool = typer.Option(
237
+ False, "--include-timestamps", help="Include timestamps in logs.", is_flag=True
238
+ ),
239
+ follow: bool = typer.Option(
240
+ False, "--follow", help="Stream logs in real-time.", is_flag=True
241
+ ),
242
+ follow_interval: int = typer.Option(
243
+ 2,
244
+ "--follow-interval",
245
+ help="Set custom polling intervals for log streaming (--follow flag) in seconds.",
223
246
  ),
224
247
  **options,
225
248
  ):
226
249
  """
227
250
  Retrieves local logs from a service container.
228
251
  """
229
- results = ServiceManager().logs(
230
- service_name=name.identifier,
231
- instance_id=instance_id,
232
- container_name=container_name,
233
- num_lines=num_lines,
234
- )
235
- cursor = results.fetchone()
236
- logs = next(iter(cursor)).split("\n")
237
- print_log_lines(sys.stdout, name, "0", logs)
252
+ if follow:
253
+ if num_lines != DEFAULT_NUM_LINES:
254
+ raise IncompatibleParametersError(["--follow", "--num-lines"])
255
+ if previous_logs:
256
+ raise IncompatibleParametersError(["--follow", "--previous-logs"])
257
+
258
+ manager = ServiceManager()
259
+
260
+ if follow:
261
+ stream: Iterable[CommandResult] = (
262
+ MessageResult(log_batch)
263
+ for log_batch in manager.stream_logs(
264
+ service_name=name.identifier,
265
+ container_name=container_name,
266
+ instance_id=instance_id,
267
+ num_lines=num_lines,
268
+ since_timestamp=since_timestamp,
269
+ include_timestamps=include_timestamps,
270
+ interval_seconds=follow_interval,
271
+ )
272
+ )
273
+ stream = itertools.chain(stream, [MessageResult("")])
274
+ else:
275
+ stream = (
276
+ MessageResult(log)
277
+ for log in manager.logs(
278
+ service_name=name.identifier,
279
+ container_name=container_name,
280
+ instance_id=instance_id,
281
+ num_lines=num_lines,
282
+ previous_logs=previous_logs,
283
+ since_timestamp=since_timestamp,
284
+ include_timestamps=include_timestamps,
285
+ )
286
+ )
287
+
288
+ return StreamResult(cast(Generator[CommandResult, None, None], stream))
238
289
 
239
290
 
240
291
  @app.command(requires_connection=True)
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
+ import time
18
19
  from pathlib import Path
19
20
  from typing import List, Optional
20
21
 
@@ -22,7 +23,9 @@ import yaml
22
23
  from snowflake.cli._plugins.object.common import Tag
23
24
  from snowflake.cli._plugins.spcs.common import (
24
25
  NoPropertiesProvidedError,
26
+ filter_log_timestamp,
25
27
  handle_object_already_exists,
28
+ new_logs_only,
26
29
  strip_empty_lines,
27
30
  )
28
31
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, ObjectType
@@ -81,7 +84,7 @@ class ServiceManager(SqlExecutionMixin):
81
84
  query.append(f"WITH TAG ({tag_list})")
82
85
 
83
86
  try:
84
- return self._execute_query(strip_empty_lines(query))
87
+ return self.execute_query(strip_empty_lines(query))
85
88
  except ProgrammingError as e:
86
89
  handle_object_already_exists(e, ObjectType.SERVICE, service_name)
87
90
 
@@ -119,7 +122,7 @@ class ServiceManager(SqlExecutionMixin):
119
122
  query.append(f"COMMENT = {comment}")
120
123
 
121
124
  try:
122
- return self._execute_query(strip_empty_lines(query))
125
+ return self.execute_query(strip_empty_lines(query))
123
126
  except ProgrammingError as e:
124
127
  handle_object_already_exists(e, ObjectType.SERVICE, job_service_name)
125
128
 
@@ -130,37 +133,94 @@ class ServiceManager(SqlExecutionMixin):
130
133
  return json.dumps(data)
131
134
 
132
135
  def status(self, service_name: str) -> SnowflakeCursor:
133
- return self._execute_query(f"CALL SYSTEM$GET_SERVICE_STATUS('{service_name}')")
136
+ return self.execute_query(f"CALL SYSTEM$GET_SERVICE_STATUS('{service_name}')")
134
137
 
135
138
  def logs(
136
- self, service_name: str, instance_id: str, container_name: str, num_lines: int
139
+ self,
140
+ service_name: str,
141
+ instance_id: str,
142
+ container_name: str,
143
+ num_lines: int,
144
+ previous_logs: bool = False,
145
+ since_timestamp: str = "",
146
+ include_timestamps: bool = False,
137
147
  ):
138
- return self._execute_query(
139
- f"call SYSTEM$GET_SERVICE_LOGS('{service_name}', '{instance_id}', '{container_name}', {num_lines});"
148
+ cursor = self.execute_query(
149
+ f"call SYSTEM$GET_SERVICE_LOGS('{service_name}', '{instance_id}', '{container_name}', "
150
+ f"{num_lines}, {previous_logs}, '{since_timestamp}', {include_timestamps});"
140
151
  )
141
152
 
153
+ for log in cursor.fetchall():
154
+ yield log[0] if isinstance(log, tuple) else log
155
+
156
+ def stream_logs(
157
+ self,
158
+ service_name: str,
159
+ instance_id: str,
160
+ container_name: str,
161
+ num_lines: int,
162
+ since_timestamp: str,
163
+ include_timestamps: bool,
164
+ interval_seconds: int,
165
+ ):
166
+ try:
167
+ prev_timestamp = since_timestamp
168
+ prev_log_records: List[str] = []
169
+
170
+ while True:
171
+ raw_log_blocks = [
172
+ log
173
+ for log in self.logs(
174
+ service_name=service_name,
175
+ instance_id=instance_id,
176
+ container_name=container_name,
177
+ num_lines=num_lines,
178
+ since_timestamp=prev_timestamp,
179
+ include_timestamps=True,
180
+ )
181
+ ]
182
+
183
+ new_log_records = []
184
+ for block in raw_log_blocks:
185
+ new_log_records.extend(block.split("\n"))
186
+
187
+ new_log_records = [line for line in new_log_records if line.strip()]
188
+
189
+ if new_log_records:
190
+ dedup_log_records = new_logs_only(prev_log_records, new_log_records)
191
+ for log in dedup_log_records:
192
+ yield filter_log_timestamp(log, include_timestamps)
193
+
194
+ prev_timestamp = dedup_log_records[-1].split(" ", 1)[0]
195
+ prev_log_records = dedup_log_records
196
+
197
+ time.sleep(interval_seconds)
198
+
199
+ except KeyboardInterrupt:
200
+ return
201
+
142
202
  def upgrade_spec(self, service_name: str, spec_path: Path):
143
203
  spec = self._read_yaml(spec_path)
144
204
  query = f"alter service {service_name} from specification $$ {spec} $$"
145
- return self._execute_query(query)
205
+ return self.execute_query(query)
146
206
 
147
207
  def list_endpoints(self, service_name: str) -> SnowflakeCursor:
148
- return self._execute_query(f"show endpoints in service {service_name}")
208
+ return self.execute_query(f"show endpoints in service {service_name}")
149
209
 
150
210
  def list_instances(self, service_name: str) -> SnowflakeCursor:
151
- return self._execute_query(f"show service instances in service {service_name}")
211
+ return self.execute_query(f"show service instances in service {service_name}")
152
212
 
153
213
  def list_containers(self, service_name: str) -> SnowflakeCursor:
154
- return self._execute_query(f"show service containers in service {service_name}")
214
+ return self.execute_query(f"show service containers in service {service_name}")
155
215
 
156
216
  def list_roles(self, service_name: str) -> SnowflakeCursor:
157
- return self._execute_query(f"show roles in service {service_name}")
217
+ return self.execute_query(f"show roles in service {service_name}")
158
218
 
159
219
  def suspend(self, service_name: str):
160
- return self._execute_query(f"alter service {service_name} suspend")
220
+ return self.execute_query(f"alter service {service_name} suspend")
161
221
 
162
222
  def resume(self, service_name: str):
163
- return self._execute_query(f"alter service {service_name} resume")
223
+ return self.execute_query(f"alter service {service_name} resume")
164
224
 
165
225
  def set_property(
166
226
  self,
@@ -211,7 +271,7 @@ class ServiceManager(SqlExecutionMixin):
211
271
  if comment is not None:
212
272
  query.append(f" comment = {comment}")
213
273
 
214
- return self._execute_query(strip_empty_lines(query))
274
+ return self.execute_query(strip_empty_lines(query))
215
275
 
216
276
  def unset_property(
217
277
  self,
@@ -237,4 +297,4 @@ class ServiceManager(SqlExecutionMixin):
237
297
  )
238
298
  unset_list = [property_name for property_name, value in property_pairs if value]
239
299
  query = f"alter service {service_name} unset {','.join(unset_list)}"
240
- return self._execute_query(query)
300
+ return self.execute_query(query)
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  from pathlib import Path
18
18
  from typing import List, Optional
19
19
 
20
+ import typer
20
21
  from snowflake.cli._plugins.sql.manager import SqlManager
21
22
  from snowflake.cli.api.commands.decorators import with_project_definition
22
23
  from snowflake.cli.api.commands.flags import (
@@ -63,6 +64,11 @@ def execute_sql(
63
64
  "String in format of key=value. If provided the SQL content will "
64
65
  "be treated as template and rendered using provided data.",
65
66
  ),
67
+ retain_comments: Optional[bool] = typer.Option(
68
+ False,
69
+ "--retain-comments",
70
+ help="Retains comments in queries passed to Snowflake",
71
+ ),
66
72
  **options,
67
73
  ) -> CommandResult:
68
74
  """
@@ -80,7 +86,9 @@ def execute_sql(
80
86
  if data_override:
81
87
  data = {v.key: v.value for v in parse_key_value_variables(data_override)}
82
88
 
83
- single_statement, cursors = SqlManager().execute(query, files, std_in, data=data)
89
+ single_statement, cursors = SqlManager().execute(
90
+ query, files, std_in, data=data, retain_comments=retain_comments
91
+ )
84
92
  if single_statement:
85
93
  return QueryResult(next(cursors))
86
94
  return MultipleResults((QueryResult(c) for c in cursors))
@@ -39,6 +39,7 @@ class SqlManager(SqlExecutionMixin):
39
39
  files: List[Path] | None,
40
40
  std_in: bool,
41
41
  data: Dict | None = None,
42
+ retain_comments: bool = False,
42
43
  ) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
43
44
  inputs = [query, files, std_in]
44
45
  # Check if any two inputs were provided simultaneously
@@ -50,7 +51,9 @@ class SqlManager(SqlExecutionMixin):
50
51
  if std_in:
51
52
  query = sys.stdin.read()
52
53
  if query:
53
- return self._execute_single_query(query=query, data=data)
54
+ return self._execute_single_query(
55
+ query=query, data=data, retain_comments=retain_comments
56
+ )
54
57
 
55
58
  if files:
56
59
  # Multiple files
@@ -61,7 +64,7 @@ class SqlManager(SqlExecutionMixin):
61
64
  file_size_limit_mb=UNLIMITED
62
65
  )
63
66
  single_statement, result = self._execute_single_query(
64
- query=query_from_file, data=data
67
+ query=query_from_file, data=data, retain_comments=retain_comments
65
68
  )
66
69
  results.append(result)
67
70
 
@@ -73,7 +76,7 @@ class SqlManager(SqlExecutionMixin):
73
76
  raise UsageError("Use either query, filename or input option.")
74
77
 
75
78
  def _execute_single_query(
76
- self, query: str, data: Dict | None = None
79
+ self, query: str, data: Dict | None = None, retain_comments: bool = False
77
80
  ) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
78
81
  try:
79
82
  query = transpile_snowsql_templates(query)
@@ -83,7 +86,9 @@ class SqlManager(SqlExecutionMixin):
83
86
 
84
87
  statements = tuple(
85
88
  statement
86
- for statement, _ in split_statements(StringIO(query), remove_comments=True)
89
+ for statement, _ in split_statements(
90
+ StringIO(query), remove_comments=not retain_comments
91
+ )
87
92
  )
88
93
  single_statement = len(statements) == 1
89
94
 
@@ -141,7 +141,7 @@ def copy(
141
141
  )
142
142
  return _put(
143
143
  recursive=recursive,
144
- source_path=source_path,
144
+ source_path=Path(source_path),
145
145
  destination_path=destination_path,
146
146
  parallel=parallel,
147
147
  overwrite=overwrite,
@@ -247,23 +247,27 @@ def get(recursive: bool, source_path: str, destination_path: str, parallel: int)
247
247
 
248
248
  def _put(
249
249
  recursive: bool,
250
- source_path: str,
250
+ source_path: Path,
251
251
  destination_path: str,
252
252
  parallel: int,
253
253
  overwrite: bool,
254
254
  auto_compress: bool,
255
255
  ):
256
- if recursive:
257
- raise click.ClickException("Recursive flag for upload is not supported.")
258
-
259
- source = Path(source_path).resolve()
260
- local_path = str(source) + "/*" if source.is_dir() else str(source)
261
-
262
- cursor = StageManager().put(
263
- local_path=local_path,
264
- stage_path=destination_path,
265
- overwrite=overwrite,
266
- parallel=parallel,
267
- auto_compress=auto_compress,
268
- )
269
- return QueryResult(cursor)
256
+ if recursive and not source_path.is_file():
257
+ cursor_generator = StageManager().put_recursive(
258
+ local_path=source_path,
259
+ stage_path=destination_path,
260
+ overwrite=overwrite,
261
+ parallel=parallel,
262
+ auto_compress=auto_compress,
263
+ )
264
+ return CollectionResult(cursor_generator)
265
+ else:
266
+ cursor = StageManager().put(
267
+ local_path=source_path.resolve(),
268
+ stage_path=destination_path,
269
+ overwrite=overwrite,
270
+ parallel=parallel,
271
+ auto_compress=auto_compress,
272
+ )
273
+ return QueryResult(cursor)
@@ -221,7 +221,7 @@ def put_files_on_stage(
221
221
 
222
222
 
223
223
  def sync_local_diff_with_stage(
224
- role: str, deploy_root_path: Path, diff_result: DiffResult, stage_fqn: str
224
+ role: str | None, deploy_root_path: Path, diff_result: DiffResult, stage_fqn: str
225
225
  ):
226
226
  """
227
227
  Syncs a given local directory's contents with a Snowflake stage, including removing old files, and re-uploading modified and new files.