snowflake-cli-labs 2.5.0rc3__py3-none-any.whl → 2.6.0rc0__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 (67) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/cli_global_context.py +31 -3
  3. snowflake/cli/api/commands/decorators.py +21 -6
  4. snowflake/cli/api/commands/flags.py +60 -51
  5. snowflake/cli/api/commands/snow_typer.py +24 -0
  6. snowflake/cli/api/commands/typer_pre_execute.py +26 -0
  7. snowflake/cli/api/console/abc.py +8 -0
  8. snowflake/cli/api/console/console.py +29 -4
  9. snowflake/cli/api/constants.py +3 -0
  10. snowflake/cli/api/project/definition.py +17 -35
  11. snowflake/cli/api/project/definition_manager.py +22 -19
  12. snowflake/cli/api/project/errors.py +9 -6
  13. snowflake/cli/api/project/schemas/identifier_model.py +1 -1
  14. snowflake/cli/api/project/schemas/native_app/application.py +15 -3
  15. snowflake/cli/api/project/schemas/native_app/native_app.py +5 -1
  16. snowflake/cli/api/project/schemas/native_app/path_mapping.py +14 -3
  17. snowflake/cli/api/project/schemas/project_definition.py +37 -6
  18. snowflake/cli/api/project/schemas/streamlit/streamlit.py +3 -0
  19. snowflake/cli/api/project/schemas/updatable_model.py +2 -6
  20. snowflake/cli/api/rest_api.py +113 -0
  21. snowflake/cli/api/sanitizers.py +43 -0
  22. snowflake/cli/api/sql_execution.py +7 -0
  23. snowflake/cli/api/utils/definition_rendering.py +95 -25
  24. snowflake/cli/api/utils/models.py +31 -26
  25. snowflake/cli/api/utils/rendering.py +24 -3
  26. snowflake/cli/app/cli_app.py +2 -0
  27. snowflake/cli/app/commands_registration/command_plugins_loader.py +8 -0
  28. snowflake/cli/app/dev/docs/commands_docs_generator.py +100 -0
  29. snowflake/cli/app/dev/docs/generator.py +8 -67
  30. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +58 -0
  31. snowflake/cli/app/dev/docs/project_definition_generate_json_schema.py +227 -0
  32. snowflake/cli/app/dev/docs/template_utils.py +23 -0
  33. snowflake/cli/app/dev/docs/templates/definition_description.rst.jinja2 +38 -0
  34. snowflake/cli/app/dev/docs/templates/usage.rst.jinja2 +6 -1
  35. snowflake/cli/app/loggers.py +25 -0
  36. snowflake/cli/app/printing.py +7 -5
  37. snowflake/cli/app/telemetry.py +11 -0
  38. snowflake/cli/plugins/nativeapp/artifacts.py +78 -9
  39. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +3 -11
  40. snowflake/cli/plugins/nativeapp/codegen/compiler.py +6 -24
  41. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +27 -27
  42. snowflake/cli/plugins/nativeapp/commands.py +23 -12
  43. snowflake/cli/plugins/nativeapp/constants.py +2 -0
  44. snowflake/cli/plugins/nativeapp/errno.py +15 -0
  45. snowflake/cli/plugins/nativeapp/feature_flags.py +24 -0
  46. snowflake/cli/plugins/nativeapp/init.py +5 -0
  47. snowflake/cli/plugins/nativeapp/manager.py +101 -103
  48. snowflake/cli/plugins/nativeapp/project_model.py +181 -0
  49. snowflake/cli/plugins/nativeapp/run_processor.py +178 -110
  50. snowflake/cli/plugins/nativeapp/teardown_processor.py +89 -64
  51. snowflake/cli/plugins/nativeapp/utils.py +2 -2
  52. snowflake/cli/plugins/nativeapp/version/commands.py +3 -3
  53. snowflake/cli/plugins/object/commands.py +69 -3
  54. snowflake/cli/plugins/object/manager.py +44 -3
  55. snowflake/cli/plugins/snowpark/commands.py +2 -2
  56. snowflake/cli/plugins/sql/commands.py +2 -10
  57. snowflake/cli/plugins/sql/manager.py +4 -2
  58. snowflake/cli/plugins/stage/commands.py +23 -4
  59. snowflake/cli/plugins/stage/diff.py +81 -51
  60. snowflake/cli/plugins/stage/manager.py +2 -1
  61. snowflake/cli/plugins/streamlit/commands.py +2 -1
  62. snowflake/cli/plugins/streamlit/manager.py +6 -0
  63. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0rc0.dist-info}/METADATA +15 -9
  64. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0rc0.dist-info}/RECORD +67 -56
  65. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0rc0.dist-info}/WHEEL +1 -1
  66. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0rc0.dist-info}/entry_points.txt +0 -0
  67. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -28,10 +28,13 @@ from snowflake.cli.api.project.util import (
28
28
  unquote_identifier,
29
29
  )
