Source code for helix_ir.ddl.compile

"""DDL compilation: produce CREATE TABLE / ALTER TABLE SQL from a NormalizationPlan."""

from __future__ import annotations

from typing import Any

from helix_ir.ddl.dialects import DDLOptions, DDLScript, get_dialect
from helix_ir.ddl.dialects.base import BaseDialect
from helix_ir.diff.classifier import SchemaDiff, SchemaChange
from helix_ir.exceptions import DDLCompilationError
from helix_ir.schema.schema import Schema


[docs] def compile_ddl( schema_or_plan: Any, dialect: str = "duckdb", options: DDLOptions | None = None, ) -> DDLScript: """Compile a Schema or NormalizationPlan to a DDL script. Args: schema_or_plan: A Schema instance or NormalizationPlan instance. dialect: Target SQL dialect name. options: DDL generation options. Returns: A DDLScript with all CREATE TABLE statements. """ if options is None: options = DDLOptions() try: engine = get_dialect(dialect) except ValueError as e: raise DDLCompilationError(str(e)) from e script = DDLScript(dialect=dialect) # Handle NormalizationPlan if hasattr(schema_or_plan, "tables"): plan = schema_or_plan for table_schema in plan.tables: stmt = engine.compile_create_table(table_schema, options) script.add(stmt) # Add foreign key constraints for fk in plan.foreign_keys: fk_stmt = _compile_foreign_key(fk, engine, dialect) if fk_stmt: script.add(fk_stmt) elif isinstance(schema_or_plan, Schema): stmt = engine.compile_create_table(schema_or_plan, options) script.add(stmt) else: raise DDLCompilationError( f"Expected a Schema or NormalizationPlan, got {type(schema_or_plan).__name__}" ) return script
[docs] def compile_migration( schema_diff: SchemaDiff, dialect: str = "duckdb", table_name: str | None = None, options: DDLOptions | None = None, skip_breaking: bool = False, ) -> DDLScript: """Compile a SchemaDiff into ALTER TABLE migration statements. Args: schema_diff: The diff between old and new schema. dialect: Target SQL dialect. table_name: Table name to alter. Defaults to schema_diff.new_name. options: DDL options. skip_breaking: If True, skip breaking changes (dangerous!). Returns: A DDLScript with ALTER TABLE statements. """ if options is None: options = DDLOptions() try: engine = get_dialect(dialect) except ValueError as e: raise DDLCompilationError(str(e)) from e tname = table_name or schema_diff.new_name script = DDLScript(dialect=dialect) if schema_diff.has_breaking_changes and not skip_breaking: breaking = [c for c in schema_diff.changes if c.severity == "breaking"] msg = "\n".join(f" - {c.description}" for c in breaking) raise DDLCompilationError( f"Schema diff contains breaking changes:\n{msg}\n" "Pass skip_breaking=True to generate migration anyway." ) for change in schema_diff.changes: if change.severity == "breaking" and skip_breaking: script.add(f"-- BREAKING CHANGE SKIPPED: {change.description}") continue stmt = _compile_change(change, tname, engine) if stmt: script.add(stmt) return script
def _compile_change( change: SchemaChange, table_name: str, engine: BaseDialect, ) -> str | None: """Compile a single SchemaChange to a SQL statement.""" if change.kind == "added" and change.new_type is not None: return engine.compile_add_column( table_name, str(change.path), change.new_type, ) elif change.kind == "removed": return engine.compile_drop_column(table_name, str(change.path)) elif change.kind == "type_changed" and change.new_type is not None: return engine.compile_alter_column_type( table_name, str(change.path), change.new_type, ) elif change.kind in ("nullable_changed", "semantic_changed", "pii_changed"): return f"-- {change.severity.upper()}: {change.description}" return None def _compile_foreign_key(fk: Any, engine: BaseDialect, dialect: str) -> str | None: """Compile a ForeignKey to a SQL ALTER TABLE ADD CONSTRAINT statement.""" from_table = engine.quote_identifier(fk.from_table) from_col = engine.quote_identifier(fk.from_column) to_table = engine.quote_identifier(fk.to_table) to_col = engine.quote_identifier(fk.to_column) constraint_name = engine.quote_identifier( f"fk_{fk.from_table}_{fk.from_column}" ) return ( f"ALTER TABLE {from_table} " f"ADD CONSTRAINT {constraint_name} " f"FOREIGN KEY ({from_col}) REFERENCES {to_table}({to_col});" )