snowflake-cli 3.13.1__py3-none-any.whl → 3.15.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 (32) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/project_definition_generate_json_schema.py +2 -2
  3. snowflake/cli/_app/printing.py +14 -12
  4. snowflake/cli/_app/snow_connector.py +59 -9
  5. snowflake/cli/_plugins/dbt/commands.py +37 -7
  6. snowflake/cli/_plugins/dbt/manager.py +81 -53
  7. snowflake/cli/_plugins/dcm/commands.py +94 -4
  8. snowflake/cli/_plugins/dcm/manager.py +87 -33
  9. snowflake/cli/_plugins/dcm/reporters.py +462 -0
  10. snowflake/cli/_plugins/dcm/styles.py +26 -0
  11. snowflake/cli/_plugins/dcm/utils.py +88 -0
  12. snowflake/cli/_plugins/git/manager.py +24 -22
  13. snowflake/cli/_plugins/object/command_aliases.py +7 -1
  14. snowflake/cli/_plugins/object/commands.py +12 -2
  15. snowflake/cli/_plugins/object/manager.py +7 -2
  16. snowflake/cli/_plugins/snowpark/commands.py +8 -1
  17. snowflake/cli/_plugins/snowpark/package/commands.py +1 -1
  18. snowflake/cli/_plugins/streamlit/commands.py +23 -4
  19. snowflake/cli/_plugins/streamlit/streamlit_entity.py +89 -46
  20. snowflake/cli/api/commands/decorators.py +1 -1
  21. snowflake/cli/api/commands/flags.py +30 -5
  22. snowflake/cli/api/console/abc.py +7 -3
  23. snowflake/cli/api/console/console.py +14 -2
  24. snowflake/cli/api/exceptions.py +1 -1
  25. snowflake/cli/api/feature_flags.py +1 -3
  26. snowflake/cli/api/output/types.py +6 -0
  27. snowflake/cli/api/utils/types.py +20 -1
  28. {snowflake_cli-3.13.1.dist-info → snowflake_cli-3.15.0.dist-info}/METADATA +10 -5
  29. {snowflake_cli-3.13.1.dist-info → snowflake_cli-3.15.0.dist-info}/RECORD +32 -29
  30. {snowflake_cli-3.13.1.dist-info → snowflake_cli-3.15.0.dist-info}/WHEEL +1 -1
  31. {snowflake_cli-3.13.1.dist-info → snowflake_cli-3.15.0.dist-info}/entry_points.txt +0 -0
  32. {snowflake_cli-3.13.1.dist-info → snowflake_cli-3.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from enum import Enum, unique
18
18
 
19
- VERSION = "3.13.1"
19
+ VERSION = "3.15.0"
20
20
 
21
21
 
22
22
  @unique
@@ -38,8 +38,8 @@ class ProjectDefinitionProperty:
38
38
 
39
39
 
40
40
  class ProjectDefinitionGenerateJsonSchema(GenerateJsonSchema):
41
- def __init__(self, by_alias: bool = False, ref_template: str = ""):
42
- super().__init__(by_alias, "{model}")
41
+ def __init__(self, by_alias: bool = False, ref_template: str = "", **kwargs):
42
+ super().__init__(by_alias, "{model}", **kwargs)
43
43
  self._remapped_definitions: Dict[str, Any] = {}
44
44
 
45
45
  def generate(self, schema, mode="validation"):
@@ -33,6 +33,7 @@ from snowflake.cli.api.output.formats import OutputFormat
33
33
  from snowflake.cli.api.output.types import (
34
34
  CollectionResult,
35
35
  CommandResult,
36
+ EmptyResult,
36
37
  MessageResult,
37
38
  MultipleResults,
38
39
  ObjectResult,
@@ -341,15 +342,16 @@ def _print_single_table(obj):
341
342
 
342
343
  def print_result(cmd_result: CommandResult, output_format: OutputFormat | None = None):
343
344
  output_format = output_format or _get_format_type()
344
- if is_structured_format(output_format):
345
- print_structured(cmd_result, output_format)
346
- elif isinstance(cmd_result, (MultipleResults, StreamResult)):
347
- for res in cmd_result.result:
348
- print_result(res)
349
- elif (
350
- isinstance(cmd_result, (MessageResult, ObjectResult, CollectionResult))
351
- or cmd_result is None
352
- ):
353
- print_unstructured(cmd_result)
354
- else:
355
- raise ValueError(f"Unexpected type {type(cmd_result)}")
345
+
346
+ match cmd_result:
347
+ case EmptyResult():
348
+ return
349
+ case _ if is_structured_format(output_format):
350
+ print_structured(cmd_result, output_format)
351
+ case MultipleResults() | StreamResult():
352
+ for res in cmd_result.result:
353
+ print_result(res)
354
+ case MessageResult() | ObjectResult() | CollectionResult() | None:
355
+ print_unstructured(cmd_result)
356
+ case _:
357
+ raise ValueError(f"Unexpected type {type(cmd_result)}")
@@ -15,9 +15,11 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import contextlib
18
+ import io
18
19
  import logging
19
20
  import os
20
- from typing import Dict, Optional
21
+ import sys
22
+ from typing import Dict, Literal, Optional, TextIO
21
23
 
22
24
  import snowflake.connector
23
25
  from click.exceptions import ClickException
@@ -29,9 +31,7 @@ from snowflake.cli._app.constants import (
29
31
  PARAM_APPLICATION_NAME,
30
32
  )
31
33
  from snowflake.cli._app.telemetry import command_info
32
- from snowflake.cli._plugins.auth.oidc.manager import (
33
- OidcManager,
34
- )
34
+ from snowflake.cli._plugins.auth.oidc.manager import OidcManager
35
35
  from snowflake.cli.api.config import (
36
36
  get_connection_dict,
37
37
  get_env_value,
@@ -53,6 +53,8 @@ log = logging.getLogger(__name__)
53
53
 
54
54
  ENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
55
55
  UNENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN PRIVATE KEY-----"
56
+ AUTHENTICATOR_EXTERNAL_BROWSER: Literal["externalbrowser"] = "externalbrowser"
57
+ AUTHENTICATOR_PARAM: Literal["authenticator"] = "authenticator"
56
58
 
57
59
  # connection keys that can be set using SNOWFLAKE_* env vars
58
60
  SUPPORTED_ENV_OVERRIDES = [
@@ -88,6 +90,32 @@ SUPPORTED_ENV_OVERRIDES = [
88
90
  CONNECTION_KEY_ALIASES = {"private_key_path": "private_key_file"}
89
91
 
90
92
 
93
+ class _BufferedMirrorStream(io.StringIO):
94
+ """Buffer connector chatter while optionally mirroring to another stream."""
95
+
96
+ def __init__(self, mirror: Optional[TextIO] = None) -> None:
97
+ super().__init__()
98
+ self._mirror = mirror
99
+
100
+ def write(self, text: str) -> int:
101
+ text = text or ""
102
+ written = super().write(text)
103
+ if self._mirror is not None:
104
+ self._mirror.write(text)
105
+ self._mirror.flush()
106
+ return written
107
+
108
+ def flush(self) -> None:
109
+ super().flush()
110
+ if self._mirror is not None:
111
+ self._mirror.flush()
112
+
113
+ def isatty(self) -> bool:
114
+ if self._mirror is not None and hasattr(self._mirror, "isatty"):
115
+ return bool(self._mirror.isatty())
116
+ return False
117
+
118
+
91
119
  def _resolve_alias(key_or_alias: str):
92
120
  """
93
121
  Given the key of an override / env var, what key should it be set as in the connection parameters?
@@ -95,6 +123,24 @@ def _resolve_alias(key_or_alias: str):
95
123
  return CONNECTION_KEY_ALIASES.get(key_or_alias, key_or_alias)
96
124
 
97
125
 
126
+ def _build_silent_streams(
127
+ connection_parameters: Dict,
128
+ ) -> tuple[_BufferedMirrorStream, _BufferedMirrorStream]:
129
+ """
130
+ Build stdout/stderr silent streams.
131
+
132
+ We must provide a writable stdout for authenticators (notably externalbrowser)
133
+ that prompt via input(); redirecting to None breaks sys.stdout.
134
+ For externalbrowser we mirror stdout to stderr so prompts remain visible
135
+ without polluting structured stdout (e.g., JSON output).
136
+ """
137
+ authenticator = str(connection_parameters.get(AUTHENTICATOR_PARAM, "")).lower()
138
+ mirror_stdout: Optional[TextIO] = (
139
+ sys.stderr if authenticator == AUTHENTICATOR_EXTERNAL_BROWSER else None
140
+ )
141
+ return _BufferedMirrorStream(mirror_stdout), _BufferedMirrorStream()
142
+
143
+
98
144
  def connect_to_snowflake(
99
145
  temporary_connection: bool = False,
100
146
  mfa_passcode: Optional[str] = None,
@@ -186,12 +232,16 @@ def connect_to_snowflake(
186
232
 
187
233
  _update_internal_application_info(connection_parameters)
188
234
 
235
+ silent_stdout, silent_stderr = _build_silent_streams(connection_parameters)
236
+
189
237
  try:
190
- # Whatever output is generated when creating connection,
191
- # we don't want it in our output. This is particularly important
192
- # for cases when external browser and json format are used.
193
- # Redirecting both stdout and stderr for offline usage.
194
- with contextlib.redirect_stdout(None), contextlib.redirect_stderr(None):
238
+ # The output is redirected to silent stream for reuse
239
+ # in cases like externalbrowser auth not to pollute output
240
+ # in formats like JSON
241
+ with (
242
+ contextlib.redirect_stdout(silent_stdout),
243
+ contextlib.redirect_stderr(silent_stderr),
244
+ ):
195
245
  return snowflake.connector.connect(
196
246
  application=command_info(),
197
247
  **connection_parameters,
@@ -25,7 +25,11 @@ from snowflake.cli._plugins.dbt.constants import (
25
25
  PROFILES_FILENAME,
26
26
  RESULT_COLUMN_NAME,
27
27
  )
28
- from snowflake.cli._plugins.dbt.manager import DBTManager
28
+ from snowflake.cli._plugins.dbt.manager import (
29
+ DBTDeployAttributes,
30
+ DBTManager,
31
+ SemanticVersionType,
32
+ )
29
33
  from snowflake.cli._plugins.object.command_aliases import add_object_command_aliases
30
34
  from snowflake.cli._plugins.object.commands import scope_option
31
35
  from snowflake.cli.api.commands.decorators import global_options_with_connection
@@ -35,6 +39,7 @@ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
35
39
  from snowflake.cli.api.console.console import cli_console
36
40
  from snowflake.cli.api.constants import ObjectType
37
41
  from snowflake.cli.api.exceptions import CliError
42
+ from snowflake.cli.api.feature_flags import FeatureFlag
38
43
  from snowflake.cli.api.identifiers import FQN
39
44
  from snowflake.cli.api.output.types import (
40
45
  CommandResult,
@@ -118,6 +123,14 @@ def deploy_dbt(
118
123
  show_default=False,
119
124
  help="Installs local dependencies from project that don't require external access.",
120
125
  ),
126
+ dbt_version: Optional[str] = typer.Option(
127
+ None,
128
+ "--dbt-version",
129
+ click_type=SemanticVersionType(),
130
+ show_default=False,
131
+ hidden=not FeatureFlag.ENABLE_DBT_VERSION.is_enabled(),
132
+ help="dbt version to use for the project, for example '1.9.4'.",
133
+ ),
121
134
  **options,
122
135
  ) -> CommandResult:
123
136
  """
@@ -127,18 +140,24 @@ def deploy_dbt(
127
140
  snow dbt deploy PROJECT
128
141
  snow dbt deploy PROJECT --source=/Users/jdoe/project --force
129
142
  """
143
+ if not FeatureFlag.ENABLE_DBT_VERSION.is_enabled():
144
+ dbt_version = None
130
145
  project_path = SecurePath(source) if source is not None else SecurePath.cwd()
131
146
  profiles_dir_path = SecurePath(profiles_dir) if profiles_dir else project_path
147
+ attrs = DBTDeployAttributes(
148
+ default_target=default_target,
149
+ unset_default_target=unset_default_target,
150
+ external_access_integrations=external_access_integrations,
151
+ install_local_deps=install_local_deps,
152
+ dbt_version=dbt_version,
153
+ )
132
154
  return QueryResult(
133
155
  DBTManager().deploy(
134
156
  name,
135
157
  path=project_path.resolve(),
136
158
  profiles_path=profiles_dir_path.resolve(),
137
159
  force=force,
138
- default_target=default_target,
139
- unset_default_target=unset_default_target,
140
- external_access_integrations=external_access_integrations,
141
- install_local_deps=install_local_deps,
160
+ attrs=attrs,
142
161
  )
143
162
  )
144
163
 
@@ -159,9 +178,17 @@ def before_callback(
159
178
  run_async: Optional[bool] = typer.Option(
160
179
  False, help="Run dbt command asynchronously and check it's result later."
161
180
  ),
181
+ dbt_version: Optional[str] = typer.Option(
182
+ None,
183
+ "--dbt-version",
184
+ click_type=SemanticVersionType(),
185
+ show_default=False,
186
+ hidden=not FeatureFlag.ENABLE_DBT_VERSION.is_enabled(),
187
+ help="dbt version to use for execution (ephemeral, does not change project configuration).",
188
+ ),
162
189
  **options,
163
190
  ):
164
- """Handles global options passed before the command and takes pipeline name to be accessed through child context later"""
191
+ """Handles global options passed before the command and takes pipeline name to be accessed through child context later."""
165
192
  pass
166
193
 
167
194
 
@@ -182,7 +209,10 @@ for cmd in DBT_COMMANDS:
182
209
  dbt_command = ctx.command.name
183
210
  name = FQN.from_string(ctx.parent.params["name"])
184
211
  run_async = ctx.parent.params["run_async"]
185
- execute_args = (dbt_command, name, run_async, *dbt_cli_args)
212
+ dbt_version = ctx.parent.params.get("dbt_version")
213
+ if not FeatureFlag.ENABLE_DBT_VERSION.is_enabled():
214
+ dbt_version = None
215
+ execute_args = (dbt_command, name, run_async, dbt_version, *dbt_cli_args)
186
216
  dbt_manager = DBTManager()
187
217
 
188
218
  if run_async is True:
@@ -14,11 +14,14 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import re
17
18
  from collections import defaultdict
19
+ from dataclasses import dataclass
18
20
  from pathlib import Path
19
21
  from tempfile import TemporaryDirectory
20
22
  from typing import Dict, List, Optional, TypedDict
21
23
 
24
+ import click
22
25
  import yaml
23
26
  from snowflake.cli._plugins.dbt.constants import PROFILES_FILENAME
24
27
  from snowflake.cli._plugins.object.manager import ObjectManager
@@ -32,10 +35,43 @@ from snowflake.cli.api.sql_execution import SqlExecutionMixin
32
35
  from snowflake.connector.cursor import SnowflakeCursor
33
36
  from snowflake.connector.errors import ProgrammingError
34
37
 
38
+ SEMANTIC_VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
39
+
40
+
41
+ class SemanticVersionType(click.ParamType):
42
+ """Custom Click type that validates semantic version format (major.minor.patch)."""
43
+
44
+ name = "VERSION"
45
+
46
+ def convert(self, value, param, ctx):
47
+ if value is None:
48
+ return None
49
+ if not isinstance(value, str):
50
+ self.fail(f"Expected string, got {type(value).__name__}.", param, ctx)
51
+ if not SEMANTIC_VERSION_PATTERN.match(value):
52
+ self.fail(
53
+ f"Invalid version format '{value}'. Expected format: major.minor.patch (e.g., '1.9.4').",
54
+ param,
55
+ ctx,
56
+ )
57
+ return value
58
+
35
59
 
36
60
  class DBTObjectEditableAttributes(TypedDict):
37
61
  default_target: Optional[str]
38
62
  external_access_integrations: Optional[List[str]]
63
+ dbt_version: Optional[str]
64
+
65
+
66
+ @dataclass
67
+ class DBTDeployAttributes:
68
+ """Attributes for deploying a DBT project."""
69
+
70
+ default_target: Optional[str] = None
71
+ unset_default_target: bool = False
72
+ external_access_integrations: Optional[List[str]] = None
73
+ install_local_deps: bool = False
74
+ dbt_version: Optional[str] = None
39
75
 
40
76
 
41
77
  class DBTManager(SqlExecutionMixin):
@@ -90,6 +126,7 @@ class DBTManager(SqlExecutionMixin):
90
126
  return DBTObjectEditableAttributes(
91
127
  default_target=row_dict.get("default_target"),
92
128
  external_access_integrations=external_access_integrations,
129
+ dbt_version=row_dict.get("dbt_version"),
93
130
  )
94
131
 
95
132
  def deploy(
@@ -98,10 +135,7 @@ class DBTManager(SqlExecutionMixin):
98
135
  path: SecurePath,
99
136
  profiles_path: SecurePath,
100
137
  force: bool,
101
- default_target: Optional[str] = None,
102
- unset_default_target: bool = False,
103
- external_access_integrations: Optional[List[str]] = None,
104
- install_local_deps: bool = False,
138
+ attrs: DBTDeployAttributes,
105
139
  ) -> SnowflakeCursor:
106
140
  dbt_project_path = path / "dbt_project.yml"
107
141
  if not dbt_project_path.exists():
@@ -116,7 +150,7 @@ class DBTManager(SqlExecutionMixin):
116
150
  except KeyError:
117
151
  raise CliError("`profile` is not defined in dbt_project.yml")
118
152
 
119
- self._validate_profiles(profiles_path, profile, default_target)
153
+ self._validate_profiles(profiles_path, profile, attrs.default_target)
120
154
 
121
155
  with cli_console.phase("Creating temporary stage"):
122
156
  stage_manager = StageManager()
@@ -140,43 +174,22 @@ class DBTManager(SqlExecutionMixin):
140
174
 
141
175
  with cli_console.phase("Creating DBT project"):
142
176
  if force is True:
143
- return self._deploy_create_or_replace(
144
- fqn,
145
- stage_name,
146
- default_target,
147
- external_access_integrations,
148
- install_local_deps,
149
- )
177
+ return self._deploy_create_or_replace(fqn, stage_name, attrs)
150
178
  else:
151
179
  dbt_object_attributes = self.get_dbt_object_attributes(fqn)
152
180
  if dbt_object_attributes is not None:
153
181
  return self._deploy_alter(
154
- fqn,
155
- stage_name,
156
- dbt_object_attributes,
157
- default_target,
158
- unset_default_target,
159
- external_access_integrations,
160
- install_local_deps,
182
+ fqn, stage_name, dbt_object_attributes, attrs
161
183
  )
162
184
  else:
163
- return self._deploy_create(
164
- fqn,
165
- stage_name,
166
- default_target,
167
- external_access_integrations,
168
- install_local_deps,
169
- )
185
+ return self._deploy_create(fqn, stage_name, attrs)
170
186
 
171
187
  def _deploy_alter(
172
188
  self,
173
189
  fqn: FQN,
174
190
  stage_name: str,
175
191
  dbt_object_attributes: DBTObjectEditableAttributes,
176
- default_target: Optional[str],
177
- unset_default_target: bool,
178
- external_access_integrations: Optional[List[str]],
179
- install_local_deps: bool,
192
+ attrs: DBTDeployAttributes,
180
193
  ) -> SnowflakeCursor:
181
194
  query = f"ALTER DBT PROJECT {fqn} ADD VERSION"
182
195
  query += f"\nFROM {stage_name}"
@@ -186,28 +199,35 @@ class DBTManager(SqlExecutionMixin):
186
199
  unset_properties = []
187
200
 
188
201
  current_default_target = dbt_object_attributes.get("default_target")
189
- if unset_default_target and current_default_target is not None:
202
+ if attrs.unset_default_target and current_default_target is not None:
190
203
  unset_properties.append("DEFAULT_TARGET")
191
- elif default_target and (
204
+ elif attrs.default_target and (
192
205
  current_default_target is None
193
- or current_default_target.lower() != default_target.lower()
206
+ or current_default_target.lower() != attrs.default_target.lower()
194
207
  ):
195
- set_properties.append(f"DEFAULT_TARGET='{default_target}'")
208
+ set_properties.append(f"DEFAULT_TARGET='{attrs.default_target}'")
209
+
210
+ # Comparing dbt_version to existing project's dbt_version might be ambiguous
211
+ # if previously project was locked to just minor version and now user wants to
212
+ # lock it to a patch as well. If target version is provided, it's better to just
213
+ # apply it.
214
+ if attrs.dbt_version:
215
+ set_properties.append(f"DBT_VERSION='{attrs.dbt_version}'")
196
216
 
197
217
  current_external_access_integrations = dbt_object_attributes.get(
198
218
  "external_access_integrations"
199
219
  )
200
220
  if self._should_update_external_access_integrations(
201
221
  current_external_access_integrations,
202
- external_access_integrations,
203
- install_local_deps,
222
+ attrs.external_access_integrations,
223
+ attrs.install_local_deps,
204
224
  ):
205
- if external_access_integrations:
206
- integrations_str = ", ".join(sorted(external_access_integrations))
225
+ if attrs.external_access_integrations:
226
+ integrations_str = ", ".join(sorted(attrs.external_access_integrations))
207
227
  set_properties.append(
208
228
  f"EXTERNAL_ACCESS_INTEGRATIONS=({integrations_str})"
209
229
  )
210
- elif install_local_deps:
230
+ elif attrs.install_local_deps:
211
231
  set_properties.append("EXTERNAL_ACCESS_INTEGRATIONS=()")
212
232
 
213
233
  if set_properties or unset_properties:
@@ -245,16 +265,16 @@ class DBTManager(SqlExecutionMixin):
245
265
  self,
246
266
  fqn: FQN,
247
267
  stage_name: str,
248
- default_target: Optional[str],
249
- external_access_integrations: Optional[List[str]],
250
- install_local_deps: bool,
268
+ attrs: DBTDeployAttributes,
251
269
  ) -> SnowflakeCursor:
252
270
  query = f"CREATE DBT PROJECT {fqn}"
253
271
  query += f"\nFROM {stage_name}"
254
- if default_target:
255
- query += f" DEFAULT_TARGET='{default_target}'"
272
+ if attrs.default_target:
273
+ query += f" DEFAULT_TARGET='{attrs.default_target}'"
274
+ if attrs.dbt_version:
275
+ query += f" DBT_VERSION='{attrs.dbt_version}'"
256
276
  query = self._handle_external_access_integrations_query(
257
- query, external_access_integrations, install_local_deps
277
+ query, attrs.external_access_integrations, attrs.install_local_deps
258
278
  )
259
279
  return self.execute_query(query)
260
280
 
@@ -276,16 +296,16 @@ class DBTManager(SqlExecutionMixin):
276
296
  self,
277
297
  fqn: FQN,
278
298
  stage_name: str,
279
- default_target: Optional[str],
280
- external_access_integrations: Optional[List[str]],
281
- install_local_deps: bool,
299
+ attrs: DBTDeployAttributes,
282
300
  ) -> SnowflakeCursor:
283
301
  query = f"CREATE OR REPLACE DBT PROJECT {fqn}"
284
302
  query += f"\nFROM {stage_name}"
285
- if default_target:
286
- query += f" DEFAULT_TARGET='{default_target}'"
303
+ if attrs.default_target:
304
+ query += f" DEFAULT_TARGET='{attrs.default_target}'"
305
+ if attrs.dbt_version:
306
+ query += f" DBT_VERSION='{attrs.dbt_version}'"
287
307
  query = self._handle_external_access_integrations_query(
288
- query, external_access_integrations, install_local_deps
308
+ query, attrs.external_access_integrations, attrs.install_local_deps
289
309
  )
290
310
  return self.execute_query(query)
291
311
 
@@ -379,13 +399,21 @@ class DBTManager(SqlExecutionMixin):
379
399
  yaml.safe_dump(yaml.safe_load(sfd), tfd)
380
400
 
381
401
  def execute(
382
- self, dbt_command: str, name: FQN, run_async: bool, *dbt_cli_args
402
+ self,
403
+ dbt_command: str,
404
+ name: FQN,
405
+ run_async: bool,
406
+ dbt_version: Optional[str] = None,
407
+ *dbt_cli_args,
383
408
  ) -> SnowflakeCursor:
384
409
  if dbt_cli_args:
385
410
  processed_args = self._process_dbt_args(dbt_cli_args)
386
411
  dbt_command = f"{dbt_command} {processed_args}".strip()
387
412
  dbt_command_escaped = dbt_command.replace("'", "\\'")
388
- query = f"EXECUTE DBT PROJECT {name} args='{dbt_command_escaped}'"
413
+ query = f"EXECUTE DBT PROJECT {name}"
414
+ if dbt_version:
415
+ query += f" dbt_version='{dbt_version}'"
416
+ query += f" args='{dbt_command_escaped}'"
389
417
  return self.execute_query(query, _exec_async=run_async)
390
418
 
391
419
  @staticmethod
@@ -15,10 +15,13 @@ from typing import List, Optional
15
15
 
16
16
  import typer
17
17
  from snowflake.cli._plugins.dcm.manager import DCMProjectManager
18
+ from snowflake.cli._plugins.dcm.reporters import RefreshReporter, TestReporter
19
+ from snowflake.cli._plugins.dcm.utils import mock_dcm_response
18
20
  from snowflake.cli._plugins.object.command_aliases import add_object_command_aliases
19
21
  from snowflake.cli._plugins.object.commands import scope_option
20
22
  from snowflake.cli._plugins.object.manager import ObjectManager
21
23
  from snowflake.cli.api.commands.flags import (
24
+ IdentifierType,
22
25
  IfExistsOption,
23
26
  IfNotExistsOption,
24
27
  OverrideableOption,
@@ -35,6 +38,7 @@ from snowflake.cli.api.exceptions import CliError
35
38
  from snowflake.cli.api.feature_flags import FeatureFlag
36
39
  from snowflake.cli.api.identifiers import FQN
37
40
  from snowflake.cli.api.output.types import (
41
+ EmptyResult,
38
42
  MessageResult,
39
43
  QueryJsonValueResult,
40
44
  QueryResult,
@@ -112,6 +116,12 @@ def deploy(
112
116
  variables: Optional[List[str]] = variables_flag,
113
117
  configuration: Optional[str] = configuration_flag,
114
118
  alias: Optional[str] = alias_option,
119
+ skip_plan: bool = typer.Option(
120
+ False,
121
+ "--skip-plan",
122
+ help="Skips planning step",
123
+ hidden=True,
124
+ ),
115
125
  **options,
116
126
  ):
117
127
  """
@@ -122,13 +132,15 @@ def deploy(
122
132
 
123
133
  with cli_console.spinner() as spinner:
124
134
  spinner.add_task(description=f"Deploying dcm project {identifier}", total=None)
125
- result = manager.execute(
135
+ if skip_plan:
136
+ cli_console.warning("Skipping planning step")
137
+ result = manager.deploy(
126
138
  project_identifier=identifier,
127
139
  configuration=configuration,
128
140
  from_stage=effective_stage,
129
141
  variables=variables,
130
142
  alias=alias,
131
- output_path=None,
143
+ skip_plan=skip_plan,
132
144
  )
133
145
  return QueryJsonValueResult(result)
134
146
 
@@ -152,11 +164,10 @@ def plan(
152
164
 
153
165
  with cli_console.spinner() as spinner:
154
166
  spinner.add_task(description=f"Planning dcm project {identifier}", total=None)
155
- result = manager.execute(
167
+ result = manager.plan(
156
168
  project_identifier=identifier,
157
169
  configuration=configuration,
158
170
  from_stage=effective_stage,
159
- dry_run=True,
160
171
  variables=variables,
161
172
  output_path=output_path,
162
173
  )
@@ -235,6 +246,85 @@ def drop_deployment(
235
246
  )
236
247
 
237
248
 
249
+ @app.command(requires_connection=True)
250
+ def preview(
251
+ identifier: FQN = dcm_identifier,
252
+ object_identifier: FQN = typer.Option(
253
+ ...,
254
+ "--object",
255
+ help="FQN of table/view/dynamic table to be previewed.",
256
+ show_default=False,
257
+ click_type=IdentifierType(),
258
+ ),
259
+ from_location: Optional[str] = from_option,
260
+ variables: Optional[List[str]] = variables_flag,
261
+ configuration: Optional[str] = configuration_flag,
262
+ limit: Optional[int] = typer.Option(
263
+ None,
264
+ "--limit",
265
+ help="The maximum number of rows to be returned.",
266
+ show_default=False,
267
+ ),
268
+ **options,
269
+ ):
270
+ """
271
+ Returns rows from any table, view, dynamic table.
272
+ """
273
+ manager = DCMProjectManager()
274
+ effective_stage = _get_effective_stage(identifier, from_location)
275
+
276
+ with cli_console.spinner() as spinner:
277
+ spinner.add_task(
278
+ description=f"Previewing {object_identifier}.",
279
+ total=None,
280
+ )
281
+ result = manager.preview(
282
+ project_identifier=identifier,
283
+ object_identifier=object_identifier,
284
+ configuration=configuration,
285
+ from_stage=effective_stage,
286
+ variables=variables,
287
+ limit=limit,
288
+ )
289
+
290
+ return QueryResult(result)
291
+
292
+
293
+ @app.command(requires_connection=True)
294
+ @mock_dcm_response("refresh")
295
+ def refresh(
296
+ identifier: FQN = dcm_identifier,
297
+ **options,
298
+ ):
299
+ """
300
+ Refreshes dynamic tables defined in DCM project.
301
+ """
302
+ with cli_console.spinner() as spinner:
303
+ spinner.add_task(description=f"Refreshing dcm project {identifier}", total=None)
304
+ result = DCMProjectManager().refresh(project_identifier=identifier)
305
+
306
+ RefreshReporter().process(result)
307
+ return EmptyResult()
308
+
309
+
310
+ @app.command(requires_connection=True)
311
+ @mock_dcm_response("test")
312
+ def test(
313
+ identifier: FQN = dcm_identifier,
314
+ **options,
315
+ ):
316
+ """
317
+ Tests all expectations defined in DCM project.
318
+ """
319
+ with cli_console.spinner() as spinner:
320
+ spinner.add_task(description=f"Testing dcm project {identifier}", total=None)
321
+ result = DCMProjectManager().test(project_identifier=identifier)
322
+
323
+ reporter = TestReporter()
324
+ reporter.process(result)
325
+ return EmptyResult()
326
+
327
+
238
328
  def _get_effective_stage(identifier: FQN, from_location: Optional[str]):
239
329
  manager = DCMProjectManager()
240
330
  if not from_location: