|
170 | 170 | from altimate_engine.dbt.profiles import discover_dbt_connections |
171 | 171 | from altimate_engine.local.schema_sync import sync_schema |
172 | 172 | from altimate_engine.local.test_local import test_sql_local |
| 173 | +from altimate_engine.sql.jinja_preprocessor import ( |
| 174 | + contains_jinja, |
| 175 | + preprocess_jinja, |
| 176 | +) |
173 | 177 | from altimate_engine.models import ( |
| 178 | + SqlPreprocessJinjaParams, |
| 179 | + SqlPreprocessJinjaResult, |
174 | 180 | AltimateCoreFixParams, |
175 | 181 | AltimateCorePolicyParams, |
176 | 182 | AltimateCoreSemanticsParams, |
@@ -298,13 +304,48 @@ def dispatch(request: JsonRpcRequest) -> JsonRpcResponse: |
298 | 304 | params = request.params or {} |
299 | 305 |
|
300 | 306 | try: |
301 | | - if method == "sql.execute": |
| 307 | + if method == "sql.preprocess_jinja": |
| 308 | + pp_params = SqlPreprocessJinjaParams(**params) |
| 309 | + pp_result = preprocess_jinja(pp_params.sql) |
| 310 | + result = SqlPreprocessJinjaResult( |
| 311 | + success=True, |
| 312 | + preprocessed_sql=pp_result.preprocessed_sql, |
| 313 | + original_sql=pp_result.original_sql, |
| 314 | + was_preprocessed=pp_result.was_preprocessed, |
| 315 | + refs_found=pp_result.refs_found, |
| 316 | + sources_found=pp_result.sources_found, |
| 317 | + variables_found=pp_result.variables_found, |
| 318 | + macros_removed=pp_result.macros_removed, |
| 319 | + warnings=pp_result.warnings, |
| 320 | + ) |
| 321 | + elif method == "sql.execute": |
302 | 322 | result = execute_sql(SqlExecuteParams(**params)) |
303 | 323 | elif method == "schema.inspect": |
304 | 324 | result = inspect_schema(SchemaInspectParams(**params)) |
305 | 325 | elif method == "sql.analyze": |
306 | 326 | params_obj = SqlAnalyzeParams(**params) |
307 | | - statements = _split_sql_statements(params_obj.sql) |
| 327 | + |
| 328 | + # Auto-preprocess Jinja if present |
| 329 | + jinja_note = "" |
| 330 | + sql_to_analyze = params_obj.sql |
| 331 | + if contains_jinja(sql_to_analyze): |
| 332 | + pp = preprocess_jinja(sql_to_analyze) |
| 333 | + if pp.was_preprocessed: |
| 334 | + sql_to_analyze = pp.preprocessed_sql |
| 335 | + parts = [] |
| 336 | + if pp.refs_found: |
| 337 | + parts.append(f"refs: {', '.join(pp.refs_found)}") |
| 338 | + if pp.sources_found: |
| 339 | + parts.append(f"sources: {', '.join(pp.sources_found)}") |
| 340 | + if pp.variables_found: |
| 341 | + parts.append(f"vars: {', '.join(pp.variables_found)}") |
| 342 | + detail = f" ({'; '.join(parts)})" if parts else "" |
| 343 | + jinja_note = ( |
| 344 | + f"Jinja templates were preprocessed before analysis{detail}. " |
| 345 | + "Results are based on the rendered SQL and may be approximate." |
| 346 | + ) |
| 347 | + |
| 348 | + statements = _split_sql_statements(sql_to_analyze) |
308 | 349 | issues = [] |
309 | 350 | any_error = None |
310 | 351 |
|
@@ -360,37 +401,65 @@ def dispatch(request: JsonRpcRequest) -> JsonRpcResponse: |
360 | 401 | ) |
361 | 402 | ) |
362 | 403 |
|
| 404 | + confidence_factors = [] |
| 405 | + if any_error is not None: |
| 406 | + confidence_factors.append(f"Parse failed on one statement: {any_error}") |
| 407 | + if jinja_note: |
| 408 | + confidence_factors.append(jinja_note) |
| 409 | + |
363 | 410 | result = SqlAnalyzeResult( |
364 | 411 | success=any_error is None, |
365 | 412 | issues=issues, |
366 | 413 | issue_count=len(issues), |
367 | 414 | confidence=_compute_overall_confidence(issues), |
368 | | - confidence_factors=[] |
369 | | - if any_error is None |
370 | | - else [f"Parse failed on one statement: {any_error}"], |
| 415 | + confidence_factors=confidence_factors, |
371 | 416 | error=any_error, |
372 | 417 | ) |
373 | 418 | elif method == "sql.translate": |
374 | 419 | params_obj = SqlTranslateParams(**params) |
| 420 | + |
| 421 | + # Auto-preprocess Jinja if present |
| 422 | + sql_to_translate = params_obj.sql |
| 423 | + jinja_warnings: list[str] = [] |
| 424 | + if contains_jinja(sql_to_translate): |
| 425 | + pp = preprocess_jinja(sql_to_translate) |
| 426 | + if pp.was_preprocessed: |
| 427 | + sql_to_translate = pp.preprocessed_sql |
| 428 | + jinja_warnings.append( |
| 429 | + "Jinja templates were preprocessed before translation. " |
| 430 | + "Review the translated SQL and re-apply Jinja syntax as needed." |
| 431 | + ) |
| 432 | + |
375 | 433 | raw = guard_transpile( |
376 | | - params_obj.sql, params_obj.source_dialect, params_obj.target_dialect |
| 434 | + sql_to_translate, params_obj.source_dialect, params_obj.target_dialect |
377 | 435 | ) |
| 436 | + all_warnings = jinja_warnings + raw.get("warnings", []) |
378 | 437 | result = SqlTranslateResult( |
379 | 438 | success=raw.get("success", True), |
380 | 439 | translated_sql=raw.get("sql", raw.get("translated_sql")), |
381 | 440 | source_dialect=params_obj.source_dialect, |
382 | 441 | target_dialect=params_obj.target_dialect, |
383 | | - warnings=raw.get("warnings", []), |
| 442 | + warnings=all_warnings, |
384 | 443 | error=raw.get("error"), |
385 | 444 | ) |
386 | 445 | elif method == "sql.optimize": |
387 | 446 | params_obj = SqlOptimizeParams(**params) |
| 447 | + |
| 448 | + # Auto-preprocess Jinja if present |
| 449 | + sql_to_optimize = params_obj.sql |
| 450 | + jinja_preprocessed = False |
| 451 | + if contains_jinja(sql_to_optimize): |
| 452 | + pp = preprocess_jinja(sql_to_optimize) |
| 453 | + if pp.was_preprocessed: |
| 454 | + sql_to_optimize = pp.preprocessed_sql |
| 455 | + jinja_preprocessed = True |
| 456 | + |
388 | 457 | # Rewrite for optimization |
389 | 458 | rw = guard_rewrite_sql( |
390 | | - params_obj.sql, schema_context=params_obj.schema_context |
| 459 | + sql_to_optimize, schema_context=params_obj.schema_context |
391 | 460 | ) |
392 | 461 | # Lint for remaining issues |
393 | | - lint = guard_lint(params_obj.sql, schema_context=params_obj.schema_context) |
| 462 | + lint = guard_lint(sql_to_optimize, schema_context=params_obj.schema_context) |
394 | 463 |
|
395 | 464 | suggestions = [] |
396 | 465 | for r in rw.get("rewrites", []): |
@@ -424,12 +493,17 @@ def dispatch(request: JsonRpcRequest) -> JsonRpcResponse: |
424 | 493 | } |
425 | 494 | ) |
426 | 495 |
|
| 496 | + opt_confidence = "high" |
| 497 | + if jinja_preprocessed: |
| 498 | + opt_confidence = "medium" |
| 499 | + |
427 | 500 | result = SqlOptimizeResult( |
428 | 501 | success=True, |
429 | 502 | original_sql=params_obj.sql, |
430 | | - optimized_sql=rw.get("rewritten_sql", params_obj.sql), |
| 503 | + optimized_sql=rw.get("rewritten_sql", sql_to_optimize), |
431 | 504 | suggestions=suggestions, |
432 | 505 | anti_patterns=anti_patterns, |
| 506 | + confidence=opt_confidence, |
433 | 507 | error=rw.get("error"), |
434 | 508 | ) |
435 | 509 | elif method == "lineage.check": |
@@ -492,12 +566,28 @@ def dispatch(request: JsonRpcRequest) -> JsonRpcResponse: |
492 | 566 |
|
493 | 567 | elif method == "sql.format": |
494 | 568 | fmt_params = SqlFormatParams(**params) |
495 | | - raw = guard_format_sql(fmt_params.sql, fmt_params.dialect) |
| 569 | + |
| 570 | + # Auto-preprocess Jinja if present |
| 571 | + sql_to_format = fmt_params.sql |
| 572 | + jinja_fmt_note = None |
| 573 | + if contains_jinja(sql_to_format): |
| 574 | + pp = preprocess_jinja(sql_to_format) |
| 575 | + if pp.was_preprocessed: |
| 576 | + sql_to_format = pp.preprocessed_sql |
| 577 | + jinja_fmt_note = ( |
| 578 | + "Note: Jinja templates were removed before formatting. " |
| 579 | + "The formatted output contains plain SQL only." |
| 580 | + ) |
| 581 | + |
| 582 | + raw = guard_format_sql(sql_to_format, fmt_params.dialect) |
| 583 | + fmt_error = raw.get("error") |
| 584 | + if jinja_fmt_note and not fmt_error: |
| 585 | + fmt_error = jinja_fmt_note |
496 | 586 | result = SqlFormatResult( |
497 | 587 | success=raw.get("success", True), |
498 | 588 | formatted_sql=raw.get("formatted_sql", raw.get("sql")), |
499 | 589 | statement_count=raw.get("statement_count", 1), |
500 | | - error=raw.get("error"), |
| 590 | + error=fmt_error, |
501 | 591 | ) |
502 | 592 | elif method == "sql.explain": |
503 | 593 | result = explain_sql(SqlExplainParams(**params)) |
|
0 commit comments