This is an automated email from the ASF dual-hosted git repository.

pankajastro pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 3c19778c2ec Allow DataSourceConfig to represent a plain database table 
(#73273)
3c19778c2ec is described below

commit 3c19778c2ec4ad8e6ec7155ab56315c423090a77
Author: Pankaj Singh <[email protected]>
AuthorDate: Sat Sep 19 09:02:24 2026 +0530

    Allow DataSourceConfig to represent a plain database table (#73273)
    
    LLMSchemaCompareOperator accepts a DataSourceConfig with only conn_id
    and table_name, introspected via DbApiHook instead of DataFusion when
    the connection resolves to one, but construction failed for that
    shape. Also hoist the table_name check so it can no longer be skipped
    by an explicit storage_type, and require a uri whenever format is set,
    since format alone can't be registered without one.
    
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 .../ai/docs/operators/llm_schema_compare.rst       | 15 ++++++++----
 .../ai/example_dags/example_llm_schema_compare.py  |  7 ++++--
 .../sql/src/airflow/providers/common/sql/config.py | 16 ++++++++++---
 .../common/sql/datafusion/test_format_handlers.py  |  2 +-
 .../sql/tests/unit/common/sql/test_config.py       | 28 ++++++++++++++++++++++
 5 files changed, 57 insertions(+), 11 deletions(-)

diff --git a/providers/common/ai/docs/operators/llm_schema_compare.rst 
b/providers/common/ai/docs/operators/llm_schema_compare.rst
index e7ae94e2c9b..2efed391b01 100644
--- a/providers/common/ai/docs/operators/llm_schema_compare.rst
+++ b/providers/common/ai/docs/operators/llm_schema_compare.rst
@@ -57,13 +57,17 @@ in the schema context sent to the LLM.
     :start-after: [START howto_operator_llm_schema_compare_full]
     :end-before: [END howto_operator_llm_schema_compare_full]
 
-With Object Storage
--------------------
+With Object Storage or a Database Table
+---------------------------------------
 
 Use ``data_sources`` with
 :class:`~airflow.providers.common.sql.config.DataSourceConfig` to include
 object-storage sources (S3 Parquet, CSV, Iceberg, etc.) in the comparison.
-These can be freely combined with ``db_conn_ids``:
+These can be freely combined with ``db_conn_ids``. Whether an entry is
+introspected via ``DbApiHook`` or DataFusion depends on what its ``conn_id``
+resolves to, not on its ``uri``/``format`` fields — a ``DataSourceConfig``
+with neither ``uri`` nor ``format`` set only works when ``conn_id`` resolves
+to a ``DbApiHook``:
 
 .. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
     :language: python
@@ -188,8 +192,9 @@ Parameters
 - ``db_conn_ids``: List of database connection IDs to compare. Each must 
resolve
   to a ``DbApiHook``.
 - ``table_names``: Tables to introspect from each ``db_conn_id``.
-- ``data_sources``: List of ``DataSourceConfig`` objects for object-storage or
-  catalog-managed sources.
+- ``data_sources``: List of ``DataSourceConfig`` objects for object-storage
+  or catalog-managed sources. An entry with neither ``uri`` nor ``format``
+  set works only if its ``conn_id`` resolves to a ``DbApiHook``.
 - ``context_strategy``: To fetch primary keys, foreign keys, and 
indexes.``full`` or ``basic``,
   strongly recommended for cross-system comparisons. default is ``full``
 - ``require_approval``: If ``True``, the task pauses after the comparison and
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
index bf8020e684e..06caf979bbd 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm_schema_compare.py
@@ -72,14 +72,17 @@ def example_llm_schema_compare_with_object_storage():
         uri="s3://data-lake/customers/",
         format="parquet",
     )
+    # snowflake_default resolves to a DbApiHook, so no uri/format is needed;
+    # a plain database table can be listed alongside object-storage sources.
+    snowflake_source = DataSourceConfig(conn_id="snowflake_default", 
table_name="customers")
 
     LLMSchemaCompareOperator(
         task_id="compare_s3_vs_db",
-        prompt="Compare S3 Parquet schema against the Postgres table and flag 
breaking changes",
+        prompt="Compare S3 Parquet schema against the Postgres and Snowflake 
tables and flag breaking changes",
         llm_conn_id="pydanticai_default",
         db_conn_ids=["postgres_default"],
         table_names=["customers"],
-        data_sources=[s3_source],
+        data_sources=[s3_source, snowflake_source],
     )
 
 
diff --git a/providers/common/sql/src/airflow/providers/common/sql/config.py 
b/providers/common/sql/src/airflow/providers/common/sql/config.py
index 75cc15efedc..975bc58c9d4 100644
--- a/providers/common/sql/src/airflow/providers/common/sql/config.py
+++ b/providers/common/sql/src/airflow/providers/common/sql/config.py
@@ -62,6 +62,9 @@ class DataSourceConfig:
     require ``uri`` or ``storage_type``; they use ``conn_id`` and 
format-specific
     keys in ``options`` (e.g. ``catalog_table_name`` for Iceberg).
 
+    **Plain database tables** (neither ``uri`` nor ``format`` set) are not
+    object-store backed either; ``storage_type`` stays ``None`` and is not 
inferred.
+
     :param conn_id: The connection ID to use for accessing the data source.
     :param uri: The URI of the data source (e.g., file path, S3 bucket, etc.).
         Not required for catalog-managed formats.
@@ -88,17 +91,24 @@ class DataSourceConfig:
         return bool(self.format and self.format.lower() in TABLE_PROVIDERS)
 
     def __post_init__(self):
+        if not self.table_name or not self.table_name.strip():
+            raise ValueError("Table name must be provided for storage type")
+
         if self.is_table_provider:
             if self.db_name is None:
                 raise ValueError(f"Database name must be provided for table 
providers {TABLE_PROVIDERS}")
             return
 
+        if not self.format and not self.uri:
+            # Plain database table: no object store involved, so storage_type 
stays unset.
+            return
+
+        if not self.uri:
+            raise ValueError("URI must be provided when format is set")
+
         if self.storage_type is None:
             self.storage_type = self._extract_storage_type
 
-        if self.storage_type is not None and (not self.table_name or not 
self.table_name.strip()):
-            raise ValueError("Table name must be provided for storage type")
-
     @property
     def _extract_storage_type(self) -> StorageType | None:
         """Extract storage type."""
diff --git 
a/providers/common/sql/tests/unit/common/sql/datafusion/test_format_handlers.py 
b/providers/common/sql/tests/unit/common/sql/datafusion/test_format_handlers.py
index c6221c62bf4..39ec10532d8 100644
--- 
a/providers/common/sql/tests/unit/common/sql/datafusion/test_format_handlers.py
+++ 
b/providers/common/sql/tests/unit/common/sql/datafusion/test_format_handlers.py
@@ -141,7 +141,7 @@ class TestFormatHandlers:
             ),
             (
                 {"table_name": "t", "format": "parquet", "conn_id": "c"},
-                "Unsupported storage type for URI:",
+                "URI must be provided when format is set",
             ),
         ],
     )
