Skip to content

Commit 1abd3af

Browse files
feat: [SNOW-2412813] wip: analyze
1 parent 925c9f4 commit 1abd3af

File tree

6 files changed

+899
-1
lines changed

6 files changed

+899
-1
lines changed

src/snowflake/cli/_plugins/dcm/commands.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414
import json
1515
from pathlib import Path
16-
from typing import List, Optional
16+
from dataclasses import dataclass, field
17+
from typing import Dict, List, Optional
1718

1819
import typer
20+
from snowflake.cli._plugins.dcm.manager import AnalysisType
1921
from snowflake.cli._plugins.dcm.manager import DCMProjectManager
2022
from snowflake.cli._plugins.dcm.utils import (
2123
TestResultFormat,
@@ -56,6 +58,7 @@
5658
is_hidden=FeatureFlag.ENABLE_SNOWFLAKE_PROJECTS.is_disabled,
5759
)
5860

61+
5962
dcm_identifier = identifier_argument(sf_object="DCM Project", example="MY_PROJECT")
6063
variables_flag = variables_option(
6164
'Variables for the execution context; for example: `-D "<key>=<value>"`.'
@@ -384,6 +387,124 @@ def preview(
384387
return QueryResult(result)
385388

386389

390+
@app.command(requires_connection=True)
391+
def analyze(
392+
identifier: FQN = dcm_identifier,
393+
from_location: Optional[str] = from_option,
394+
variables: Optional[List[str]] = variables_flag,
395+
configuration: Optional[str] = configuration_flag,
396+
analysis_type: Optional[AnalysisType] = typer.Option(
397+
None,
398+
"--type",
399+
help="Type of analysis to perform.",
400+
show_default=False,
401+
case_sensitive=False,
402+
),
403+
output_path: Optional[str] = output_path_option(
404+
help="Path where the analysis result will be stored. Can be a stage path (starting with '@') or a local directory path."
405+
),
406+
**options,
407+
):
408+
"""
409+
Analyzes a DCM Project.
410+
"""
411+
manager = DCMProjectManager()
412+
effective_stage = _get_effective_stage(identifier, from_location)
413+
414+
with cli_console.spinner() as spinner:
415+
spinner.add_task(description=f"Analyzing dcm project {identifier}", total=None)
416+
result = manager.analyze(
417+
project_identifier=identifier,
418+
configuration=configuration,
419+
from_stage=effective_stage,
420+
variables=variables,
421+
analysis_type=analysis_type,
422+
output_path=output_path,
423+
)
424+
425+
row = result.fetchone()
426+
if not row:
427+
return MessageResult("No data.")
428+
429+
result_data = row[0]
430+
result_json = (
431+
json.loads(result_data) if isinstance(result_data, str) else result_data
432+
)
433+
434+
summary = _analyze_result_summary(result_json)
435+
436+
if summary.has_errors:
437+
error_message = _format_error_message(summary)
438+
raise CliError(error_message)
439+
440+
return MessageResult(
441+
f"✓ Analysis complete: {summary.total_files} file(s) analyzed, "
442+
f"{summary.total_definitions} definition(s) found. No errors detected."
443+
)
444+
445+
446+
@dataclass
447+
class AnalysisSummary:
448+
total_files: int = 0
449+
total_definitions: int = 0
450+
files_with_errors: int = 0
451+
total_errors: int = 0
452+
errors_by_file: Dict[str, List[str]] = field(default_factory=dict)
453+
has_errors: bool = False
454+
455+
456+
def _analyze_result_summary(result_json) -> AnalysisSummary:
457+
summary = AnalysisSummary()
458+
459+
if not isinstance(result_json, dict):
460+
return summary
461+
462+
files = result_json.get("files", [])
463+
summary.total_files = len(files)
464+
465+
for file_info in files:
466+
source_path = file_info.get("sourcePath", "unknown")
467+
file_errors = []
468+
469+
# Check file-level errors
470+
for error in file_info.get("errors", []):
471+
error_msg = error.get("message", "Unknown error")
472+
file_errors.append(error_msg)
473+
summary.total_errors += 1
474+
475+
# Check definition-level errors
476+
definitions = file_info.get("definitions", [])
477+
summary.total_definitions += len(definitions)
478+
479+
for definition in definitions:
480+
for error in definition.get("errors", []):
481+
error_msg = error.get("message", "Unknown error")
482+
file_errors.append(error_msg)
483+
summary.total_errors += 1
484+
485+
if file_errors:
486+
summary.errors_by_file[source_path] = file_errors
487+
summary.files_with_errors += 1
488+
summary.has_errors = True
489+
490+
return summary
491+
492+
493+
def _format_error_message(summary: AnalysisSummary) -> str:
494+
lines = [
495+
f"Analysis found {summary.total_errors} error(s) in {summary.files_with_errors} file(s):",
496+
"",
497+
]
498+
499+
for file_path, errors in summary.errors_by_file.items():
500+
lines.append(f" {file_path}:")
501+
for error in errors:
502+
lines.append(f" • {error}")
503+
lines.append("")
504+
505+
return "\n".join(lines).rstrip()
506+
507+
387508
def _get_effective_stage(identifier: FQN, from_location: Optional[str]):
388509
manager = DCMProjectManager()
389510
if not from_location:

src/snowflake/cli/_plugins/dcm/manager.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
from contextlib import contextmanager, nullcontext
15+
from enum import Enum
1516
from pathlib import Path
1617
from typing import Generator, List
1718

@@ -38,6 +39,10 @@
3839
DCM_PROJECT_TYPE = "dcm_project"
3940

4041

42+
class AnalysisType(str, Enum):
43+
DEPENDENCIES = "dependencies"
44+
45+
4146
class DCMProjectManager(SqlExecutionMixin):
4247
@contextmanager
4348
def _collect_output(
@@ -164,6 +169,31 @@ def preview(
164169
query += f" LIMIT {limit}"
165170
return self.execute_query(query=query)
166171

172+
def analyze(
173+
self,
174+
project_identifier: FQN,
175+
from_stage: str,
176+
configuration: str | None = None,
177+
variables: List[str] | None = None,
178+
analysis_type: AnalysisType | None = None,
179+
output_path: str | None = None,
180+
):
181+
with self._collect_output(
182+
project_identifier, output_path
183+
) if output_path else nullcontext() as output_stage:
184+
query = f"EXECUTE DCM PROJECT {project_identifier.sql_identifier} ANALYZE"
185+
if analysis_type:
186+
query += f" {analysis_type.value.upper()}"
187+
query += self._get_configuration_and_variables_query(
188+
configuration, variables
189+
)
190+
query += self._get_from_stage_query(from_stage)
191+
if output_stage is not None:
192+
query += f" OUTPUT_PATH {output_stage}"
193+
result = self.execute_query(query=query)
194+
195+
return result
196+
167197
@staticmethod
168198
def _get_from_stage_query(from_stage: str) -> str:
169199
stage_path = StagePath.from_stage_str(from_stage)

tests/__snapshots__/test_help_messages.ambr

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7191,6 +7191,162 @@
71917191
+------------------------------------------------------------------------------+
71927192

71937193

7194+
'''
7195+
# ---
7196+
# name: test_help_messages[dcm.analyze]
7197+
'''
7198+
7199+
Usage: root dcm analyze [OPTIONS] IDENTIFIER
7200+
7201+
Analyzes a DCM Project.
7202+
7203+
+- Arguments ------------------------------------------------------------------+
7204+
| * identifier TEXT Identifier of the DCM Project; for example: |
7205+
| MY_PROJECT |
7206+
| [required] |
7207+
+------------------------------------------------------------------------------+
7208+
+- Options --------------------------------------------------------------------+
7209+
| --from TEXT Source location: stage path |
7210+
| (starting with '@') or local |
7211+
| directory path. Omit to use current |
7212+
| directory. |
7213+
| --variable -D TEXT Variables for the execution |
7214+
| context; for example: -D |
7215+
| "<key>=<value>". |
7216+
| --configuration TEXT Configuration of the DCM Project to |
7217+
| use. If not specified default |
7218+
| configuration is used. |
7219+
| --type [dependencies] Type of analysis to perform. |
7220+
| --output-path TEXT Path where the analysis result will |
7221+
| be stored. Can be a stage path |
7222+
| (starting with '@') or a local |
7223+
| directory path. |
7224+
| --help -h Show this message and exit. |
7225+
+------------------------------------------------------------------------------+
7226+
+- Connection configuration ---------------------------------------------------+
7227+
| --connection,--environment -c TEXT Name of the connection, as |
7228+
| defined in your config.toml |
7229+
| file. Default: default. |
7230+
| --host TEXT Host address for the |
7231+
| connection. Overrides the |
7232+
| value specified for the |
7233+
| connection. |
7234+
| --port INTEGER Port for the connection. |
7235+
| Overrides the value specified |
7236+
| for the connection. |
7237+
| --account,--accountname TEXT Name assigned to your |
7238+
| Snowflake account. Overrides |
7239+
| the value specified for the |
7240+
| connection. |
7241+
| --user,--username TEXT Username to connect to |
7242+
| Snowflake. Overrides the |
7243+
| value specified for the |
7244+
| connection. |
7245+
| --password TEXT Snowflake password. Overrides |
7246+
| the value specified for the |
7247+
| connection. |
7248+
| --authenticator TEXT Snowflake authenticator. |
7249+
| Overrides the value specified |
7250+
| for the connection. |
7251+
| --workload-identity-provider TEXT Workload identity provider |
7252+
| (AWS, AZURE, GCP, OIDC). |
7253+
| Overrides the value specified |
7254+
| for the connection |
7255+
| --private-key-file,--privat… TEXT Snowflake private key file |
7256+
| path. Overrides the value |
7257+
| specified for the connection. |
7258+
| --token TEXT OAuth token to use when |
7259+
| connecting to Snowflake. |
7260+
| --token-file-path TEXT Path to file with an OAuth |
7261+
| token to use when connecting |
7262+
| to Snowflake. |
7263+
| --database,--dbname TEXT Database to use. Overrides |
7264+
| the value specified for the |
7265+
| connection. |
7266+
| --schema,--schemaname TEXT Database schema to use. |
7267+
| Overrides the value specified |
7268+
| for the connection. |
7269+
| --role,--rolename TEXT Role to use. Overrides the |
7270+
| value specified for the |
7271+
| connection. |
7272+
| --warehouse TEXT Warehouse to use. Overrides |
7273+
| the value specified for the |
7274+
| connection. |
7275+
| --temporary-connection -x Uses a connection defined |
7276+
| with command line parameters, |
7277+
| instead of one defined in |
7278+
| config |
7279+
| --mfa-passcode TEXT Token to use for multi-factor |
7280+
| authentication (MFA) |
7281+
| --enable-diag Whether to generate a |
7282+
| connection diagnostic report. |
7283+
| --diag-log-path TEXT Path for the generated |
7284+
| report. Defaults to system |
7285+
| temporary directory. |
7286+
| --diag-allowlist-path TEXT Path to a JSON file that |
7287+
| contains allowlist |
7288+
| parameters. |
7289+
| --oauth-client-id TEXT Value of client id provided |
7290+
| by the Identity Provider for |
7291+
| Snowflake integration. |
7292+
| --oauth-client-secret TEXT Value of the client secret |
7293+
| provided by the Identity |
7294+
| Provider for Snowflake |
7295+
| integration. |
7296+
| --oauth-authorization-url TEXT Identity Provider endpoint |
7297+
| supplying the authorization |
7298+
| code to the driver. |
7299+
| --oauth-token-request-url TEXT Identity Provider endpoint |
7300+
| supplying the access tokens |
7301+
| to the driver. |
7302+
| --oauth-redirect-uri TEXT URI to use for authorization |
7303+
| code redirection. |
7304+
| --oauth-scope TEXT Scope requested in the |
7305+
| Identity Provider |
7306+
| authorization request. |
7307+
| --oauth-disable-pkce Disables Proof Key for Code |
7308+
| Exchange (PKCE). Default: |
7309+
| False. |
7310+
| --oauth-enable-refresh-toke… Enables a silent |
7311+
| re-authentication when the |
7312+
| actual access token becomes |
7313+
| outdated. Default: False. |
7314+
| --oauth-enable-single-use-r… Whether to opt-in to |
7315+
| single-use refresh token |
7316+
| semantics. Default: False. |
7317+
| --client-store-temporary-cr… Store the temporary |
7318+
| credential. |
7319+
+------------------------------------------------------------------------------+
7320+
+- Global configuration -------------------------------------------------------+
7321+
| --format [TABLE|JSON|JSON_EXT| Specifies the output |
7322+
| CSV] format. |
7323+
| [default: TABLE] |
7324+
| --verbose -v Displays log entries |
7325+
| for log levels info |
7326+
| and higher. |
7327+
| --debug Displays log entries |
7328+
| for log levels debug |
7329+
| and higher; debug logs |
7330+
| contain additional |
7331+
| information. |
7332+
| --silent Turns off intermediate |
7333+
| output to console. |
7334+
| --enhanced-exit-codes Differentiate exit |
7335+
| error codes based on |
7336+
| failure type. |
7337+
| [env var: |
7338+
| SNOWFLAKE_ENHANCED_EX… |
7339+
| --decimal-precision INTEGER Number of decimal |
7340+
| places to display for |
7341+
| decimal values. Uses |
7342+
| Python's default |
7343+
| precision if not |
7344+
| specified. |
7345+
| [env var: |
7346+
| SNOWFLAKE_DECIMAL_PRE… |
7347+
+------------------------------------------------------------------------------+
7348+
7349+
71947350
'''
71957351
# ---
71967352
# name: test_help_messages[dcm.create]
@@ -8812,6 +8968,7 @@
88128968
| --help -h Show this message and exit. |
88138969
+------------------------------------------------------------------------------+
88148970
+- Commands -------------------------------------------------------------------+
8971+
| analyze Analyzes a DCM Project. |
88158972
| create Creates a DCM Project in Snowflake. |
88168973
| deploy Applies changes defined in DCM Project to Snowflake. |
88178974
| describe Provides description of DCM Project. |
@@ -22401,6 +22558,7 @@
2240122558
| --help -h Show this message and exit. |
2240222559
+------------------------------------------------------------------------------+
2240322560
+- Commands -------------------------------------------------------------------+
22561+
| analyze Analyzes a DCM Project. |
2240422562
| create Creates a DCM Project in Snowflake. |
2240522563
| deploy Applies changes defined in DCM Project to Snowflake. |
2240622564
| describe Provides description of DCM Project. |

0 commit comments

Comments
 (0)