snowflake-cli 3.2.1__py3-none-any.whl → 3.3.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 (56) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/constants.py +4 -0
  3. snowflake/cli/_app/snow_connector.py +12 -0
  4. snowflake/cli/_app/telemetry.py +10 -3
  5. snowflake/cli/_plugins/connection/util.py +12 -19
  6. snowflake/cli/_plugins/helpers/commands.py +207 -1
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
  8. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
  9. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
  10. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
  11. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
  12. snowflake/cli/_plugins/nativeapp/commands.py +92 -2
  13. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  14. snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
  15. snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
  16. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  17. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  18. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  19. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
  20. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  21. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
  22. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  23. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  24. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
  25. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  26. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
  27. snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
  28. snowflake/cli/_plugins/notebook/manager.py +4 -2
  29. snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
  30. snowflake/cli/_plugins/spcs/common.py +129 -0
  31. snowflake/cli/_plugins/spcs/services/commands.py +134 -14
  32. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  33. snowflake/cli/_plugins/stage/manager.py +12 -4
  34. snowflake/cli/_plugins/streamlit/manager.py +8 -1
  35. snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
  36. snowflake/cli/_plugins/workspace/commands.py +3 -2
  37. snowflake/cli/_plugins/workspace/manager.py +8 -4
  38. snowflake/cli/api/cli_global_context.py +22 -1
  39. snowflake/cli/api/config.py +6 -2
  40. snowflake/cli/api/connections.py +12 -1
  41. snowflake/cli/api/constants.py +9 -1
  42. snowflake/cli/api/entities/common.py +85 -0
  43. snowflake/cli/api/entities/utils.py +9 -8
  44. snowflake/cli/api/errno.py +60 -3
  45. snowflake/cli/api/feature_flags.py +20 -4
  46. snowflake/cli/api/metrics.py +21 -27
  47. snowflake/cli/api/project/definition_conversion.py +1 -2
  48. snowflake/cli/api/project/schemas/project_definition.py +27 -6
  49. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  50. snowflake/cli/api/project/util.py +45 -0
  51. snowflake/cli/api/rest_api.py +3 -2
  52. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
  53. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
  54. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
  55. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
  56. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import re
6
+ from datetime import datetime
5
7
  from pathlib import Path
6
8
  from textwrap import dedent
7
- from typing import List, Literal, Optional, Union
9
+ from typing import Any, List, Literal, Optional, Set, Union
8
10
 
9
11
  import typer
10
- from click import BadOptionUsage, ClickException
12
+ from click import BadOptionUsage, ClickException, UsageError
11
13
  from pydantic import Field, field_validator
14
+ from snowflake.cli._plugins.connection.util import UIParameter
12
15
  from snowflake.cli._plugins.nativeapp.artifacts import (
13
16
  BundleMap,
14
17
  VersionInfo,
15
18
  build_bundle,
19
+ find_setup_script_file,
16
20
  find_version_info_in_manifest_file,
17
21
  )
18
22
  from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext
@@ -20,14 +24,19 @@ from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler
20
24
  from snowflake.cli._plugins.nativeapp.constants import (
21
25
  ALLOWED_SPECIAL_COMMENTS,
22
26
  COMMENT_COL,
27
+ DEFAULT_CHANNEL,
28
+ DEFAULT_DIRECTIVE,
23
29
  EXTERNAL_DISTRIBUTION,
24
30
  INTERNAL_DISTRIBUTION,
31
+ MAX_VERSIONS_IN_RELEASE_CHANNEL,
25
32
  NAME_COL,
26
33
  OWNER_COL,
27
34
  PATCH_COL,
28
- SPECIAL_COMMENT,
29
35
  VERSION_COL,
30
36
  )
37
+ from snowflake.cli._plugins.nativeapp.entities.application_package_child_interface import (
38
+ ApplicationPackageChildInterface,
39
+ )
31
40
  from snowflake.cli._plugins.nativeapp.exceptions import (
32
41
  ApplicationPackageAlreadyExistsError,
33
42
  ApplicationPackageDoesNotExistError,
@@ -35,6 +44,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
35
44
  ObjectPropertyNotFoundError,
36
45
  SetupScriptFailedValidation,
37
46
  )
47
+ from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
38
48
  from snowflake.cli._plugins.nativeapp.policy import (
39
49
  AllowAlwaysPolicy,
40
50
  AskAlwaysPolicy,
@@ -45,11 +55,24 @@ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
45
55
  from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
46
56
  InsufficientPrivilegesError,
47
57
  )
48
- from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
58
+ from snowflake.cli._plugins.nativeapp.sf_sql_facade import ReleaseChannel, Version
59
+ from snowflake.cli._plugins.nativeapp.utils import needs_confirmation, sanitize_dir_name
60
+ from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
61
+ FunctionEntityModel,
62
+ ProcedureEntityModel,
63
+ )
49
64
  from snowflake.cli._plugins.stage.diff import DiffResult
50
65
  from snowflake.cli._plugins.stage.manager import StageManager
66
+ from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
67
+ StreamlitEntityModel,
68
+ )
51
69
  from snowflake.cli._plugins.workspace.context import ActionContext
