From a5d2b69c9fdce1ad7f304208779f06f018c1b7fd Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:10:30 +0100 Subject: [PATCH 1/9] fix: deduplicate sql metric aliases to prevent view schema collisions (#10926) --- .../sqlalchemy_execution_engine.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/great_expectations/execution_engine/sqlalchemy_execution_engine.py b/great_expectations/execution_engine/sqlalchemy_execution_engine.py index f14c281dad26..8ed2a18bf227 100644 --- a/great_expectations/execution_engine/sqlalchemy_execution_engine.py +++ b/great_expectations/execution_engine/sqlalchemy_execution_engine.py @@ -1187,8 +1187,21 @@ def _organize_metrics_by_domain( # noqa: C901 # FIXME ) domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) else: + alias = metric_to_resolve.metric_name + + # Prevent "Duplicated field name in view schema" SQL errors by deduplicating aliases + existing_aliases = { + col.name for col in domain_batches[domain_id]["select"] if hasattr(col, "name") + } + + if alias in existing_aliases: + suffix = 1 + while f"{alias}_{suffix}" in existing_aliases: + suffix += 1 + alias = f"{alias}_{suffix}" + domain_batches[domain_id]["select"].append( - metric_fn.label(metric_to_resolve.metric_name) + metric_fn.label(alias) ) domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) From 6c8957f8104bc7b51010757969f23d581ae82061 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:18:12 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../execution_engine/sqlalchemy_execution_engine.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/great_expectations/execution_engine/sqlalchemy_execution_engine.py b/great_expectations/execution_engine/sqlalchemy_execution_engine.py index 8ed2a18bf227..1b90a97c95c2 100644 --- a/great_expectations/execution_engine/sqlalchemy_execution_engine.py +++ b/great_expectations/execution_engine/sqlalchemy_execution_engine.py @@ -1188,21 +1188,19 @@ def _organize_metrics_by_domain( # noqa: C901 # FIXME domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) else: alias = metric_to_resolve.metric_name - + # Prevent "Duplicated field name in view schema" SQL errors by deduplicating aliases existing_aliases = { col.name for col in domain_batches[domain_id]["select"] if hasattr(col, "name") } - + if alias in existing_aliases: suffix = 1 while f"{alias}_{suffix}" in existing_aliases: suffix += 1 alias = f"{alias}_{suffix}" - domain_batches[domain_id]["select"].append( - metric_fn.label(alias) - ) + domain_batches[domain_id]["select"].append(metric_fn.label(alias)) domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) for domain_id in list(domain_batches.keys()): From ae26d9f38e00d9aa941f92b4ed0d8ac566f6a554 Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:13:16 +0000 Subject: [PATCH 3/9] test: add regression test for postgres sql alias deduplication (#10926) --- ...est_postgres_metric_alias_deduplication.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py diff --git a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py new file mode 100644 index 000000000000..adb1f3f19bd3 --- /dev/null +++ b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py @@ -0,0 +1,76 @@ +import pandas as pd +import sqlalchemy.types as sqltypes +import great_expectations as gx +import great_expectations.expectations as gxe +from great_expectations.data_context import get_context +from tests.integration.test_utils.data_source_config.postgres import ( + PostgresBatchTestSetup, + PostgreSQLDatasourceTestConfig, +) + + +def test_postgres_multiple_null_expectations_alias_deduplication(): + """ + Regression test for Issue #10926 / PR #11905. + + Verifies that metric aliasing deduplication functions correctly during SQL compilation. + Executing multiple expectations that generate identical underlying metrics + (e.g., column_values.nonnull.unexpected_count) on different columns must not trigger + a 'Duplicated field name in view schema' OperationalError. + """ + + # 1. Define schema and intentional null values + df = pd.DataFrame( + { + "field1": [1, 2, None, 4], + "field2": ["a", "b", "c", None], + "field3": [10.0, 20.0, 30.0, 40.0], + } + ) + + context = get_context(mode="ephemeral") + + # 2. Initialize Postgres fixture + batch_setup = PostgresBatchTestSetup( + config=PostgreSQLDatasourceTestConfig( + column_types={ + "field1": sqltypes.INTEGER, + "field2": sqltypes.VARCHAR, + "field3": sqltypes.FLOAT, + } + ), + data=df, + extra_data={}, + context=context, + ) + + with batch_setup.batch_test_context() as batch: + # 3. Construct Expectation Suite + suite = context.suites.add( + gx.ExpectationSuite(name="alias_deduplication_suite") + ) + + # 4. Add multiple identical expectations on different columns + suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field1")) + suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field2")) + suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field3")) + + # 5. Execute Validation + # `batch.batch_definition` is a LegacyBatchDefinition and fails + # ValidationDefinition's pydantic isinstance check. The real fluent + # BatchDefinition (created via add_batch_definition_whole_table in + # PostgresBatchTestSetup.make_batch) lives on the data asset instead. + batch_definition = batch.data_asset.batch_definitions[0] + + validation_definition = context.validation_definitions.add( + gx.ValidationDefinition( + name="alias_deduplication_validation", + data=batch_definition, + suite=suite, + ) + ) + + result = validation_definition.run() + + # 6. Assert successful execution without alias collision + assert len(result.results) == 3 \ No newline at end of file From 5ceade4bac9cb6566fd6ee7bdfbc3c07c6f80d98 Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:19:38 +0000 Subject: [PATCH 4/9] fix: sort imports and add trailing newline for ruff --- .../test_postgres_metric_alias_deduplication.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py index adb1f3f19bd3..a429509ec8ca 100644 --- a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py +++ b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py @@ -1,5 +1,6 @@ import pandas as pd import sqlalchemy.types as sqltypes + import great_expectations as gx import great_expectations.expectations as gxe from great_expectations.data_context import get_context @@ -73,4 +74,5 @@ def test_postgres_multiple_null_expectations_alias_deduplication(): result = validation_definition.run() # 6. Assert successful execution without alias collision - assert len(result.results) == 3 \ No newline at end of file + assert len(result.results) == 3 + From 44fb406c6a2f045bf0b4678a5c32746ca3dc151b Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:59:06 +0000 Subject: [PATCH 5/9] style: apply ruff format --- .../test_postgres_metric_alias_deduplication.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py index a429509ec8ca..c28543aa8e2e 100644 --- a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py +++ b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py @@ -47,9 +47,7 @@ def test_postgres_multiple_null_expectations_alias_deduplication(): with batch_setup.batch_test_context() as batch: # 3. Construct Expectation Suite - suite = context.suites.add( - gx.ExpectationSuite(name="alias_deduplication_suite") - ) + suite = context.suites.add(gx.ExpectationSuite(name="alias_deduplication_suite")) # 4. Add multiple identical expectations on different columns suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field1")) @@ -75,4 +73,3 @@ def test_postgres_multiple_null_expectations_alias_deduplication(): # 6. Assert successful execution without alias collision assert len(result.results) == 3 - From 3d9fbbd7e8cad89675baf7e7f9f7e5d7e13e7137 Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:32:11 +0000 Subject: [PATCH 6/9] test: add postgresql marker for marker-coverage-check --- .../test_postgres_metric_alias_deduplication.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py index c28543aa8e2e..76898c2647b8 100644 --- a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py +++ b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py @@ -1,4 +1,5 @@ import pandas as pd +import pytest import sqlalchemy.types as sqltypes import great_expectations as gx @@ -9,6 +10,8 @@ PostgreSQLDatasourceTestConfig, ) +pytestmark = pytest.mark.postgresql + def test_postgres_multiple_null_expectations_alias_deduplication(): """ From ac17a81d2b7e7dde183a4a0314a71d6bbd52c488 Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:32:44 +0000 Subject: [PATCH 7/9] refactor: remove postgres regression test and alias dedup logic per review --- .../sqlalchemy_execution_engine.py | 17 +--- ...est_postgres_metric_alias_deduplication.py | 78 ------------------- 2 files changed, 3 insertions(+), 92 deletions(-) delete mode 100644 tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py diff --git a/great_expectations/execution_engine/sqlalchemy_execution_engine.py b/great_expectations/execution_engine/sqlalchemy_execution_engine.py index c769abd5ebef..9680a9f71447 100644 --- a/great_expectations/execution_engine/sqlalchemy_execution_engine.py +++ b/great_expectations/execution_engine/sqlalchemy_execution_engine.py @@ -1186,20 +1186,9 @@ def _organize_metrics_by_domain( # noqa: C901 # FIXME ) domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) else: - alias = metric_to_resolve.metric_name - - # Prevent "Duplicated field name in view schema" SQL errors by deduplicating aliases - existing_aliases = { - col.name for col in domain_batches[domain_id]["select"] if hasattr(col, "name") - } - - if alias in existing_aliases: - suffix = 1 - while f"{alias}_{suffix}" in existing_aliases: - suffix += 1 - alias = f"{alias}_{suffix}" - - domain_batches[domain_id]["select"].append(metric_fn.label(alias)) + domain_batches[domain_id]["select"].append( + metric_fn.label(metric_to_resolve.metric_name) + ) domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) for domain_id in list(domain_batches.keys()): diff --git a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py b/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py deleted file mode 100644 index 76898c2647b8..000000000000 --- a/tests/integration/data_sources_and_expectations/test_postgres_metric_alias_deduplication.py +++ /dev/null @@ -1,78 +0,0 @@ -import pandas as pd -import pytest -import sqlalchemy.types as sqltypes - -import great_expectations as gx -import great_expectations.expectations as gxe -from great_expectations.data_context import get_context -from tests.integration.test_utils.data_source_config.postgres import ( - PostgresBatchTestSetup, - PostgreSQLDatasourceTestConfig, -) - -pytestmark = pytest.mark.postgresql - - -def test_postgres_multiple_null_expectations_alias_deduplication(): - """ - Regression test for Issue #10926 / PR #11905. - - Verifies that metric aliasing deduplication functions correctly during SQL compilation. - Executing multiple expectations that generate identical underlying metrics - (e.g., column_values.nonnull.unexpected_count) on different columns must not trigger - a 'Duplicated field name in view schema' OperationalError. - """ - - # 1. Define schema and intentional null values - df = pd.DataFrame( - { - "field1": [1, 2, None, 4], - "field2": ["a", "b", "c", None], - "field3": [10.0, 20.0, 30.0, 40.0], - } - ) - - context = get_context(mode="ephemeral") - - # 2. Initialize Postgres fixture - batch_setup = PostgresBatchTestSetup( - config=PostgreSQLDatasourceTestConfig( - column_types={ - "field1": sqltypes.INTEGER, - "field2": sqltypes.VARCHAR, - "field3": sqltypes.FLOAT, - } - ), - data=df, - extra_data={}, - context=context, - ) - - with batch_setup.batch_test_context() as batch: - # 3. Construct Expectation Suite - suite = context.suites.add(gx.ExpectationSuite(name="alias_deduplication_suite")) - - # 4. Add multiple identical expectations on different columns - suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field1")) - suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field2")) - suite.add_expectation(gxe.ExpectColumnValuesToNotBeNull(column="field3")) - - # 5. Execute Validation - # `batch.batch_definition` is a LegacyBatchDefinition and fails - # ValidationDefinition's pydantic isinstance check. The real fluent - # BatchDefinition (created via add_batch_definition_whole_table in - # PostgresBatchTestSetup.make_batch) lives on the data asset instead. - batch_definition = batch.data_asset.batch_definitions[0] - - validation_definition = context.validation_definitions.add( - gx.ValidationDefinition( - name="alias_deduplication_validation", - data=batch_definition, - suite=suite, - ) - ) - - result = validation_definition.run() - - # 6. Assert successful execution without alias collision - assert len(result.results) == 3 From 6a374d9ebd118fdec1d48ddc93282223e3a15f2b Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:16:50 +0000 Subject: [PATCH 8/9] fix: deterministic metric alias deduplication for all sql dialects (#10926) Replaces the ClickHouse-specific random-suffix aliasing with a general, deterministic deduplication of metric aliases in _organize_metrics_by_domain, and adds a unit test proving bundled metrics get unique, deterministic aliases. --- .../sqlalchemy_execution_engine.py | 31 +++++----- ...t_sqlalchemy_metric_alias_deduplication.py | 60 +++++++++++++++++++ 2 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py diff --git a/great_expectations/execution_engine/sqlalchemy_execution_engine.py b/great_expectations/execution_engine/sqlalchemy_execution_engine.py index 9680a9f71447..a09715f6e4b0 100644 --- a/great_expectations/execution_engine/sqlalchemy_execution_engine.py +++ b/great_expectations/execution_engine/sqlalchemy_execution_engine.py @@ -6,9 +6,7 @@ import logging import math import os -import random import re -import string import traceback from collections.abc import Generator from contextlib import contextmanager @@ -1176,20 +1174,21 @@ def _organize_metrics_by_domain( # noqa: C901 # FIXME domain_batches[domain_id] = {"select": [], "metric_ids": []} batch_counters[domain_id] = new_batch_counter - if self.engine.dialect.name.lower() == GXSqlDialect.CLICKHOUSE: - domain_batches[domain_id]["select"].append( - metric_fn.label( - metric_to_resolve.metric_name.join( - random.choices(string.ascii_lowercase, k=4) - ) - ) - ) - domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) - else: - domain_batches[domain_id]["select"].append( - metric_fn.label(metric_to_resolve.metric_name) - ) - domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) + alias = metric_to_resolve.metric_name + + # Prevent "Duplicated field name in view schema" SQL errors by deduplicating aliases + existing_aliases = { + col.name for col in domain_batches[domain_id]["select"] if hasattr(col, "name") + } + + if alias in existing_aliases: + suffix = 1 + while f"{alias}_{suffix}" in existing_aliases: + suffix += 1 + alias = f"{alias}_{suffix}" + + domain_batches[domain_id]["select"].append(metric_fn.label(alias)) + domain_batches[domain_id]["metric_ids"].append(metric_to_resolve.id) for domain_id in list(domain_batches.keys()): final_domain_id, new_query_entry, new_batch_counter = self._finalize_domain_query( diff --git a/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py b/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py new file mode 100644 index 000000000000..9a1e5c25f838 --- /dev/null +++ b/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py @@ -0,0 +1,60 @@ +from unittest.mock import patch + +from great_expectations.compatibility.sqlalchemy import sqlalchemy as sa +from great_expectations.execution_engine.execution_engine import ( + MetricComputationConfiguration, +) +from great_expectations.execution_engine.sqlalchemy_execution_engine import ( + SqlAlchemyExecutionEngine, +) +from great_expectations.validator.metric_configuration import MetricConfiguration + + +def _make_bundle_entry(column: str) -> MetricComputationConfiguration: + """Build a bundle entry whose metric name collides with the others.""" + return MetricComputationConfiguration( + metric_configuration=MetricConfiguration( + metric_name="column_values.nonnull.unexpected_count", + metric_domain_kwargs={"column": column}, + metric_value_kwargs=None, + ), + metric_fn=sa.func.sum(sa.column(column)), + compute_domain_kwargs={}, + accessor_domain_kwargs={"column": column}, + metric_provider_kwargs={}, + ) + + +def test_organize_metrics_by_domain_deduplicates_metric_aliases(): + """Regression test for #10926. + + Bundling multiple expectations that resolve to the same underlying metric + (e.g. column_values.nonnull.unexpected_count on different columns) must + produce unique, deterministic SQL aliases so strict backends (ClickHouse, + BigQuery) do not fail with "Duplicated field name in view schema". + """ + engine = SqlAlchemyExecutionEngine(connection_string="sqlite://") + selectable = sa.table( + "test_table", + sa.column("field1"), + sa.column("field2"), + sa.column("field3"), + ) + + bundle = [ + _make_bundle_entry("field1"), + _make_bundle_entry("field2"), + _make_bundle_entry("field3"), + ] + + with patch.object(SqlAlchemyExecutionEngine, "get_domain_records", return_value=selectable): + queries = engine._organize_metrics_by_domain(bundle) + + assert len(queries) == 1 + labels = [select.name for select in queries[0]["select"]] + assert labels == [ + "column_values.nonnull.unexpected_count", + "column_values.nonnull.unexpected_count_1", + "column_values.nonnull.unexpected_count_2", + ] + assert len(queries[0]["metric_ids"]) == 3 From 6a71bb37dc32bcf0e7cda31bfdfb0f8ac344aa05 Mon Sep 17 00:00:00 2001 From: Temidayo Akindahunsi <98013820+TemidayoA@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:58:05 +0000 Subject: [PATCH 9/9] test: add unit marker for marker-coverage check --- .../test_sqlalchemy_metric_alias_deduplication.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py b/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py index 9a1e5c25f838..b7d481b5b2ea 100644 --- a/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py +++ b/tests/execution_engine/test_sqlalchemy_metric_alias_deduplication.py @@ -1,5 +1,7 @@ from unittest.mock import patch +import pytest + from great_expectations.compatibility.sqlalchemy import sqlalchemy as sa from great_expectations.execution_engine.execution_engine import ( MetricComputationConfiguration, @@ -9,6 +11,8 @@ ) from great_expectations.validator.metric_configuration import MetricConfiguration +pytestmark = pytest.mark.unit + def _make_bundle_entry(column: str) -> MetricComputationConfiguration: """Build a bundle entry whose metric name collides with the others."""