diff --git a/providers/common/sql/tests/unit/common/sql/test_config.py 
b/providers/common/sql/tests/unit/common/sql/test_config.py
index 0e90c873443..5478b5a2e44 100644
--- a/providers/common/sql/tests/unit/common/sql/test_config.py
+++ b/providers/common/sql/tests/unit/common/sql/test_config.py
@@ -44,10 +44,38 @@ class TestDataSourceConfig:
         with pytest.raises(ValueError, match="Unsupported storage type for 
URI"):
             DataSourceConfig(conn_id="test", uri="unknown://bucket/path", 
table_name="a_table")
 
+    def test_plain_db_table_without_uri_does_not_infer_storage_type(self):
+        config = DataSourceConfig(conn_id="postgres_default", 
table_name="my_table")
+        assert config.storage_type is None
+        assert config.uri == ""
+
+    def test_plain_db_table_with_blank_table_name_raises_error(self):
+        with pytest.raises(ValueError, match="Table name must be provided for 
storage type"):
+            DataSourceConfig(conn_id="postgres_default", table_name="   ")
+
     def test_missing_table_name_raises_error(self):
         with pytest.raises(ValueError, match="Table name must be provided for 
storage type"):
             DataSourceConfig(conn_id="test", uri="s3://bucket/path", 
table_name="")
 
+    def test_missing_table_name_raises_error_with_explicit_storage_type(self):
+        with pytest.raises(ValueError, match="Table name must be provided for 
storage type"):
+            DataSourceConfig(conn_id="pg", table_name="", 
storage_type=StorageType.S3)
+
+    def test_iceberg_with_blank_table_name_raises_error(self):
+        with pytest.raises(ValueError, match="Table name must be provided for 
storage type"):
+            DataSourceConfig(conn_id="iceberg_conn", table_name="", 
format="iceberg", db_name="default")
+
+    def test_explicit_storage_type_without_uri_or_format_is_accepted(self):
+        config = DataSourceConfig(conn_id="pg", table_name="my_table", 
storage_type=StorageType.S3)
+        assert config.storage_type == StorageType.S3
+        assert config.uri == ""
+
+    def test_format_without_uri_raises_error(self):
+        with pytest.raises(ValueError, match="URI must be provided when format 
is set"):
+            DataSourceConfig(
+                conn_id="test", table_name="my_table", format="parquet", 
storage_type=StorageType.LOCAL
+            )
+
     def test_parquet_with_partition_cols(self):
         config = DataSourceConfig(
             conn_id="test_conn",

Reply via email to