52
- from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
70
+ from snowflake.cli.api.cli_global_context import span
71
+ from snowflake.cli.api.entities.common import (
72
+ EntityBase,
73
+ attach_spans_to_entity_actions,
74
+ get_sql_executor,
75
+ )
53
76
  from snowflake.cli.api.entities.utils import (
54
77
  drop_generic_object,
55
78
  execute_post_deploy_hooks,
@@ -67,14 +90,18 @@ from snowflake.cli.api.project.schemas.entities.common import (
67
90
  from snowflake.cli.api.project.schemas.updatable_model import (
68
91
  DiscriminatorField,
69
92
  IdentifierField,
93
+ UpdatableModel,
70
94
  )
71
95
  from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
72
96
  from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
73
97
  from snowflake.cli.api.project.util import (
74
98
  SCHEMA_AND_NAME,
99
+ VALID_IDENTIFIER_REGEX,
75
100
  append_test_resource_suffix,
76
101
  extract_schema,
77
102
  identifier_to_show_like_pattern,
103
+ same_identifiers,
104
+ sql_match,
78
105
  to_identifier,
79
106
  unquote_identifier,
80
107
  )
@@ -82,6 +109,43 @@ from snowflake.cli.api.utils.cursor import find_all_rows
82
109
  from snowflake.connector import DictCursor, ProgrammingError
83
110
  from snowflake.connector.cursor import SnowflakeCursor
84
111
 
112
+ ApplicationPackageChildrenTypes = (
113
+ StreamlitEntityModel | FunctionEntityModel | ProcedureEntityModel
114
+ )
115
+
116
+
117
+ class ApplicationPackageChildIdentifier(UpdatableModel):
118
+ schema_: Optional[str] = Field(
119
+ title="Child entity schema", alias="schema", default=None
120
+ )
121
+
122
+
123
+ class EnsureUsableByField(UpdatableModel):
124
+ application_roles: Optional[Union[str, Set[str]]] = Field(
125
+ title="One or more application roles to be granted with the required privileges",
126
+ default=None,
127
+ )
128
+
129
+ @field_validator("application_roles")
130
+ @classmethod
131
+ def ensure_app_roles_is_a_set(
132
+ cls, application_roles: Optional[Union[str, Set[str]]]
133
+ ) -> Optional[Union[Set[str]]]:
134
+ if isinstance(application_roles, str):
135
+ return set([application_roles])
136
+ return application_roles
137
+
138
+
139
+ class ApplicationPackageChildField(UpdatableModel):
140
+ target: str = Field(title="The key of the entity to include in this package")
141
+ ensure_usable_by: Optional[EnsureUsableByField] = Field(
142
+ title="Automatically grant the required privileges on the child object and its schema",
143
+ default=None,
144
+ )
145
+ identifier: ApplicationPackageChildIdentifier = Field(
146
+ title="Entity identifier", default=None
147
+ )
148
+
85
149
 
86
150
  class ApplicationPackageEntityModel(EntityModelBase):
87
151
  type: Literal["application package"] = DiscriminatorField() # noqa: A003
@@ -89,23 +153,27 @@ class ApplicationPackageEntityModel(EntityModelBase):
89
153
  title="List of paths or file source/destination pairs to add to the deploy root",
90
154
  )
91
155
  bundle_root: Optional[str] = Field(
92
- title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored.",
156
+ title="Folder at the root of your project where artifacts necessary to perform the bundle step are stored",
93
157
  default="output/bundle/",
94
158
  )
95
159
  deploy_root: Optional[str] = Field(
96
160
  title="Folder at the root of your project where the build step copies the artifacts",
97
161
  default="output/deploy/",
98
162
  )
163
+ children_artifacts_dir: Optional[str] = Field(
164
+ title="Folder under deploy_root where the child artifacts will be stored",
165
+ default="_children/",
166
+ )
99
167
  generated_root: Optional[str] = Field(
100
- title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written.",
168
+ title="Subdirectory of the deploy root where files generated by the Snowflake CLI will be written",
101
169
  default="__generated/",
102
170
  )
103
171
  stage: Optional[str] = IdentifierField(
104
- title="Identifier of the stage that stores the application artifacts.",
172
+ title="Identifier of the stage that stores the application artifacts",
105
173
  default="app_src.stage",
106
174
  )
107
175
  scratch_stage: Optional[str] = IdentifierField(
108
- title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI.",
176
+ title="Identifier of the stage that stores temporary scratch data used by the Snowflake CLI",
109
177
  default="app_src.stage_snowflake_cli_scratch",
110
178
  )
111
179
  distribution: Optional[DistributionOptions] = Field(
@@ -116,6 +184,19 @@ class ApplicationPackageEntityModel(EntityModelBase):
116
184
  title="Path to manifest.yml. Unused and deprecated starting with Snowflake CLI 3.2",
117
185
  default="",
118
186
  )
187
+ children: Optional[List[ApplicationPackageChildField]] = Field(
188
+ title="Entities that will be bundled and deployed as part of this application package",
189
+ default=[],
190
+ )
191
+
192
+ @field_validator("children")
193
+ @classmethod
194
+ def verify_children_behind_flag(
195
+ cls, input_value: Optional[List[ApplicationPackageChildField]]
196
+ ) -> Optional[List[ApplicationPackageChildField]]:
197
+ if input_value and not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
198
+ raise AttributeError("Application package children are not supported yet")
199
+ return input_value
119
200
 
120
201
  @field_validator("identifier")
121
202
  @classmethod
@@ -157,6 +238,7 @@ class ApplicationPackageEntityModel(EntityModelBase):
157
238
  return input_value
158
239
 
159
240
 
241
+ @attach_spans_to_entity_actions(entity_name="app_pkg")
160
242
  class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
161
243
  """
162
244
  A Native App application package.
@@ -170,6 +252,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
170
252
  def deploy_root(self) -> Path:
171
253
  return self.project_root / self._entity_model.deploy_root
172
254
 
255
+ @property
256
+ def children_artifacts_deploy_root(self) -> Path:
257
+ return self.deploy_root / self._entity_model.children_artifacts_dir
258
+
173
259
  @property
174
260
  def bundle_root(self) -> Path:
175
261
  return self.project_root / self._entity_model.bundle_root
@@ -208,7 +294,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
208
294
  return model.meta and model.meta.post_deploy
209
295
 
210
296
  def action_bundle(self, action_ctx: ActionContext, *args, **kwargs):
211
- return self._bundle()
297
+ return self._bundle(action_ctx)
212
298
 
213
299
  def action_deploy(
214
300
  self,
@@ -224,6 +310,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
224
310
  **kwargs,
225
311
  ):
226
312
  return self._deploy(
313
+ action_ctx=action_ctx,
227
314
  bundle_map=None,
228
315
  prune=prune,
229
316
  recursive=recursive,
@@ -248,21 +335,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
248
335
  )
249
336
  return
250
337
 
251
- with sql_executor.use_role(self.role):
252
- # 2. Check for versions in the application package
253
- show_versions_query = f"show versions in application package {self.name}"
254
- show_versions_cursor = sql_executor.execute_query(
255
- show_versions_query, cursor_class=DictCursor
256
- )
257
- if show_versions_cursor.rowcount is None:
258
- raise SnowflakeSQLExecutionError(show_versions_query)
259
-
260
- if show_versions_cursor.rowcount > 0:
261
- # allow dropping a package with versions when --force is set
262
- if not force_drop:
263
- raise CouldNotDropApplicationPackageWithVersions(
264
- "Drop versions first, or use --force to override."
265
- )
338
+ # 2. Check for versions in the application package
339
+ versions_in_pkg = get_snowflake_facade().show_versions(self.name, self.role)
340
+ if len(versions_in_pkg) > 0:
341
+ # allow dropping a package with versions when --force is set
342
+ if not force_drop:
343
+ raise CouldNotDropApplicationPackageWithVersions(
344
+ "Drop versions first, or use --force to override."
345
+ )
266
346
 
267
347
  # 3. Check distribution of the existing application package
268
348
  actual_distribution = self.get_app_pkg_distribution_in_snowflake()
@@ -323,6 +403,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
323
403
  **kwargs,
324
404
  ):
325
405
  self.validate_setup_script(
406
+ action_ctx=action_ctx,
326
407
  use_scratch_stage=use_scratch_stage,
327
408
  interactive=interactive,
328
409
  force=force,
@@ -336,15 +417,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
336
417
  Get all existing versions, if defined, for an application package.
337
418
  It executes a 'show versions in application package' query and returns all the results.
338
419
  """
339
- sql_executor = get_sql_executor()
340
- with sql_executor.use_role(self.role):
341
- show_obj_query = f"show versions in application package {self.name}"
342
- show_obj_cursor = sql_executor.execute_query(show_obj_query)
343
-
344
- if show_obj_cursor.rowcount is None:
345
- raise SnowflakeSQLExecutionError(show_obj_query)
346
-
347
- return show_obj_cursor
420
+ return get_snowflake_facade().show_versions(self.name, self.role)
348
421
 
349
422
  def action_version_create(
350
423
  self,
@@ -355,9 +428,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
355
428
  skip_git_check: bool,
356
429
  interactive: bool,
357
430
  force: bool,
431
+ from_stage: Optional[bool],
358
432
  *args,
359
433
  **kwargs,
360
- ):
434
+ ) -> VersionInfo:
361
435
  """
362
436
  Create a version and/or patch for a new or existing application package.
363
437
  Always performs a deploy action before creating version or patch.
@@ -372,12 +446,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
372
446
  else:
373
447
  policy = DenyAlwaysPolicy()
374
448
 
375
- if skip_git_check:
376
- git_policy = DenyAlwaysPolicy()
377
- else:
378
- git_policy = AllowAlwaysPolicy()
379
-
380
- bundle_map = self._bundle()
449
+ bundle_map = self._bundle(action_ctx)
381
450
  resolved_version, resolved_patch, resolved_label = self.resolve_version_info(
382
451
  version=version,
383
452
  patch=patch,
@@ -387,20 +456,31 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
387
456
  interactive=interactive,
388
457
  )
389
458
 
390
- if git_policy.should_proceed():
459
+ if not skip_git_check:
391
460
  self.check_index_changes_in_git_repo(policy=policy, interactive=interactive)
392
461
 
393
- self._deploy(
394
- bundle_map=bundle_map,
395
- prune=True,
396
- recursive=True,
397
- paths=[],
398
- print_diff=True,
399
- validate=True,
400
- stage_fqn=self.stage_fqn,
401
- interactive=interactive,
402
- force=force,
403
- )
462
+ # if user is asking to create the version from the current stage,
463
+ # then do not re-deploy the artifacts or touch the stage
464
+ if from_stage:
465
+ # verify package exists:
466
+ if not self.get_existing_app_pkg_info():
467
+ raise ClickException(
468
+ "Cannot create version from stage because the application package does not exist yet. "
469
+ "Try removing --from-stage flag or executing `snow app deploy` to deploy the application package first."
470
+ )
471
+ else:
472
+ self._deploy(
473
+ action_ctx=action_ctx,
474
+ bundle_map=bundle_map,
475
+ prune=True,
476
+ recursive=True,
477
+ paths=[],
478
+ print_diff=True,
479
+ validate=True,
480
+ stage_fqn=self.stage_fqn,
481
+ interactive=interactive,
482
+ force=force,
483
+ )
404
484
 
405
485
  # Warn if the version exists in a release directive(s)
406
486
  try:
@@ -440,12 +520,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
440
520
  # Define a new version in the application package
441
521
  if not self.get_existing_version_info(resolved_version):
442
522
  self.add_new_version(version=resolved_version, label=resolved_label)
443
- return # A new version created automatically has patch 0, we do not need to further increment the patch.
523
+ # A new version created automatically has patch 0, we do not need to further increment the patch.
524
+ return VersionInfo(resolved_version, 0, resolved_label)
444
525
 
445
526
  # Add a new patch to an existing (old) version
446
- self.add_new_patch_to_version(
527
+ patch = self.add_new_patch_to_version(
447
528
  version=resolved_version, patch=resolved_patch, label=resolved_label
448
529
  )
530
+ return VersionInfo(resolved_version, patch, resolved_label)
449
531
 
450
532
  def action_version_drop(
451
533
  self,
@@ -492,7 +574,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
492
574
  """