30
30
  from snowflake.cli.api.utils.cursor import find_all_rows
31
+ from snowflake.cli.api.utils.rendering import snowflake_sql_jinja_render
31
32
  from snowflake.cli.plugins.nativeapp.artifacts import BundleMap
32
33
  from snowflake.cli.plugins.nativeapp.constants import (
33
34
  ALLOWED_SPECIAL_COMMENTS,
34
35
  COMMENT_COL,
36
+ ERROR_MESSAGE_093079,
37
+ ERROR_MESSAGE_093128,
35
38
  LOOSE_FILES_MAGIC_VERSION,
36
39
  PATCH_COL,
37
40
  SPECIAL_COMMENT,
@@ -48,99 +51,117 @@ from snowflake.cli.plugins.nativeapp.manager import (
48
51
  generic_sql_error_handler,
49
52
  )
50
53
  from snowflake.cli.plugins.nativeapp.policy import PolicyBase
51
- from snowflake.cli.plugins.stage.diff import DiffResult
54
+ from snowflake.cli.plugins.nativeapp.project_model import (
55
+ NativeAppProjectModel,
56
+ )
52
57
  from snowflake.cli.plugins.stage.manager import StageManager
53
58
  from snowflake.connector import ProgrammingError
54
59
  from snowflake.connector.cursor import DictCursor, SnowflakeCursor
55
60
 
56
- UPGRADE_RESTRICTION_CODES = {93044, 93055, 93045, 93046}
61
+ # Reasons why an `alter application ... upgrade` might fail
62
+ UPGRADE_RESTRICTION_CODES = {
63
+ 93044, # Cannot upgrade dev mode application from loose stage files to version
64
+ 93045, # Cannot upgrade dev mode application from version to loose stage files
65
+ 93046, # Operation only permitted on dev mode application
66
+ 93055, # Operation not supported on dev mode application
67
+ 93079, # App package access lost
68
+ }
57
69
 
58
70
 
59
- class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
60
- def __init__(self, project_definition: NativeApp, project_root: Path):
61
- super().__init__(project_definition, project_root)
71
+ class SameAccountInstallMethod:
72
+ _requires_created_by_cli: bool
73
+ _from_release_directive: bool
74
+ version: Optional[str]
75
+ patch: Optional[int]
62
76
 
63
- def _create_dev_app(self, diff: DiffResult) -> None:
64
- """
65
- (Re-)creates the application object with our up-to-date stage.
66
- """
67
- with self.use_role(self.app_role):
77
+ def __init__(
78
+ self,
79
+ requires_created_by_cli: bool,
80
+ version: Optional[str] = None,
81
+ patch: Optional[int] = None,
82
+ from_release_directive: bool = False,
83
+ ):
84
+ self._requires_created_by_cli = requires_created_by_cli
85
+ self.version = version
86
+ self.patch = patch
87
+ self._from_release_directive = from_release_directive
68
88
 
69
- # 1. Need to use a warehouse to create an application object
70
- try:
71
- if self.application_warehouse:
72
- self._execute_query(f"use warehouse {self.application_warehouse}")
73
- except ProgrammingError as err:
74
- generic_sql_error_handler(
75
- err=err, role=self.app_role, warehouse=self.application_warehouse
76
- )
89
+ @classmethod
90
+ def unversioned_dev(cls):
91
+ """aka. stage dev aka loose files"""
92
+ return cls(True)
77
93
 
78
- # 2. Check for an existing application object by the same name
79
- show_app_row = self.get_existing_app_info()
94
+ @classmethod
95
+ def versioned_dev(cls, version: str, patch: Optional[int] = None):
96
+ return cls(False, version, patch)
80
97
 
81
- # 3. If existing application object is found, perform a few validations and upgrade the application object.
82
- if show_app_row:
98
+ @classmethod
99
+ def release_directive(cls):
100
+ return cls(False, from_release_directive=True)
83
101
 
84
- # Check if not created by Snowflake CLI or not created using "files on a named stage" / stage dev mode.
85
- if show_app_row[COMMENT_COL] not in ALLOWED_SPECIAL_COMMENTS or (
86
- show_app_row[VERSION_COL] != LOOSE_FILES_MAGIC_VERSION
87
- ):
88
- raise ApplicationAlreadyExistsError(self.app_name)
102
+ @property
103
+ def is_dev_mode(self) -> bool:
104
+ return not self._from_release_directive
89
105
 
90
- # Check for the right owner
91
- ensure_correct_owner(
92
- row=show_app_row, role=self.app_role, obj_name=self.app_name
93
- )
106
+ def using_clause(self, app: NativeAppProjectModel) -> str:
107
+ if self._from_release_directive:
108
+ return ""
94
109
 
95
- # If all the above checks are in order, proceed to upgrade
96
- try:
97
- cc.step(f"Upgrading existing application object {self.app_name}.")
98
- self._execute_query(
99
- f"alter application {self.app_name} upgrade using @{self.stage_fqn}"
100
- )
110
+ if self.version:
111
+ patch_clause = f"patch {self.patch}" if self.patch else ""
112
+ return f"using version {self.version} {patch_clause}"
101
113
 
102
- # ensure debug_mode is up-to-date
103
- self._execute_query(
104
- f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
105
- )
114
+ stage_name = StageManager.quote_stage_name(app.stage_fqn)
115
+ return f"using {stage_name}"
106
116
 
107
- return
117
+ def ensure_app_usable(self, app: NativeAppProjectModel, show_app_row: dict):
118
+ """Raise an exception if we cannot proceed with install given the pre-existing application object"""
108
119
 
109
- except ProgrammingError as err:
110
- generic_sql_error_handler(err)
120
+ if self._requires_created_by_cli:
121
+ if show_app_row[COMMENT_COL] not in ALLOWED_SPECIAL_COMMENTS or (
122
+ show_app_row[VERSION_COL] != LOOSE_FILES_MAGIC_VERSION
123
+ ):
124
+ # this application object was not created by this tooling
125
+ raise ApplicationAlreadyExistsError(app.app_name)
111
126
 
112
- # 4. If no existing application object is found, create an application object using "files on a named stage" / stage dev mode.
113
- cc.step(f"Creating new application {self.app_name} in account.")
127
+ # expected owner
128
+ ensure_correct_owner(row=show_app_row, role=app.app_role, obj_name=app.app_name)
114
129
 
115
- if self.app_role != self.package_role:
116
- with self.use_role(new_role=self.package_role):
117
- self._execute_queries(
118
- dedent(
119
- f"""\
120
- grant install, develop on application package {self.package_name} to role {self.app_role};
121
- grant usage on schema {self.package_name}.{self.stage_schema} to role {self.app_role};
122
- grant read on stage {self.stage_fqn} to role {self.app_role};
123
- """
124
- )
125
- )
126
130
 
127
- stage_name = StageManager.quote_stage_name(self.stage_fqn)
131
+ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
132
+ def __init__(self, project_definition: NativeApp, project_root: Path):
133
+ super().__init__(project_definition, project_root)
128
134
 
135
+ def _execute_sql_script(self, sql_script_path):
136
+ """
137
+ Executing the SQL script in the provided file path after expanding template variables.
138
+ "use warehouse" and "use database" will be executed first if they are set in definition file or in the current connection.
139
+ """
140
+ with open(sql_script_path) as f:
141
+ sql_script = f.read()
129
142
  try:
130
- self._execute_query(
131
- dedent(
132
- f"""\
133
- create application {self.app_name}
134
- from application package {self.package_name}
135
- using {stage_name}
136
- debug_mode = {self.debug_mode}
137
- comment = {SPECIAL_COMMENT}
138
- """
139
- )
140
- )
143
+ if self.application_warehouse:
144
+ self._execute_query(f"use warehouse {self.application_warehouse}")
145
+ if self._conn.database:
146
+ self._execute_query(f"use database {self._conn.database}")
147
+ sql_script = snowflake_sql_jinja_render(content=sql_script)
148
+ self._execute_queries(sql_script)
141
149
  except ProgrammingError as err:
142
150
  generic_sql_error_handler(err)
143
151
 
152
+ def _execute_post_deploy_hooks(self):
153
+ post_deploy_script_hooks = self.app_post_deploy_hooks
154
+ if post_deploy_script_hooks:
155
+ with cc.phase("Executing application post-deploy actions"):
156
+ for hook in post_deploy_script_hooks:
157
+ if hook.sql_script:
158
+ cc.step(f"Executing SQL script: {hook.sql_script}")
159
+ self._execute_sql_script(hook.sql_script)
160
+ else:
161
+ raise ValueError(
162
+ f"Unsupported application post-deploy hook type: {hook}"
163
+ )
164
+
144
165
  def get_all_existing_versions(self) -> SnowflakeCursor:
145
166
  """
146
167
  Get all existing versions, if defined, for an application package.
@@ -186,11 +207,31 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
186
207
  generic_sql_error_handler(err=err, role=self.package_role)
187
208
  return None
188
209
 
189
- def drop_application_before_upgrade(self, policy: PolicyBase, is_interactive: bool):
210
+ def drop_application_before_upgrade(
211
+ self, policy: PolicyBase, is_interactive: bool, cascade: bool = False
212
+ ):
190
213
  """
191
214
  This method will attempt to drop an application object if a previous upgrade fails.
192
215
  """
193
- user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?"
216
+ if cascade:
217
+ try:
218
+ if application_objects := self.get_objects_owned_by_application():
219
+ application_objects_str = self._application_objects_to_str(
220
+ application_objects
221
+ )
222
+ cc.message(
223
+ f"The following objects are owned by application {self.app_name} and need to be dropped:\n{application_objects_str}"
224
+ )
225
+ except ProgrammingError as err:
226
+ if err.errno != 93079 and ERROR_MESSAGE_093079 not in err.msg:
227
+ generic_sql_error_handler(err)
228
+ cc.warning(
229
+ "The application owns other objects but they could not be determined."
230
+ )
231
+ user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?"
232
+ else:
233
+ user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?"
234
+
194
235
  if not policy.should_proceed(user_prompt):
195
236
  if is_interactive:
196
237
  cc.message("Not upgrading the application object.")
@@ -201,21 +242,23 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
201
242
  )
202
243
  raise typer.Exit(1)
203
244
  try:
204
- self._execute_query(f"drop application {self.app_name}")
245
+ cascade_sql = " cascade" if cascade else ""
246
+ self._execute_query(f"drop application {self.app_name}{cascade_sql}")
205
247
  except ProgrammingError as err:
206
- generic_sql_error_handler(err)
248
+ if (err.errno == 93128 or ERROR_MESSAGE_093128 in err.msg) and not cascade:
249
+ # We need to cascade the deletion, let's try again (only if we didn't try with cascade already)
250
+ return self.drop_application_before_upgrade(
251
+ policy, is_interactive, cascade=True
252
+ )
253
+ else:
254
+ generic_sql_error_handler(err)
207
255
 
208
- def upgrade_app(
256
+ def create_or_upgrade_app(
209
257
  self,
210
258
  policy: PolicyBase,
211
- is_interactive: bool,
212
- version: Optional[str] = None,
213
- patch: Optional[int] = None,
259
+ install_method: SameAccountInstallMethod,
260
+ is_interactive: bool = False,
214
261
  ):
215
-
216
- patch_clause = f"patch {patch}" if patch else ""
217
- using_clause = f"using version {version} {patch_clause}" if version else ""
218
-
219
262
  with self.use_role(self.app_role):
220
263
 
221
264
  # 1. Need to use a warehouse to create an application object
@@ -233,23 +276,25 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
233
276
  # 3. If existing application is found, perform a few validations and upgrade the application object.
234
277
  if show_app_row:
235
278
 
236
- # We skip comment check here, because prod/pre-existing application objects may not be created by the Snowflake CLI.
237
- # Check for the right owner
238
- ensure_correct_owner(
239
- row=show_app_row, role=self.app_role, obj_name=self.app_name
240
- )
279
+ install_method.ensure_app_usable(self._na_project, show_app_row)
241
280
 
242
281
  # If all the above checks are in order, proceed to upgrade
243
282
  try:
283
+ cc.step(f"Upgrading existing application object {self.app_name}.")
284
+ using_clause = install_method.using_clause(self._na_project)
244
285
  self._execute_query(
245
286
  f"alter application {self.app_name} upgrade {using_clause}"
246
287
  )
247
288
 
248
- # ensure debug_mode is up-to-date
249
- if using_clause:
250
- self._execute_query(
251
- f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
252
- )
289
+ if install_method.is_dev_mode:
290
+ # if debug_mode is present (controlled), ensure it is up-to-date
291
+ if self.debug_mode is not None:
292
+ self._execute_query(
293
+ f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
294
+ )
295
+
296
+ # hooks always executed after a create or upgrade
297
+ self._execute_post_deploy_hooks()
253
298
  return
254
299
 
255
300
  except ProgrammingError as err:
@@ -263,31 +308,41 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
263
308
  cc.step(f"Creating new application object {self.app_name} in account.")
264
309
 
265
310
  if self.app_role != self.package_role:
266
- with self.use_role(new_role=self.package_role):
311
+ with self.use_role(self.package_role):
267
312
  self._execute_query(
268
- f"grant install on application package {self.package_name} to role {self.app_role}"
313
+ f"grant install, develop on application package {self.package_name} to role {self.app_role}"
314
+ )
315
+ self._execute_query(
316
+ f"grant usage on schema {self.package_name}.{self.stage_schema} to role {self.app_role}"
317
+ )
318
+ self._execute_query(
319
+ f"grant read on stage {self.stage_fqn} to role {self.app_role}"
269
320
  )
270
- if version:
271
- self._execute_query(
272
- f"grant develop on application package {self.package_name} to role {self.app_role}"
273
- )
274
321
 
275
322
  try:
323
+ # by default, applications are created in debug mode when possible;
324
+ # this can be overridden in the project definition
325
+ debug_mode_clause = ""
326
+ if install_method.is_dev_mode:
327
+ initial_debug_mode = (
328
+ self.debug_mode if self.debug_mode is not None else True
329
+ )
330
+ debug_mode_clause = f"debug_mode = {initial_debug_mode}"
331
+
332
+ using_clause = install_method.using_clause(self._na_project)
276
333
  self._execute_query(
277
334
  dedent(
278
335
  f"""\
279
336
  create application {self.app_name}
280
- from application package {self.package_name} {using_clause}
337
+ from application package {self.package_name} {using_clause} {debug_mode_clause}
281
338
  comment = {SPECIAL_COMMENT}
282
339
  """
283
340
  )
284
341
  )
285
342
 
286
- # ensure debug_mode is up-to-date
287
- if using_clause:
288
- self._execute_query(
289
- f"alter application {self.app_name} set debug_mode = {self.debug_mode}"
290
- )
343
+ # hooks always executed after a create or upgrade
344
+ self._execute_post_deploy_hooks()
345
+
291
346
  except ProgrammingError as err:
292
347
  generic_sql_error_handler(err)
293
348
 
@@ -303,12 +358,21 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
303
358
  *args,
304
359
  **kwargs,
305
360
  ):
306
- """app run process"""
361
+ """
362
+ Create or upgrade the application object using the given strategy
363
+ (unversioned dev, versioned dev, or same-account release directive).
364
+ """
307
365
 
366
+ # same-account release directive
308
367
  if from_release_directive:
309
- self.upgrade_app(policy=policy, is_interactive=is_interactive)
368
+ self.create_or_upgrade_app(
369
+ policy=policy,
370
+ is_interactive=is_interactive,
371
+ install_method=SameAccountInstallMethod.release_directive(),
372
+ )
310
373
  return
311
374
 
375
+ # versioned dev
312
376
  if version:
313
377
  try:
314
378
  version_exists = self.get_existing_version_info(version)
@@ -321,15 +385,19 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
321
385
  f"Application package {self.package_name} does not exist. Use 'snow app version create' to first create an application package and then define a version in it."
322
386
  )
323
387
 
324
- self.upgrade_app(
388
+ self.create_or_upgrade_app(
325
389
  policy=policy,
326
- version=version,
327
- patch=patch,
390
+ install_method=SameAccountInstallMethod.versioned_dev(version, patch),
328
391
  is_interactive=is_interactive,
329
392
  )
330
393
  return
331
394
 
332
- diff = self.deploy(
395
+ # unversioned dev
396
+ self.deploy(
333
397
  bundle_map=bundle_map, prune=True, recursive=True, validate=validate
334
398
  )
335
- self._create_dev_app(diff)
399
+ self.create_or_upgrade_app(
400
+ policy=policy,
401
+ is_interactive=is_interactive,
402
+ install_method=SameAccountInstallMethod.unversioned_dev(),
403
+ )
@@ -28,11 +28,11 @@ from snowflake.cli.plugins.nativeapp.constants import (
28
28
  INTERNAL_DISTRIBUTION,
29
29
  OWNER_COL,
30
30
  )
31
+ from snowflake.cli.plugins.nativeapp.errno import APPLICATION_NO_LONGER_AVAILABLE
31
32
  from snowflake.cli.plugins.nativeapp.exceptions import (
32
33
  CouldNotDropApplicationPackageWithVersions,
33
34
  )
34
35
  from snowflake.cli.plugins.nativeapp.manager import (
35
- ApplicationOwnedObject,
36
36
  NativeAppCommandProcessor,
37
37
  NativeAppManager,
38
38
  ensure_correct_owner,
@@ -40,6 +40,7 @@ from snowflake.cli.plugins.nativeapp.manager import (
40
40
  from snowflake.cli.plugins.nativeapp.utils import (
41
41
  needs_confirmation,
42
42
  )
43
+ from snowflake.connector import ProgrammingError
43
44
  from snowflake.connector.cursor import DictCursor
44
45
 
45
46
 
@@ -65,20 +66,6 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
65
66
 
66
67
  cc.message(f"Dropped {object_type} {object_name} successfully.")
67
68
 
68
- def _application_objects_to_str(
69
- self, application_objects: ApplicationOwnedObject
70
- ) -> str:
71
- """
72
- Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
73
- (COMPUTE_POOL) POOL_NAME
74
- (DATABASE) DB_NAME
75
- (SCHEMA) DB_NAME.PUBLIC
76
- ...
77
- """
78
- return "\n".join(
79
- [f"({obj['type']}) {obj['name']}" for obj in application_objects]
80
- )
81
-
82
69
  def drop_application(
83
70
  self, auto_yes: bool, interactive: bool = False, cascade: Optional[bool] = None
84
71
  ):
@@ -88,7 +75,7 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
88
75
 
89
76
  needs_confirm = True
90
77
 
91
- # 1. If existing application package is not found, exit gracefully
78
+ # 1. If existing application is not found, exit gracefully
92
79
  show_obj_row = self.get_existing_app_info()
93
80
  if show_obj_row is None:
94
81
  cc.warning(
@@ -103,64 +90,102 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
103
90
 
104
91
  # 3. Check if created by the Snowflake CLI
105
92
  row_comment = show_obj_row[COMMENT_COL]
106
- if row_comment in ALLOWED_SPECIAL_COMMENTS:
107
- # No confirmation needed before dropping
108
- needs_confirm = False
109
- else:
110
- if needs_confirmation(needs_confirm, auto_yes):
111
- should_drop_object = typer.confirm(
112
- dedent(
113
- f"""\
114
- Application object {self.app_name} was not created by Snowflake CLI.
115
- Application object details:
116
- Name: {self.app_name}
117
- Created on: {show_obj_row["created_on"]}
118
- Source: {show_obj_row["source"]}
119
- Owner: {show_obj_row[OWNER_COL]}
120
- Comment: {show_obj_row[COMMENT_COL]}
121
- Version: {show_obj_row["version"]}
122
- Patch: {show_obj_row["patch"]}
123
- Are you sure you want to drop it?
124
- """
125
- )
93
+ if row_comment not in ALLOWED_SPECIAL_COMMENTS and needs_confirmation(
94
+ needs_confirm, auto_yes
95
+ ):
96
+ should_drop_object = typer.confirm(
97
+ dedent(
98
+ f"""\
99
+ Application object {self.app_name} was not created by Snowflake CLI.
100
+ Application object details:
101
+ Name: {self.app_name}
102
+ Created on: {show_obj_row["created_on"]}
103
+ Source: {show_obj_row["source"]}
104
+ Owner: {show_obj_row[OWNER_COL]}
105
+ Comment: {show_obj_row[COMMENT_COL]}
106
+ Version: {show_obj_row["version"]}
107
+ Patch: {show_obj_row["patch"]}
108
+ Are you sure you want to drop it?
109
+ """
126
110
  )
127
- if not should_drop_object:
128
- cc.message(f"Did not drop application object {self.app_name}.")
129
- return # The user desires to keep the app, therefore exit gracefully
111
+ )
112
+ if not should_drop_object:
113
+ cc.message(f"Did not drop application object {self.app_name}.")
114
+ # The user desires to keep the app, therefore we can't proceed since it would
115
+ # leave behind an orphan app when we get to dropping the package
116
+ raise typer.Abort()
130
117
 
131
118
  # 4. Check for application objects owned by the application
132
- application_objects = self.get_objects_owned_by_application()
133
- if len(application_objects) > 0:
134
- application_objects_str = self._application_objects_to_str(
135
- application_objects
119
+ # This query will fail if the application package has already been dropped, so handle this case gracefully
120
+ has_objects_to_drop = False
121
+ message_prefix = ""
122
+ cascade_true_message = ""
123
+ cascade_false_message = ""
124
+ interactive_prompt = ""
125
+ non_interactive_abort = ""
126
+ try:
127
+ if application_objects := self.get_objects_owned_by_application():
128
+ has_objects_to_drop = True
129
+ message_prefix = (
130
+ f"The following objects are owned by application {self.app_name}"
131
+ )
132
+ cascade_true_message = f"{message_prefix} and will be dropped:"
133
+ cascade_false_message = f"{message_prefix} and will NOT be dropped:"
134
+ interactive_prompt = "Would you like to drop these objects in addition to the application? [y/n/ABORT]"
135
+ non_interactive_abort = "Re-run teardown again with --cascade or --no-cascade to specify whether these objects should be dropped along with the application"
136
+ except ProgrammingError as e:
137
+ if e.errno != APPLICATION_NO_LONGER_AVAILABLE:
138
+ raise
139
+ application_objects = []
140
+ message_prefix = f"Could not determine which objects are owned by application {self.app_name}"
141
+ has_objects_to_drop = True # potentially, but we don't know what they are
142
+ cascade_true_message = (
143
+ f"{message_prefix}, an unknown number of objects will be dropped."
136
144
  )
145
+ cascade_false_message = f"{message_prefix}, they will NOT be dropped."
146
+ interactive_prompt = f"Would you like to drop an unknown set of objects in addition to the application? [y/n/ABORT]"
147
+ non_interactive_abort = f"Re-run teardown again with --cascade or --no-cascade to specify whether any objects should be dropped along with the application."
148
+
149
+ if has_objects_to_drop:
137
150
  if cascade is True:
138
- cc.message(
139
- f"The following objects are owned by application {self.app_name} and will be dropped:\n{application_objects_str}"
140
- )
151
+ # If the user explicitly passed the --cascade flag
152
+ cc.message(cascade_true_message)
153
+ with cc.indented():
154
+ for obj in application_objects:
155
+ cc.message(self._application_object_to_str(obj))
141
156
  elif cascade is False:
142
- cc.message(
143
- f"The following objects are owned by application {self.app_name}:\n{application_objects_str}"
144
- )
157
+ # If the user explicitly passed the --no-cascade flag
158
+ cc.message(cascade_false_message)
159
+ with cc.indented():
160
+ for obj in application_objects:
161
+ cc.message(self._application_object_to_str(obj))
145
162
  elif interactive:
146
- if interactive:
147
- user_response = typer.prompt(
148
- f"The following objects are owned by application {self.app_name}:\n{application_objects_str}\n\nWould you like to drop these objects in addition to the application? [y/n/ABORT]",
149
- show_default=False,
150
- default="ABORT",
151
- )
152
- if user_response in ["y", "yes", "Y", "Yes", "YES"]:
153
- cascade = True
154
- elif user_response in ["n", "no", "N", "No", "NO"]:
155
- cascade = False
156
- else:
157
- raise typer.Abort()
163
+ # If the user didn't pass any cascade flag and the session is interactive
164
+ cc.message(message_prefix)
165
+ with cc.indented():
166
+ for obj in application_objects:
167
+ cc.message(self._application_object_to_str(obj))
168
+ user_response = typer.prompt(
169
+ interactive_prompt,
170
+ show_default=False,
171
+ default="ABORT",
172
+ ).lower()
173
+ if user_response in ["y", "yes"]:
174
+ cascade = True
175
+ elif user_response in ["n", "no"]:
176
+ cascade = False
177
+ else:
178
+ raise typer.Abort()
158
179
  else:
159
- cc.message(
160
- f"The following application objects are owned by application {self.app_name}:\n{application_objects_str}\n\nRe-run teardown again with --cascade or --no-cascade to specify whether these objects should be dropped along with the application."
161
- )
180
+ # Else abort since we don't know what to do and can't ask the user
181
+ cc.message(message_prefix)
182
+ with cc.indented():
183
+ for obj in application_objects:
184
+ cc.message(self._application_object_to_str(obj))
185
+ cc.message(non_interactive_abort)
162
186
  raise typer.Abort()
163
187
  elif cascade is None:
188
+ # If there's nothing to drop, set cascade to an explicit False value
164
189
  cascade = False
165
190
 
166
191
  # 5. All validations have passed, drop object
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  import os
18
18
  from pathlib import Path
19
19
  from sys import stdin, stdout
20
- from typing import List, Optional, Union
20
+ from typing import Iterable, Optional, Union
21
21
 
22
22
  from click import ClickException
23
23
 
@@ -85,7 +85,7 @@ def shallow_git_clone(url: Union[str, os.PathLike], to_path: Union[str, os.PathL
85
85
  return repo
86
86
 
87
87
 
88
- def verify_no_directories(paths_to_sync: List[Path]):
88
+ def verify_no_directories(paths_to_sync: Iterable[Path]):
89
89
  for path in paths_to_sync:
90
90
  if path.is_dir():
91
91
  raise ClickException(
@@ -89,7 +89,7 @@ def create(
89
89
  git_policy = AllowAlwaysPolicy()
90
90
 
91
91
  processor = NativeAppVersionCreateProcessor(
92
- project_definition=cli_context.project_definition,
92
+ project_definition=cli_context.project_definition.native_app,
93
93
  project_root=cli_context.project_root,
94
94
  )
95
95
 
@@ -115,7 +115,7 @@ def version_list(
115
115
  Lists all versions defined in an application package.
116
116
  """
117
117
  processor = NativeAppRunProcessor(
118
- project_definition=cli_context.project_definition,
118
+ project_definition=cli_context.project_definition.native_app,
119
119
  project_root=cli_context.project_root,
120
120
  )
121
121
  cursor = processor.get_all_existing_versions()
@@ -147,7 +147,7 @@ def drop(
147
147
  policy = DenyAlwaysPolicy()
148
148
 
149
149
  processor = NativeAppVersionDropProcessor(
150
- project_definition=cli_context.project_definition,
150
+ project_definition=cli_context.project_definition.native_app,
151
151
  project_root=cli_context.project_root,
152
152
  )
153
153
  processor.process(version, policy, is_interactive)