from __future__ import annotations

from collections.abc import Sequence

from ...frontend.base import Fragment, Model
from ...metamodel.metamodel import Task
from relationalai.config.config import Config
from .analyzer import (
    SQL2Analysis,
    SQL2FragmentProjectionOutputColumn,
    SQL2FragmentProjectionSpec,
    SQL2ModelAnalyzer,
)
from .debug import SQL2DebugDumper
from .emitter import SQL2Emitter, SQL2SnowflakeEmitter
from .ir import (
    SQL2BinaryExpr,
    SQL2ColumnExpr,
    SQL2ExecutionPlan,
    SQL2ExecutionStep,
    SQL2Expr,
    SQL2FromItem,
    SQL2Join,
    SQL2ProgramIR,
    SQL2Query,
    SQL2SelectItem,
    SQL2SelectQuery,
    SQL2SubquerySource,
)
from .lower import SQL2Lowerer
from .optimize import SQL2ProgramOptimizer


class SQL2Compiler:
    """Clean-room SQL compiler entrypoint.

    The SQL2 implementation is being built incrementally. This class currently
    wires model analysis/table-shape sidecar generation (Phase A) and exposes
    isolated compiler boundaries.
    """

    def __init__(
        self,
        dialect=None,
        config: Config | None = None,
    ):
        self.config = config
        self.pipeline = "sql2"
        self._dialect = dialect
        self._analyzer = SQL2ModelAnalyzer()
        self._lowerer = SQL2Lowerer(dialect=dialect)
        self._optimizer = SQL2ProgramOptimizer()
        use_snowflake_emitter = type(dialect).__name__.lower().startswith("snowflake")
        self.emitter = (
            SQL2SnowflakeEmitter(dialect=dialect)
            if use_snowflake_emitter
            else SQL2Emitter(dialect=dialect)
        )
        self._debug_dumper = SQL2DebugDumper(emitter=self.emitter)
        self.execution_plan = SQL2ExecutionPlan()
        self.program_ir = SQL2ProgramIR()
        self.program_plan = None
        self.query_plan = None
        self.last_model_version: int | None = None
        self._last_model_token: tuple[int, int] | None = None
        self._model_artifacts: SQL2Analysis | None = None
        self._compile_seq = 0

    @property
    def model_artifacts(self) -> SQL2Analysis | None:
        return self._model_artifacts

    def _normalize_schema(self, schema_name: str | None) -> str | None:
        if not schema_name:
            return None
        schema_name = schema_name.strip()
        if not schema_name:
            return None
        use_duckdb_schema = type(self._dialect).__name__.lower().startswith("duckdb")
        if use_duckdb_schema and "." in schema_name:
            parts = schema_name.split(".")
            if len(parts) == 2:
                return f"{parts[0]}_{parts[1]}"
        return schema_name

    def _resolve_model_schema(self, model: Model) -> str | None:
        config_source = self.config if self.config is not None else model.config
        if config_source is None:
            return None
        model_cfg = getattr(config_source, "model", None)
        schema_name = getattr(model_cfg, "schema_", None)
        if not isinstance(schema_name, str):
            return None
        return self._normalize_schema(schema_name)

    def compile_model(self, model: Model) -> list[str]:
        target_version = model._version
        target_token = (id(model), target_version)
        if target_token == self._last_model_token:
            return []
        self._compile_seq += 1
        run_id = self._compile_seq

        self._model_artifacts = self._analyzer.analyze(model.to_metamodel())
        schema_name = self._resolve_model_schema(model)
        raw_program_ir = self._lowerer.lower_model(
            self._model_artifacts,
            schema_name=schema_name,
            source_schema_name=schema_name,
        )
        self.program_ir = self._optimizer.optimize(raw_program_ir)
        self._debug_dumper.dump_model(
            run_id=run_id,
            model_version=model._version,
            analysis=self._model_artifacts,
            raw_program_ir=raw_program_ir,
            optimized_program_ir=self.program_ir,
        )
        steps = [
            SQL2ExecutionStep(views=list(stratum.operations), semi_naive_loops=list(stratum.semi_naive_loops))
            for stratum in self.program_ir.strata
            if stratum.operations or stratum.semi_naive_loops
        ]
        self.execution_plan = SQL2ExecutionPlan(steps=steps)
        self._last_model_token = target_token
        self.last_model_version = target_version
        return [
            self.emitter.emit(view)
            for step in self.execution_plan.steps
            for view in step.views
        ]

    def _compile_projection_anchored_fragment(
        self,
        *,
        analysis: SQL2Analysis,
        projection_specs: tuple[SQL2FragmentProjectionSpec, ...],
        schema_name: str | None,
        projection_var_ids: tuple[int, ...],
        relation_task_var_ids_by_relation_id: dict[int, tuple[int, ...]],
    ) -> str | None:
        if not projection_specs:
            return None
        fragment_plan = analysis.fragment_plan
        if fragment_plan is None:
            return None
        spec_by_relation_id = {spec.relation_id: spec for spec in projection_specs}
        coalesced_sources = self._lowerer.lower_coalesced_projection_sources(
            analysis=analysis,
            projection_specs=projection_specs,
            projection_assembly=fragment_plan.projection_assembly,
            source_schema_name=schema_name,
            projection_var_ids=projection_var_ids,
            relation_task_var_ids_by_relation_id=relation_task_var_ids_by_relation_id,
        )
        coalesced_sources = self._merge_compatible_projection_sources(coalesced_sources)
        if not coalesced_sources:
            return None
        coalesced_queries: list[tuple[SQL2FragmentProjectionSpec, SQL2SelectQuery, str, int, str]] = []
        coalesced_input_var_columns: dict[int, dict[int, str]] = {}
        for coalesced_ix, source in enumerate(coalesced_sources):
            coalesced_input_var_columns[coalesced_ix] = dict(source.var_columns)
            for relation_id in source.relation_ids:
                spec = spec_by_relation_id.get(relation_id)
                if spec is None:
                    continue
                coalesced_queries.append(
                    (spec, source.query, source.key_field, coalesced_ix, source.output_anchor_join_type)
                )
        if not coalesced_queries:
            return None
        joins: list[SQL2Join] = []
        from_item: SQL2FromItem | None = None
        anchor_alias = "c_1"
        anchor_key_field: str | None = None
        next_alias_ix = 2
        available_var_sources: dict[int, tuple[str, str]] = {}
        expr_by_relation_field: dict[tuple[int, str], SQL2Expr] = {}

        alias_by_coalesced_index: dict[int, str] = {}
        for _entry_ix, (spec, relation_query, relation_key_field, coalesced_ix, _join_type) in enumerate(
            coalesced_queries
        ):
            alias = alias_by_coalesced_index.get(coalesced_ix)
            if alias is None:
                alias = anchor_alias if from_item is None else f"c_{next_alias_ix}"
                alias_by_coalesced_index[coalesced_ix] = alias
                if from_item is None:
                    from_item = SQL2FromItem(source=SQL2SubquerySource(query=relation_query), alias=alias)
                    anchor_key_field = relation_key_field
                    self._register_var_sources(
                        available_var_sources=available_var_sources,
                        relation_alias=alias,
                        relation_var_columns=coalesced_input_var_columns.get(coalesced_ix, {}),
                    )
                    next_alias_ix += 1
                else:
                    assert anchor_key_field is not None
                    base_predicates = [
                        SQL2BinaryExpr(
                            op="=",
                            left=SQL2ColumnExpr(table_alias=alias, column_name=relation_key_field),
                            right=SQL2ColumnExpr(table_alias=anchor_alias, column_name=anchor_key_field),
                        )
                    ]
                    join_predicates = self._build_projection_join_predicates(
                        relation_alias=alias,
                        relation_var_columns=coalesced_input_var_columns.get(coalesced_ix, {}),
                        available_var_sources=available_var_sources,
                        base_predicates=base_predicates,
                    )
                    joins.append(
                        SQL2Join(
                            source=SQL2FromItem(source=SQL2SubquerySource(query=relation_query), alias=alias),
                            join_type=_join_type,
                            on=join_predicates,
                        )
                    )
                    self._register_var_sources(
                        available_var_sources=available_var_sources,
                        relation_alias=alias,
                        relation_var_columns=coalesced_input_var_columns.get(coalesced_ix, {}),
                    )
                    next_alias_ix += 1

            for field_name in spec.value_field_names:
                expr_by_relation_field[(spec.relation_id, field_name)] = SQL2ColumnExpr(
                    table_alias=alias,
                    column_name=field_name,
                )

        if from_item is None or anchor_key_field is None:
            return None
        select_items = self._projection_select_items_from_plan(
            projection_output_columns=fragment_plan.projection_assembly.output_columns,
            expr_by_relation_field=expr_by_relation_field,
        )
        if not select_items:
            return None
        requires_row_identity_dedupe = fragment_plan.requires_row_identity_dedupe
        return self._emit_projection_query(
            select_items=select_items,
            from_item=from_item,
            joins=joins,
            requires_row_identity_dedupe=requires_row_identity_dedupe,
            row_identity_expr=SQL2ColumnExpr(table_alias=anchor_alias, column_name=anchor_key_field),
        )

    def _filter_projection_specs_by_relation_ids(
        self,
        *,
        projection_specs: tuple[SQL2FragmentProjectionSpec, ...],
        relation_ids: tuple[int, ...],
    ) -> tuple[SQL2FragmentProjectionSpec, ...]:
        relation_id_set = set(relation_ids)
        if not relation_id_set:
            return ()
        return tuple(spec for spec in projection_specs if spec.relation_id in relation_id_set)

    def _projection_select_items_from_plan(
        self,
        *,
        projection_output_columns: tuple[SQL2FragmentProjectionOutputColumn, ...],
        expr_by_relation_field: dict[tuple[int, str], SQL2Expr],
    ) -> list[SQL2SelectItem]:
        select_items: list[SQL2SelectItem] = []
        for item in projection_output_columns:
            expr = expr_by_relation_field.get((item.relation_id, item.field_name))
            if expr is None:
                continue
            select_items.append(SQL2SelectItem(expr=expr, alias=item.output_alias))
        return select_items

    def _dedupe_select_items(self, select_items: list[SQL2SelectItem]) -> list[SQL2SelectItem]:
        # Keep first occurrence order while removing exact duplicates.
        return list(dict.fromkeys(select_items))

    def _emit_projection_query(
        self,
        *,
        select_items: list[SQL2SelectItem],
        from_item: SQL2FromItem,
        joins: list[SQL2Join],
        requires_row_identity_dedupe: bool,
        row_identity_expr: SQL2Expr,
    ) -> str | None:
        deduped_select_items = self._dedupe_select_items(select_items)
        if not deduped_select_items:
            return None
        if not requires_row_identity_dedupe:
            flattened_query = self._flatten_direct_projection_query(
                select_items=deduped_select_items,
                from_item=from_item,
                joins=joins,
            )
            if flattened_query is not None:
                return self._emit_query(flattened_query)
            direct_query = SQL2SelectQuery(
                select_items=tuple(deduped_select_items),
                from_item=from_item,
                joins=tuple(joins),
            )
            return self._emit_query(direct_query)
        return self._emit_key_aware_projection_query(
            select_items=deduped_select_items,
            from_item=from_item,
            joins=joins,
            row_identity_expr=row_identity_expr,
        )

    def _flatten_direct_projection_query(
        self,
        *,
        select_items: list[SQL2SelectItem],
        from_item: SQL2FromItem,
        joins: list[SQL2Join],
    ) -> SQL2SelectQuery | None:
        if joins:
            return None
        if not isinstance(from_item.source, SQL2SubquerySource):
            return None
        inner_query = from_item.source.query
        if not isinstance(inner_query, SQL2SelectQuery):
            return None
        if inner_query.from_item is None:
            return None
        select_item_by_alias = {item.alias: item for item in inner_query.select_items if item.alias is not None}
        if not select_item_by_alias:
            return None

        flattened_select_items: list[SQL2SelectItem] = []
        for item in select_items:
            expr = item.expr
            if not isinstance(expr, SQL2ColumnExpr):
                return None
            if expr.table_alias != from_item.alias:
                return None
            inner_item = select_item_by_alias.get(expr.column_name)
            if inner_item is None:
                return None
            flattened_select_items.append(SQL2SelectItem(expr=inner_item.expr, alias=item.alias))

        return SQL2SelectQuery(
            select_items=tuple(flattened_select_items),
            from_item=inner_query.from_item,
            joins=inner_query.joins,
            where=inner_query.where,
            group_by=inner_query.group_by,
            having=inner_query.having,
            distinct=inner_query.distinct,
        )

    def _emit_key_aware_projection_query(
        self,
        *,
        select_items: list[SQL2SelectItem],
        from_item: SQL2FromItem,
        joins: list[SQL2Join],
        row_identity_expr: SQL2Expr,
    ) -> str | None:
        # Preserve row multiplicity by output identity, not projected value.
        # Inner DISTINCT removes duplicate join paths only within the same row.
        deduped_select_items = self._dedupe_select_items(select_items)
        if not deduped_select_items:
            return None
        row_identity_alias = "__sql2_row_identity"
        inner_select_items = [SQL2SelectItem(expr=row_identity_expr, alias=row_identity_alias)]
        outer_select_specs: list[tuple[str, str]] = []
        for item in deduped_select_items:
            if item.alias is None:
                continue
            inner_select_items.append(SQL2SelectItem(expr=item.expr, alias=item.alias))
            outer_select_specs.append((item.alias, item.alias))
        if not outer_select_specs:
            return None
        inner_query = SQL2SelectQuery(
            select_items=tuple(inner_select_items),
            from_item=from_item,
            joins=tuple(joins),
            distinct=True,
        )
        result_alias = "r_0"
        outer_select_items = [
            SQL2SelectItem(
                expr=SQL2ColumnExpr(table_alias=result_alias, column_name=inner_alias),
                alias=output_alias,
            )
            for inner_alias, output_alias in outer_select_specs
        ]
        outer_query = SQL2SelectQuery(
            select_items=tuple(outer_select_items),
            from_item=SQL2FromItem(
                source=SQL2SubquerySource(query=inner_query),
                alias=result_alias,
            ),
        )
        return self._emit_query(outer_query)

    def _build_projection_join_predicates(
        self,
        *,
        relation_alias: str,
        relation_var_columns: dict[int, str],
        available_var_sources: dict[int, tuple[str, str]],
        base_predicates: Sequence[SQL2Expr],
    ) -> tuple[SQL2Expr, ...]:
        predicates: list[SQL2Expr] = []
        seen_predicates: set[tuple[object, ...]] = set()
        for predicate in base_predicates:
            key = self._predicate_dedupe_key(predicate)
            if key in seen_predicates:
                continue
            seen_predicates.add(key)
            predicates.append(predicate)
        for var_id, column_name in relation_var_columns.items():
            source = available_var_sources.get(var_id)
            if source is None:
                continue
            source_alias, source_column = source
            predicate = SQL2BinaryExpr(
                op="=",
                left=SQL2ColumnExpr(table_alias=relation_alias, column_name=column_name),
                right=SQL2ColumnExpr(table_alias=source_alias, column_name=source_column),
            )
            key = self._predicate_dedupe_key(predicate)
            if key in seen_predicates:
                continue
            seen_predicates.add(key)
            predicates.append(predicate)
        return tuple(predicates)

    def _predicate_dedupe_key(self, predicate: SQL2Expr) -> tuple[object, ...]:
        if isinstance(predicate, SQL2BinaryExpr):
            op = predicate.op.upper()
            if op in {"=", "==", "!="}:
                left_key = repr(predicate.left)
                right_key = repr(predicate.right)
                if right_key < left_key:
                    left_key, right_key = right_key, left_key
                return ("binary_commutative", op, left_key, right_key)
        return ("expr", repr(predicate))

    def _register_var_sources(
        self,
        *,
        available_var_sources: dict[int, tuple[str, str]],
        relation_alias: str,
        relation_var_columns: dict[int, str],
    ) -> None:
        for var_id, column_name in relation_var_columns.items():
            available_var_sources.setdefault(var_id, (relation_alias, column_name))

    def _emit_query(self, query: SQL2Query) -> str:
        optimized_query = self._optimizer.optimize_query(query)
        return self.emitter.emit_query(optimized_query)

    def compile_fragment(self, fragment: Fragment):
        mm_fragment = fragment.to_metamodel()
        assert isinstance(mm_fragment, Task), "Expected fragment.to_metamodel() to return a single Task."
        # Materialize fragment metamodel first, then ensure the model program is
        # compiled for the latest model version. Some fragment-side literal/data
        # artifacts are introduced during `to_metamodel()`.
        self.compile_model(fragment._model)
        assert self._model_artifacts is not None
        analysis = self._analyzer.analyze_fragment(mm_fragment)
        fragment_plan = analysis.fragment_plan
        if fragment_plan is None:
            raise ValueError("SQL2 fragment analysis must provide fragment_plan for compile_fragment.")
        schema_name = self._resolve_model_schema(fragment._model)
        projection_specs = fragment_plan.projection_specs
        output_relation_id = fragment_plan.output_relation_id
        relation_task_var_ids_by_relation_id = dict(fragment_plan.projection_relation_task_var_ids_by_relation_id)
        query_mode = fragment_plan.query_mode
        projection_anchor_specs = self._filter_projection_specs_by_relation_ids(
            projection_specs=projection_specs,
            relation_ids=fragment_plan.projection_anchor_relation_ids,
        )
        output_anchor_projection_specs = self._filter_projection_specs_by_relation_ids(
            projection_specs=projection_specs,
            relation_ids=fragment_plan.output_anchor_projection_relation_ids,
        )

        if query_mode == "projection_anchor":
            projection_query_sql = self._compile_projection_anchored_fragment(
                analysis=analysis,
                projection_specs=projection_anchor_specs,
                schema_name=schema_name,
                projection_var_ids=fragment_plan.projection_anchor_correlation_var_ids,
                relation_task_var_ids_by_relation_id=relation_task_var_ids_by_relation_id,
            )
            if projection_query_sql is None:
                raise ValueError("Analyzer selected projection_anchor mode but no projection query could be emitted.")
            return projection_query_sql
        if query_mode == "fallback_relation":
            fallback_relation_id = fragment_plan.fallback_relation_id
            if fallback_relation_id is None:
                return None
            fallback_query = self._lowerer.lower_relation_query(
                analysis,
                fallback_relation_id,
                source_schema_name=schema_name,
            )
            return self._emit_query(fallback_query) if fallback_query is not None else None
        if query_mode == "none":
            return None
        if output_relation_id is None:
            return None

        output_query = self._lowerer.lower_relation_query(
            analysis,
            output_relation_id,
            source_schema_name=schema_name,
        )
        if output_query is None:
            return None

        output_key_field = fragment_plan.output_relation_key_field_name
        if output_key_field is None:
            raise ValueError("fragment_plan.output_relation_key_field_name is required for output_anchor mode.")
        query = SQL2SelectQuery(
            select_items=(),
            from_item=SQL2FromItem(source=SQL2SubquerySource(query=output_query), alias="out_0"),
        )
        select_items: list[SQL2SelectItem] = []
        joins: list[SQL2Join] = []
        available_var_sources: dict[int, tuple[str, str]] = {}
        expr_by_relation_field: dict[tuple[int, str], SQL2Expr] = {}
        if fragment_plan.output_relation_key_var_id is not None:
            available_var_sources[fragment_plan.output_relation_key_var_id] = ("out_0", output_key_field)
        if output_anchor_projection_specs:
            spec_by_relation_id = {spec.relation_id: spec for spec in output_anchor_projection_specs}
            coalesced_sources = self._lowerer.lower_coalesced_projection_sources(
                analysis=analysis,
                projection_specs=output_anchor_projection_specs,
                projection_assembly=fragment_plan.projection_assembly,
                source_schema_name=schema_name,
                projection_var_ids=fragment_plan.output_anchor_projection_correlation_var_ids,
                relation_task_var_ids_by_relation_id=relation_task_var_ids_by_relation_id,
            )
            coalesced_sources = self._merge_compatible_projection_sources(coalesced_sources)
            coalesced_entries: list[tuple[SQL2FragmentProjectionSpec, SQL2SelectQuery, str, int, str]] = []
            coalesced_input_var_columns: dict[int, dict[int, str]] = {}
            for coalesced_ix, source in enumerate(coalesced_sources):
                coalesced_input_var_columns[coalesced_ix] = dict(source.var_columns)
                for relation_id in source.relation_ids:
                    spec = spec_by_relation_id.get(relation_id)
                    if spec is None:
                        continue
                    coalesced_entries.append(
                        (spec, source.query, source.key_field, coalesced_ix, source.output_anchor_join_type)
                    )

            alias_by_coalesced_index: dict[int, str] = {}
            join_type_by_coalesced_index: dict[int, str] = {}
            for _spec, _rq, _rkf, coalesced_ix, join_type in coalesced_entries:
                join_type_by_coalesced_index[coalesced_ix] = join_type
            next_alias_ix = 1
            for spec, relation_query, relation_key_field, coalesced_ix, _join_type in coalesced_entries:
                alias = alias_by_coalesced_index.get(coalesced_ix)
                if alias is None:
                    alias = f"c_{next_alias_ix}"
                    next_alias_ix += 1
                    alias_by_coalesced_index[coalesced_ix] = alias
                    join_type = join_type_by_coalesced_index.get(coalesced_ix, "LEFT")
                    base_predicates = [
                        SQL2BinaryExpr(
                            op="=",
                            left=SQL2ColumnExpr(table_alias=alias, column_name=relation_key_field),
                            right=SQL2ColumnExpr(table_alias="out_0", column_name=output_key_field),
                        )
                    ]
                    join_predicates = self._build_projection_join_predicates(
                        relation_alias=alias,
                        relation_var_columns=coalesced_input_var_columns.get(coalesced_ix, {}),
                        available_var_sources=available_var_sources,
                        base_predicates=base_predicates,
                    )
                    joins.append(
                        SQL2Join(
                            source=SQL2FromItem(source=SQL2SubquerySource(query=relation_query), alias=alias),
                            join_type=join_type,
                            on=join_predicates,
                        )
                    )
                    self._register_var_sources(
                        available_var_sources=available_var_sources,
                        relation_alias=alias,
                        relation_var_columns=coalesced_input_var_columns.get(coalesced_ix, {}),
                    )

                for field_name in spec.value_field_names:
                    expr_by_relation_field[(spec.relation_id, field_name)] = SQL2ColumnExpr(
                        table_alias=alias,
                        column_name=field_name,
                    )

        if expr_by_relation_field:
            select_items = self._projection_select_items_from_plan(
                projection_output_columns=fragment_plan.projection_assembly.output_columns,
                expr_by_relation_field=expr_by_relation_field,
            )

        if not select_items:
            select_items.append(
                SQL2SelectItem(
                    expr=SQL2ColumnExpr(table_alias="out_0", column_name=output_key_field),
                    alias=output_key_field,
                )
            )
        requires_row_identity_dedupe = fragment_plan.requires_row_identity_dedupe
        assert query.from_item is not None
        return self._emit_projection_query(
            select_items=select_items,
            from_item=query.from_item,
            joins=joins,
            requires_row_identity_dedupe=requires_row_identity_dedupe,
            row_identity_expr=SQL2ColumnExpr(table_alias="out_0", column_name=output_key_field),
        )

    def _merge_compatible_projection_sources(
        self,
        sources: tuple[SQL2Lowerer.CoalescedProjectionSource, ...],
    ) -> tuple[SQL2Lowerer.CoalescedProjectionSource, ...]:
        if len(sources) <= 1:
            return sources

        merged: list[SQL2Lowerer.CoalescedProjectionSource] = []
        for source in sources:
            merged_into_existing = False
            for ix, existing in enumerate(merged):
                combined = self._try_merge_projection_sources(existing, source)
                if combined is None:
                    continue
                merged[ix] = combined
                merged_into_existing = True
                break
            if not merged_into_existing:
                merged.append(source)
        return tuple(merged)

    def _try_merge_projection_sources(
        self,
        left: SQL2Lowerer.CoalescedProjectionSource,
        right: SQL2Lowerer.CoalescedProjectionSource,
    ) -> SQL2Lowerer.CoalescedProjectionSource | None:
        if left.key_field != right.key_field:
            return None
        if left.output_anchor_join_type != right.output_anchor_join_type:
            return None
        if not self._same_projection_source_query_shape(left.query, right.query):
            return None

        left_by_alias = {
            item.alias: item
            for item in left.query.select_items
            if item.alias is not None
        }
        right_by_alias = {
            item.alias: item
            for item in right.query.select_items
            if item.alias is not None
        }
        for alias, right_item in right_by_alias.items():
            left_item = left_by_alias.get(alias)
            if left_item is not None and left_item.expr != right_item.expr:
                return None

        merged_select_items = list(left.query.select_items)
        seen_aliases = {item.alias for item in merged_select_items if item.alias is not None}
        for item in right.query.select_items:
            alias = item.alias
            if alias is not None and alias in seen_aliases:
                continue
            merged_select_items.append(item)
            if alias is not None:
                seen_aliases.add(alias)

        merged_query = SQL2SelectQuery(
            select_items=tuple(merged_select_items),
            from_item=left.query.from_item,
            joins=left.query.joins,
            where=left.query.where,
            group_by=left.query.group_by,
            having=left.query.having,
            distinct=left.query.distinct,
        )
        merged_var_columns = dict(left.var_columns)
        for var_id, column_name in right.var_columns:
            merged_var_columns.setdefault(var_id, column_name)
        merged_relation_ids = tuple(dict.fromkeys((*left.relation_ids, *right.relation_ids)))

        return SQL2Lowerer.CoalescedProjectionSource(
            relation_ids=merged_relation_ids,
            query=merged_query,
            key_field=left.key_field,
            output_anchor_join_type=left.output_anchor_join_type,
            var_columns=tuple(sorted(merged_var_columns.items())),
        )

    def _same_projection_source_query_shape(self, left: SQL2SelectQuery, right: SQL2SelectQuery) -> bool:
        return (
            left.from_item == right.from_item
            and left.joins == right.joins
            and left.where == right.where
            and left.group_by == right.group_by
            and left.having == right.having
            and left.distinct == right.distinct
        )


SQLCompiler = SQL2Compiler

__all__ = ["SQL2Compiler", "SQLCompiler"]