493
575
  )
494
576
  )
495
- self._bundle()
577
+ self._bundle(action_ctx)
496
578
  version_info = find_version_info_in_manifest_file(self.deploy_root)
497
579
  version = version_info.version_name
498
580
  if not version:
@@ -524,20 +606,231 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
524
606
  raise typer.Exit(1)
525
607
 
526
608
  # Drop the version
527
- sql_executor = get_sql_executor()
528
- with sql_executor.use_role(self.role):
529
- try:
530
- sql_executor.execute_query(
531
- f"alter application package {self.name} drop version {version}"
532
- )
533
- except ProgrammingError as err:
534
- raise err # e.g. version is referenced in a release directive(s)
609
+ get_snowflake_facade().drop_version_from_package(
610
+ package_name=self.name, version=version, role=self.role
611
+ )
535
612
 
536
613
  console.message(
537
614
  f"Version {version} in application package {self.name} dropped successfully."
538
615
  )
539
616
 
540
- def _bundle(self):
617
+ def _validate_target_accounts(self, accounts: list[str]) -> None:
618
+ """
619
+ Validates the target accounts provided by the user.
620
+ """
621
+ for account in accounts:
622
+ if not re.fullmatch(
623
+ f"{VALID_IDENTIFIER_REGEX}\\.{VALID_IDENTIFIER_REGEX}", account
624
+ ):
625
+ raise ClickException(
626
+ f"Target account {account} is not in a valid format. Make sure you provide the target account in the format 'org.account'."
627
+ )
628
+
629
+ def get_sanitized_release_channel(
630
+ self,
631
+ release_channel: Optional[str],
632
+ available_release_channels: Optional[list[ReleaseChannel]] = None,
633
+ ) -> Optional[str]:
634
+ """
635
+ Sanitize the release channel name provided by the user and validate it against the available release channels.
636
+
637
+ A return value of None indicates that release channels should not be used. Returns None if:
638
+ - Release channel is not provided
639
+ - Release channels are not enabled in the application package and the user provided the default release channel
640
+ """
641
+ if not release_channel:
642
+ return None
643
+
644
+ if available_release_channels is None:
645
+ available_release_channels = get_snowflake_facade().show_release_channels(
646
+ self.name, self.role
647
+ )
648
+
649
+ if not available_release_channels and same_identifiers(
650
+ release_channel, DEFAULT_CHANNEL
651
+ ):
652
+ return None
653
+
654
+ self.validate_release_channel(release_channel, available_release_channels)
655
+ return release_channel
656
+
657
+ def validate_release_channel(
658
+ self,
659
+ release_channel: str,
660
+ available_release_channels: Optional[list[ReleaseChannel]] = None,
661
+ ) -> None:
662
+ """
663
+ Validates the release channel provided by the user and make sure it is a valid release channel for the application package.
664
+ """
665
+
666
+ if available_release_channels is None:
667
+ available_release_channels = get_snowflake_facade().show_release_channels(
668
+ self.name, self.role
669
+ )
670
+ if not available_release_channels:
671
+ raise UsageError(
672
+ f"Release channels are not enabled for application package {self.name}."
673
+ )
674
+ for channel in available_release_channels:
675
+ if unquote_identifier(release_channel) == channel["name"]:
676
+ return
677
+
678
+ raise UsageError(
679
+ f"Release channel {release_channel} is not available in application package {self.name}. "
680
+ f"Available release channels are: ({', '.join(channel['name'] for channel in available_release_channels)})."
681
+ )
682
+
683
+ def action_release_directive_list(
684
+ self,
685
+ action_ctx: ActionContext,
686
+ release_channel: Optional[str],
687
+ like: str,
688
+ *args,
689
+ **kwargs,
690
+ ) -> list[dict[str, Any]]:
691
+ """
692
+ Get all existing release directives for an application package.
693
+ Limit the results to a specific release channel, if provided.
694
+
695
+ If `like` is provided, only release directives matching the SQL LIKE pattern are listed.
696
+ """
697
+ release_channel = self.get_sanitized_release_channel(release_channel)
698
+
699
+ release_directives = get_snowflake_facade().show_release_directives(
700
+ package_name=self.name,
701
+ role=self.role,
702
+ release_channel=release_channel,
703
+ )
704
+
705
+ return [
706
+ directive
707
+ for directive in release_directives
708
+ if sql_match(pattern=like, value=directive.get("name", ""))
709
+ ]
710
+
711
+ def action_release_directive_set(
712
+ self,
713
+ action_ctx: ActionContext,
714
+ version: str,
715
+ patch: int,
716
+ release_directive: str,
717
+ release_channel: str,
718
+ target_accounts: Optional[list[str]],
719
+ *args,
720
+ **kwargs,
721
+ ):
722
+ """
723
+ Sets a release directive to the specified version and patch using the specified release channel.
724
+ Target accounts can only be specified for non-default release directives.
725
+
726
+ For non-default release directives, update the existing release directive if target accounts are not provided.
727
+ """
728
+ if target_accounts:
729
+ self._validate_target_accounts(target_accounts)
730
+
731
+ if target_accounts and same_identifiers(release_directive, DEFAULT_DIRECTIVE):
732
+ raise BadOptionUsage(
733
+ "target_accounts",
734
+ "Target accounts can only be specified for non-default named release directives.",
735
+ )
736
+
737
+ sanitized_release_channel = self.get_sanitized_release_channel(release_channel)
738
+
739
+ get_snowflake_facade().set_release_directive(
740
+ package_name=self.name,
741
+ release_directive=release_directive,
742
+ release_channel=sanitized_release_channel,
743
+ target_accounts=target_accounts,
744
+ version=version,
745
+ patch=patch,
746
+ role=self.role,
747
+ )
748
+
749
+ def action_release_directive_unset(
750
+ self,
751
+ action_ctx: ActionContext,
752
+ release_directive: str,
753
+ release_channel: str,
754
+ ):
755
+ """
756
+ Unsets a release directive from the specified release channel.
757
+ """
758
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
759
+ raise ClickException(
760
+ "Cannot unset default release directive. Please specify a non-default release directive."
761
+ )
762
+
763
+ get_snowflake_facade().unset_release_directive(
764
+ package_name=self.name,
765
+ release_directive=release_directive,
766
+ release_channel=self.get_sanitized_release_channel(release_channel),
767
+ role=self.role,
768
+ )
769
+
770
+ def _print_channel_to_console(self, channel: ReleaseChannel) -> None:
771
+ """
772
+ Prints the release channel details to the console.
773
+ """
774
+ console = self._workspace_ctx.console
775
+
776
+ console.message(f"""[bold]{channel["name"]}[/bold]""")
777
+ accounts_list: Optional[list[str]] = channel["targets"].get("accounts")
778
+ target_accounts = (
779
+ f"({', '.join(accounts_list)})"
780
+ if accounts_list is not None
781
+ else "ALL ACCOUNTS"
782
+ )
783
+
784
+ formatted_created_on = (
785
+ channel["created_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
786
+ if channel["created_on"]
787
+ else ""
788
+ )
789
+
790
+ formatted_updated_on = (
791
+ channel["updated_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z")
792
+ if channel["updated_on"]
793
+ else ""
794
+ )
795
+ with console.indented():
796
+ console.message(f"Description: {channel['description']}")
797
+ console.message(f"Versions: ({', '.join(channel['versions'])})")
798
+ console.message(f"Created on: {formatted_created_on}")
799
+ console.message(f"Updated on: {formatted_updated_on}")
800
+ console.message(f"Target accounts: {target_accounts}")
801
+
802
+ def action_release_channel_list(
803
+ self,
804
+ action_ctx: ActionContext,
805
+ release_channel: Optional[str],
806
+ *args,
807
+ **kwargs,
808
+ ) -> list[ReleaseChannel]:
809
+ """
810
+ Get all existing release channels for an application package.
811
+ If `release_channel` is provided, only the specified release channel is listed.
812
+ """
813
+ console = self._workspace_ctx.console
814
+ available_channels = get_snowflake_facade().show_release_channels(
815
+ self.name, self.role
816
+ )
817
+
818
+ filtered_channels = [
819
+ channel
820
+ for channel in available_channels
821
+ if release_channel is None
822
+ or unquote_identifier(release_channel) == channel["name"]
823
+ ]
824
+
825
+ if not filtered_channels:
826
+ console.message("No release channels found.")
827
+ else:
828
+ for channel in filtered_channels:
829
+ self._print_channel_to_console(channel)
830
+
831
+ return filtered_channels
832
+
833
+ def _bundle(self, action_ctx: ActionContext = None):
541
834
  model = self._entity_model
542
835
  bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts)
