|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 | import json |
15 | | -from typing import List, Optional |
| 15 | +from dataclasses import dataclass, field |
| 16 | +from typing import Dict, List, Optional |
16 | 17 |
|
17 | 18 | import typer |
| 19 | +from snowflake.cli._plugins.dcm.manager import AnalysisType |
18 | 20 | from snowflake.cli._plugins.dcm.manager import DCMProjectManager |
19 | 21 | from snowflake.cli._plugins.dcm.utils import ( |
20 | 22 | format_refresh_results, |
|
53 | 55 | is_hidden=FeatureFlag.ENABLE_SNOWFLAKE_PROJECTS.is_disabled, |
54 | 56 | ) |
55 | 57 |
|
| 58 | + |
56 | 59 | dcm_identifier = identifier_argument(sf_object="DCM Project", example="MY_PROJECT") |
57 | 60 | variables_flag = variables_option( |
58 | 61 | 'Variables for the execution context; for example: `-D "<key>=<value>"`.' |
@@ -354,6 +357,124 @@ def preview( |
354 | 357 | return QueryResult(result) |
355 | 358 |
|
356 | 359 |
|
| 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 | + |
357 | 478 | def _get_effective_stage(identifier: FQN, from_location: Optional[str]): |
358 | 479 | manager = DCMProjectManager() |
359 | 480 | if not from_location: |
|
0 commit comments