Skip to content

Commit c04bc02

Browse files
feat: [SNOW-2412813] wip: analyze
1 parent 072f458 commit c04bc02

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
@@ -12,9 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import json
15-
from typing import List, Optional
15+
from dataclasses import dataclass, field
16+
from typing import Dict, List, Optional
1617

1718
import typer
19+
from snowflake.cli._plugins.dcm.manager import AnalysisType
1820
from snowflake.cli._plugins.dcm.manager import DCMProjectManager
1921
from snowflake.cli._plugins.dcm.utils import (
2022
format_refresh_results,
@@ -53,6 +55,7 @@
5355
is_hidden=FeatureFlag.ENABLE_SNOWFLAKE_PROJECTS.is_disabled,
5456
)
5557

58+
5659
dcm_identifier = identifier_argument(sf_object="DCM Project", example="MY_PROJECT")
5760
variables_flag = variables_option(
5861
'Variables for the execution context; for example: `-D "<key>=<value>"`.'
@@ -354,6 +357,124 @@ def preview(
354357
return QueryResult(result)
355358

356359

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