Skip to content

Commit 216c405

Browse files
feat: [SNOW-2412813] wip: analyze
1 parent f00a3fb commit 216c405

File tree

5 files changed

+741
-1
lines changed

5 files changed

+741
-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>"`.'
@@ -376,6 +379,124 @@ def preview(
376379
return QueryResult(result)
377380

378381

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

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

0 commit comments

Comments
 (0)