diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe1aebb..572bbaa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix SQL export generating multiple PRIMARY KEY constraints for composite keys (#1026) - Preserve parametrized physicalTypes for SQL export (#1086) +- Emit `CREATE VIEW` DDL when model `type` is `view` in SQL export (#1031) - Fix incorrect SQL type mappings: SQL Server `double`/`jsonb`, MySQL bare `varchar`, missing Trino types (#1110) ## [0.11.7] - 2026-03-24 diff --git a/datacontract/export/sql_exporter.py b/datacontract/export/sql_exporter.py index c56247fb..81bafa71 100644 --- a/datacontract/export/sql_exporter.py +++ b/datacontract/export/sql_exporter.py @@ -104,7 +104,13 @@ def to_sql_ddl(data_contract: OpenDataContractStandard, server_type: str = "snow def _to_sql_table(model_name: str, model: SchemaObject, server_type: str = "snowflake") -> str: - if server_type == "databricks": + is_view = model.physicalType is not None and model.physicalType.lower() == "view" + if is_view: + if server_type == "databricks": + result = f"CREATE OR REPLACE VIEW {model_name} (\n" + else: + result = f"CREATE VIEW {model_name} (\n" + elif server_type == "databricks": # Databricks recommends to use the CREATE OR REPLACE statement for unity managed tables # https://docs.databricks.com/en/sql/language-manual/sql-ref-syntax-ddl-create-table-using.html result = f"CREATE OR REPLACE TABLE {model_name} (\n" diff --git a/datacontract/output/ci_output.py b/datacontract/output/ci_output.py index 4df73065..5716219a 100644 --- a/datacontract/output/ci_output.py +++ b/datacontract/output/ci_output.py @@ -93,7 +93,9 @@ def _write_github_step_summary(results: List[Tuple[str, Run]], summary_path: str # Per-contract detail sections for data_contract_file, run in results: - result_display = RESULT_EMOJI.get(run.result, run.result.value if hasattr(run.result, "value") else str(run.result)) + result_display = RESULT_EMOJI.get( + run.result, run.result.value if hasattr(run.result, "value") else str(run.result) + ) n_total = len(run.checks) if run.checks else 0 n_passed = sum(1 for c in run.checks if c.result == "passed") if run.checks else 0 diff --git a/tests/fixtures/sql-view-export/datacontract.yaml b/tests/fixtures/sql-view-export/datacontract.yaml new file mode 100644 index 00000000..c1e41f61 --- /dev/null +++ b/tests/fixtures/sql-view-export/datacontract.yaml @@ -0,0 +1,25 @@ +dataContractSpecification: 1.2.1 +id: sql-view-export +info: + title: SQL View Export + version: 0.0.1 + owner: my-domain-team +servers: + production: + type: postgres + host: localhost + port: 4567 + database: test + schema: public +models: + my_view: + type: view + description: A sample view definition + fields: + col_a: + type: varchar + description: First column + required: true + col_b: + type: integer + description: Second column diff --git a/tests/test_export_sql.py b/tests/test_export_sql.py index 9d68012a..34476000 100644 --- a/tests/test_export_sql.py +++ b/tests/test_export_sql.py @@ -144,3 +144,19 @@ def test_to_sql_ddl_databricks_unity_catalog_staging(): ) COMMENT "A single article that is part of an order."; """.strip() assert actual == expected + + +def test_to_sql_ddl_postgres_view(): + """Model with type=view should emit CREATE VIEW, not CREATE TABLE.""" + actual = DataContract(data_contract_file="fixtures/sql-view-export/datacontract.yaml").export("sql") + expected = """ +-- Data Contract: sql-view-export +-- SQL Dialect: postgres +CREATE VIEW my_view ( + col_a text not null, + col_b integer +); +""".strip() + assert actual == expected + assert "CREATE VIEW" in actual + assert "CREATE TABLE" not in actual