543
836
  bundle_context = BundleContext(
@@ -550,10 +843,359 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
550
843
  )
551
844
  compiler = NativeAppCompiler(bundle_context)
552
845
  compiler.compile_artifacts()
846
+
847
+ if self._entity_model.children:
848
+ # Bundle children and append their SQL to setup script
849
+ # TODO Consider re-writing the logic below as a processor
850
+ children_sql = self._bundle_children(action_ctx=action_ctx)
851
+ setup_file_path = find_setup_script_file(deploy_root=self.deploy_root)
852
+ with open(setup_file_path, "r", encoding="utf-8") as file:
853
+ existing_setup_script = file.read()
854
+ if setup_file_path.is_symlink():
855
+ setup_file_path.unlink()
856
+ with open(setup_file_path, "w", encoding="utf-8") as file:
857
+ file.write(existing_setup_script)
858
+ file.write("\n-- AUTO GENERATED CHILDREN SECTION\n")
859
+ file.write("\n".join(children_sql))
860
+ file.write("\n")
861
+
553
862
  return bundle_map
554
863
 
864
+ def action_release_channel_add_accounts(
865
+ self,
866
+ action_ctx: ActionContext,
867
+ release_channel: str,
868
+ target_accounts: list[str],
869
+ *args,
870
+ **kwargs,
871
+ ):
872
+ """
873
+ Adds target accounts to a release channel.
874
+ """
875
+
876
+ if not target_accounts:
877
+ raise ClickException("No target accounts provided.")
878
+
879
+ self.validate_release_channel(release_channel)
880
+ self._validate_target_accounts(target_accounts)
881
+
882
+ get_snowflake_facade().add_accounts_to_release_channel(
883
+ package_name=self.name,
884
+ release_channel=release_channel,
885
+ target_accounts=target_accounts,
886
+ role=self.role,
887
+ )
888
+
889
+ def action_release_channel_remove_accounts(
890
+ self,
891
+ action_ctx: ActionContext,
892
+ release_channel: str,
893
+ target_accounts: list[str],
894
+ *args,
895
+ **kwargs,
896
+ ):
897
+ """
898
+ Removes target accounts from a release channel.
899
+ """
900
+
901
+ if not target_accounts:
902
+ raise ClickException("No target accounts provided.")
903
+
904
+ self.validate_release_channel(release_channel)
905
+ self._validate_target_accounts(target_accounts)
906
+
907
+ get_snowflake_facade().remove_accounts_from_release_channel(
908
+ package_name=self.name,
909
+ release_channel=release_channel,
910
+ target_accounts=target_accounts,
911
+ role=self.role,
912
+ )
913
+
914
+ def action_release_channel_add_version(
915
+ self,
916
+ action_ctx: ActionContext,
917
+ release_channel: str,
918
+ version: str,
919
+ *args,
920
+ **kwargs,
921
+ ):
922
+ """
923
+ Adds a version to a release channel.
924
+ """
925
+
926
+ self.validate_release_channel(release_channel)
927
+ get_snowflake_facade().add_version_to_release_channel(
928
+ package_name=self.name,
929
+ release_channel=release_channel,
930
+ version=version,
931
+ role=self.role,
932
+ )
933
+
934
+ def action_release_channel_remove_version(
935
+ self,
936
+ action_ctx: ActionContext,
937
+ release_channel: str,
938
+ version: str,
939
+ *args,
940
+ **kwargs,
941
+ ):
942
+ """
943
+ Removes a version from a release channel.
944
+ """
945
+
946
+ self.validate_release_channel(release_channel)
947
+ get_snowflake_facade().remove_version_from_release_channel(
948
+ package_name=self.name,
949
+ release_channel=release_channel,
950
+ version=version,
951
+ role=self.role,
952
+ )
953
+
954
+ def _find_version_with_no_recent_update(
955
+ self, versions_info: list[Version], free_versions: set[str]
956
+ ) -> Optional[str]:
957
+ """
958
+ Finds the version with the oldest created_on date from the free versions.
959
+ """
960
+
961
+ if not free_versions:
962
+ return None
963
+
964
+ # map of versionId to last Updated Date. Last Updated Date is based on patch creation date.
965
+ last_updated_map: dict[str, datetime] = {}
966
+ for version_info in versions_info:
967
+ last_updated_value = last_updated_map.get(version_info["version"], None)
968
+ if (
969
+ not last_updated_value
970
+ or version_info["created_on"] > last_updated_value
971
+ ):
972
+ last_updated_map[version_info["version"]] = version_info["created_on"]
973
+
974
+ oldest_version = None
975
+ oldest_version_last_updated_on = None
976
+
977
+ for version in free_versions:
978
+ last_updated = last_updated_map[version]
979
+ if not oldest_version or last_updated < oldest_version_last_updated_on:
980
+ oldest_version = version
981
+ oldest_version_last_updated_on = last_updated
982
+
983
+ return oldest_version
984
+
985
+ def action_publish(
986
+ self,
987
+ action_ctx: ActionContext,
988
+ version: Optional[str],
989
+ patch: Optional[int],
990
+ release_channel: Optional[str],
991
+ release_directive: str,
992
+ interactive: bool,
993
+ force: bool,
994
+ *args,
995
+ create_version: bool = False,
996
+ from_stage: bool = False,
997
+ label: Optional[str] = None,
998
+ **kwargs,
999
+ ) -> VersionInfo:
1000
+ """
1001
+ Publishes a version and a patch to a release directive of a release channel.
1002
+
1003
+ The version is first added to the release channel,
1004
+ and then the release directive is set to the version and patch provided.
1005
+
1006
+ If the number of versions in a release channel exceeds the maximum allowable versions,
1007
+ the user is prompted to remove an existing version to make space for the new version.
1008
+ """
1009
+ if force:
1010
+ policy = AllowAlwaysPolicy()
1011
+ elif interactive:
1012
+ policy = AskAlwaysPolicy()
1013
+ else:
1014
+ policy = DenyAlwaysPolicy()
1015
+
1016
+ if from_stage and not create_version:
1017
+ raise UsageError(
1018
+ "--from-stage flag can only be used with --create-version flag."
1019
+ )
1020
+ if label is not None and not create_version:
1021
+ raise UsageError("--label can only be used with --create-version flag.")
1022
+
1023
+ console = self._workspace_ctx.console
1024
+ if create_version:
1025
+ result = self.action_version_create(
1026
+ action_ctx=action_ctx,
1027
+ version=version,
1028
+ patch=patch,
1029
+ label=label,
1030
+ skip_git_check=True,
1031
+ interactive=interactive,
1032
+ force=force,
1033
+ from_stage=from_stage,
1034
+ )
1035
+ version = result.version_name
1036
+ patch = result.patch_number
1037
+
1038
+ if version is None:
1039
+ raise UsageError(
1040
+ "Please provide a version using --version or use --create-version flag to create a version based on the manifest file."
1041
+ )
1042
+ if patch is None:
1043
+ raise UsageError(
1044
+ "Please provide a patch number using --patch or use --create-version flag to auto create a patch."
1045
+ )
1046
+
1047
+ versions_info = get_snowflake_facade().show_versions(self.name, self.role)
1048
+
1049
+ available_patches = [
1050
+ version_info["patch"]
1051
+ for version_info in versions_info
1052
+ if version_info["version"] == unquote_identifier(version)
1053
+ ]
1054
+
1055
+ if not available_patches:
1056
+ raise ClickException(
1057
+ f"Version {version} does not exist in application package {self.name}. Use --create-version flag to create a new version."
1058
+ )
1059
+
1060
+ if patch not in available_patches:
1061
+ raise ClickException(
1062
+ f"Patch {patch} does not exist for version {version} in application package {self.name}. Use --create-version flag to add a new patch."
1063
+ )
1064
+
1065
+ available_release_channels = get_snowflake_facade().show_release_channels(
1066
+ self.name, self.role
1067
+ )
1068
+
1069
+ release_channel = self.get_sanitized_release_channel(
1070
+ release_channel, available_release_channels
1071
+ )
1072
+
1073
+ if release_channel:
1074
+ release_channel_info = {}
1075
+ for channel_info in available_release_channels:
1076
+ if channel_info["name"] == unquote_identifier(release_channel):
1077
+ release_channel_info = channel_info
1078
+ break
1079
+
1080
+ versions_in_channel = release_channel_info["versions"]
1081
+ if unquote_identifier(version) not in release_channel_info["versions"]:
1082
+ if len(versions_in_channel) >= MAX_VERSIONS_IN_RELEASE_CHANNEL:
1083
+ # If we hit the maximum allowable versions in a release channel, we need to remove one version to make space for the new version
1084
+ all_release_directives = (
1085
+ get_snowflake_facade().show_release_directives(
1086
+ package_name=self.name,
1087
+ role=self.role,
1088
+ release_channel=release_channel,
1089
+ )
1090
+ )
1091
+
1092
+ # check which versions are attached to any release directive
1093
+ targeted_versions = {d["version"] for d in all_release_directives}
1094
+
1095
+ free_versions = {
1096
+ v for v in versions_in_channel if v not in targeted_versions
1097
+ }
1098
+
1099
+ if not free_versions:
1100
+ raise ClickException(
1101
+ f"Maximum number of versions in release channel {release_channel} reached. Cannot add more versions."
1102
+ )
1103
+
1104
+ version_to_remove = self._find_version_with_no_recent_update(
1105
+ versions_info, free_versions
1106
+ )
1107
+ user_prompt = f"Maximum number of versions in release channel reached. Would you like to remove version {version_to_remove} to make space for version {version}?"
1108
+ if not policy.should_proceed(user_prompt):
1109
+ raise ClickException(
1110
+ "Cannot proceed with publishing the new version. Please remove an existing version from the release channel to make space for the new version, or use --force to automatically clean up unused versions."
1111
+ )
1112
+
1113
+ console.warning(
1114
+ f"Maximum number of versions in release channel reached. Removing version {version_to_remove} from release_channel {release_channel} to make space for version {version}."
1115
+ )
1116
+ get_snowflake_facade().remove_version_from_release_channel(
1117
+ package_name=self.name,
1118
+ release_channel=release_channel,
1119
+ version=version_to_remove,
1120
+ role=self.role,
1121
+ )
1122
+
1123
+ get_snowflake_facade().add_version_to_release_channel(
1124
+ package_name=self.name,
1125
+ release_channel=release_channel,
1126
+ version=version,
1127
+ role=self.role,
1128
+ )
1129
+
1130
+ get_snowflake_facade().set_release_directive(
1131
+ package_name=self.name,
1132
+ release_directive=release_directive,
1133
+ release_channel=release_channel,
1134
+ target_accounts=None,
1135
+ version=version,
1136
+ patch=patch,
1137
+ role=self.role,
1138
+ )
1139
+ return VersionInfo(version, patch, None)
1140
+
1141
+ def _bundle_children(self, action_ctx: ActionContext) -> List[str]:
1142
+ # Create _children directory
1143
+ children_artifacts_dir = self.children_artifacts_deploy_root
1144
+ os.makedirs(children_artifacts_dir)
1145
+ children_sql = []
1146
+ for child in self._entity_model.children:
1147
+ # Create child sub directory
1148
+ child_artifacts_dir = children_artifacts_dir / sanitize_dir_name(
1149
+ child.target
1150
+ )
1151
+ try:
1152
+ os.makedirs(child_artifacts_dir)
1153
+ except FileExistsError:
1154
+ raise ClickException(
1155
+ f"Could not create sub-directory at {child_artifacts_dir}. Make sure child entity names do not collide with each other."
1156
+ )
1157
+ child_entity: ApplicationPackageChildInterface = action_ctx.get_entity(
1158
+ child.target
1159
+ )
1160
+ child_entity.bundle(child_artifacts_dir)
1161
+ app_role = (
1162
+ to_identifier(
1163
+ child.ensure_usable_by.application_roles.pop() # TODO Support more than one application role
1164
+ )
1165
+ if child.ensure_usable_by and child.ensure_usable_by.application_roles
1166
+ else None
1167
+ )
1168
+ child_schema = (
1169
+ to_identifier(child.identifier.schema_)
1170
+ if child.identifier and child.identifier.schema_
1171
+ else None
1172
+ )
1173
+ children_sql.append(
1174
+ child_entity.get_deploy_sql(
1175
+ artifacts_dir=child_artifacts_dir.relative_to(self.deploy_root),
1176
+ schema=child_schema,
1177
+ # TODO Allow users to override the hard-coded value for specific children
1178
+ replace=True,
1179
+ )
1180
+ )
1181
+ if app_role:
1182
+ children_sql.append(
1183
+ f"CREATE APPLICATION ROLE IF NOT EXISTS {app_role};"
1184
+ )
1185
+ if child_schema:
1186
+ children_sql.append(
1187
+ f"GRANT USAGE ON SCHEMA {child_schema} TO APPLICATION ROLE {app_role};"
1188
+ )
1189
+ children_sql.append(
1190
+ child_entity.get_usage_grant_sql(
1191
+ app_role=app_role, schema=child_schema
1192
+ )
1193
+ )
1194
+ return children_sql
1195
+
555
1196
  def _deploy(
556
1197
  self,
1198
+ action_ctx: ActionContext,
557
1199
  bundle_map: BundleMap | None,
558
1200
  prune: bool,
559
1201
  recursive: bool,
@@ -578,7 +1220,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
578
1220
  stage_fqn = stage_fqn or self.stage_fqn
579
1221
 
580
1222
  # 1. Create a bundle if one wasn't passed in
581
- bundle_map = bundle_map or self._bundle()
1223
+ bundle_map = bundle_map or self._bundle(action_ctx)
582
1224
 
583
1225
  # 2. Create an empty application package, if none exists
584
1226
  try:
@@ -610,6 +1252,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
610
1252
 
611
1253
  if validate:
612
1254
  self.validate_setup_script(
1255
+ action_ctx=action_ctx,
613
1256
  use_scratch_stage=False,
614
1257
  interactive=interactive,
615
1258
  force=force,
@@ -657,7 +1300,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
657
1300
  It executes a 'show release directives in application package' query and returns the filtered results, if they exist.
658
1301
  """
659
1302
  release_directives = get_snowflake_facade().show_release_directives(
660
- self.name, self.role
1303
+ package_name=self.name, role=self.role
661
1304
  )
662
1305
  return [
663
1306
  directive
@@ -688,9 +1331,10 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
688
1331
 
689
1332
  def add_new_patch_to_version(
690
1333
  self, version: str, patch: int | None = None, label: str | None = None
691
- ):
1334
+ ) -> int:
692
1335
  """
693
1336
  Add a new patch, optionally a custom one, to an existing version in an application package.
1337
+ Returns the patch number of the newly created patch.
694
1338
  """
695
1339
  console = self._workspace_ctx.console
696
1340
 
@@ -710,6 +1354,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
710
1354
  console.message(
711
1355
  f"Patch {new_patch}{with_label_prompt} created for version {version} defined in application package {self.name}."
712
1356
  )
1357
+ return new_patch
713
1358
 
714
1359
  def check_index_changes_in_git_repo(
715
1360
  self, policy: PolicyBase, interactive: bool
@@ -818,6 +1463,28 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
818
1463
  return False
819
1464
  return True
820
1465
 
1466
+ def _get_enable_release_channels_flag(self) -> Optional[bool]:
1467
+ """
1468
+ Returns the requested value of enable_release_channels flag for the application package.
1469
+ It retrieves the value from the configuration file and checks that the feature is enabled in the account.
1470
+ If return value is None, it means do not explicitly set the flag.
1471
+ """
1472
+ feature_flag_from_config = FeatureFlag.ENABLE_RELEASE_CHANNELS.get_value()
1473
+ feature_enabled_in_account = (
1474
+ get_snowflake_facade().get_ui_parameter(
1475
+ UIParameter.NA_FEATURE_RELEASE_CHANNELS, "ENABLED"
1476
+ )
1477
+ == "ENABLED"
1478
+ )
1479
+
1480
+ if feature_flag_from_config is not None and not feature_enabled_in_account:
1481
+ self._workspace_ctx.console.warning(
1482
+ f"Ignoring feature flag {FeatureFlag.ENABLE_RELEASE_CHANNELS.name} because release channels are not enabled in the current account."
1483
+ )
1484
+ return None
1485
+
1486
+ return feature_flag_from_config
1487
+
821
1488
  def create_app_package(self) -> None:
822
1489
  """
823
1490
  Creates the application package with our up-to-date stage if none exists.
@@ -845,21 +1512,23 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
845
1512
  if row_comment not in ALLOWED_SPECIAL_COMMENTS:
846
1513
  raise ApplicationPackageAlreadyExistsError(self.name)
847
1514
 
1515
+ # 4. Update the application package with setting enable_release_channels if necessary
1516
+ get_snowflake_facade().alter_application_package_properties(
1517
+ package_name=self.name,
1518
+ enable_release_channels=self._get_enable_release_channels_flag(),
1519
+ role=self.role,
1520
+ )
1521
+
848
1522
  return
849
1523
 
850
1524
  # If no application package pre-exists, create an application package, with the specified distribution in the project definition file.
851
- sql_executor = get_sql_executor()
852
- with sql_executor.use_role(self.role):
853
- console.step(f"Creating new application package {self.name} in account.")
854
- sql_executor.execute_query(
855
- dedent(
856
- f"""\
857
- create application package {self.name}
858
- comment = {SPECIAL_COMMENT}
859
- distribution = {model.distribution}
860
- """
861
- )
862
- )
1525
+ console.step(f"Creating new application package {self.name} in account.")
1526
+ get_snowflake_facade().create_application_package(
1527
+ role=self.role,
1528
+ enable_release_channels=self._get_enable_release_channels_flag(),
1529
+ distribution=model.distribution,
1530
+ package_name=self.name,
1531
+ )
863
1532
 
864
1533
  def execute_post_deploy_hooks(self):
865
1534
  execute_post_deploy_hooks(
@@ -873,7 +1542,11 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
873
1542
  )
874
1543
 
875
1544
  def validate_setup_script(
876
- self, use_scratch_stage: bool, interactive: bool, force: bool
1545
+ self,
1546
+ action_ctx: ActionContext,
1547
+ use_scratch_stage: bool,
1548
+ interactive: bool,
1549
+ force: bool,
877
1550
  ):
878
1551
  workspace_ctx = self._workspace_ctx
879
1552
  console = workspace_ctx.console
@@ -881,6 +1554,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
881
1554
  """Validates Native App setup script SQL."""
882
1555
  with console.phase(f"Validating Snowflake Native App setup script."):
883
1556
  validation_result = self.get_validation_result(
1557
+ action_ctx=action_ctx,
884
1558
  use_scratch_stage=use_scratch_stage,
885
1559
  force=force,
886
1560
  interactive=interactive,
@@ -900,14 +1574,20 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
900
1574
  if validation_result["status"] == "FAIL":
901
1575
  raise SetupScriptFailedValidation()
902
1576
 
1577
+ @span("validate_setup_script")
903
1578
  def get_validation_result(
904
- self, use_scratch_stage: bool, interactive: bool, force: bool
1579
+ self,
1580
+ action_ctx: ActionContext,
1581
+ use_scratch_stage: bool,
1582
+ interactive: bool,
1583
+ force: bool,
905
1584
  ):
906
1585
  """Call system$validate_native_app_setup() to validate deployed Native App setup script."""
907
1586
  stage_fqn = self.stage_fqn
908
1587
  if use_scratch_stage:
909
1588
  stage_fqn = self.scratch_stage_fqn
910
1589
  self._deploy(
1590
+ action_ctx=action_ctx,
911
1591
  bundle_map=None,
912
1592
  prune=True,
913
1593
  recursive=True,
@@ -951,7 +1631,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
951
1631
  bundle_map: BundleMap | None,
952
1632
  policy: PolicyBase,
953
1633
  interactive: bool,
954
- ):
1634
+ ) -> VersionInfo:
955
1635
  """Determine version name, patch number, and label from CLI provided values and manifest.yml version entry.
956
1636
  @param [Optional] version: version name as specified in the command
957
1637
  @param [Optional] patch: patch number as specified in the command
@@ -959,12 +1639,14 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
959
1639
  @param [Optional] bundle_map: bundle_map if a deploy_root is prepared. _bundle() is performed otherwise.
960
1640
  @param policy: CLI policy
961
1641
  @param interactive: True if command is run in interactive mode, otherwise False
1642
+
1643
+ @return VersionInfo: version_name, patch_number, label resolved from CLI and manifest.yml
962
1644
  """
963
1645
  console = self._workspace_ctx.console
964
1646
 
965
1647
  resolved_version = None
966
1648
  resolved_patch = None
967
- resolved_label = ""
1649
+ resolved_label = None
968
1650
 
969
1651
  # If version is specified in CLI, no version information from manifest.yml is used (except for comment, we can't control comment as of now).
970
1652
  if version is not None:
@@ -972,7 +1654,7 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
972
1654
  "Ignoring version information from the application manifest since a version was explicitly specified with the command."
973
1655
  )
974
1656
  resolved_patch = patch
975
- resolved_label = label if label is not None else ""
1657
+ resolved_label = label
976
1658
  resolved_version = version
977
1659
 
978
1660
  # When version is not set by CLI, version name is read from manifest.yml. patch and label from CLI will be used, if provided.
@@ -1026,17 +1708,18 @@ class ApplicationPackageEntity(EntityBase[ApplicationPackageEntityModel]):
1026
1708
  resolved_label = label if label is not None else label_manifest
1027
1709
 
1028
1710
  # Check if patch needs to throw a bad option error, either if application package does not exist or if version does not exist
1029
- if resolved_patch is not None:
1711
+ # If patch is 0 and version does not exist, it is a valid case, because patch 0 is the first patch in a version.
1712
+ if resolved_patch:
1030
1713
  try:
1031
- if not self.get_existing_version_info(resolved_version):
1714
+ if not self.get_existing_version_info(to_identifier(resolved_version)):
1032
1715
  raise BadOptionUsage(
1033
1716
  option_name="patch",
1034
- message=f"Cannot create patch {resolved_patch} when version {resolved_version} is not defined in the application package {self.name}. Try again without specifying a patch.",
1717
+ message=f"Version {resolved_version} is not defined in the application package {self.name}. Try again with a patch of 0 or without specifying any patch.",
1035
1718
  )
1036
1719
  except ApplicationPackageDoesNotExistError as app_err:
1037
1720
  raise BadOptionUsage(
1038
1721
  option_name="patch",
1039
- message=f"Cannot create patch {resolved_patch} when application package {self.name} does not exist. Try again without specifying a patch.",
1722
+ message=f"Application package {self.name} does not exist yet. Try again with a patch of 0 or without specifying any patch.",
1040
1723
  )
1041
1724
 
1042
1725
  return VersionInfo(