"""Target-engine model for GIQL transpilation.
A :class:`Target` is a first-class SQL target engine: it carries a
:class:`Capabilities` set describing what the engine supports and the
sqlglot output dialect used to serialize standard AST for that engine.
This is step 1 of epic #137. The targets and capability descriptors defined
here are the foundation that later steps build on — the operator-expander
registry is keyed by ``(target, operator)`` and emission choices are
capability lookups rather than scattered ``if dialect == ...`` branches.
As of #143/#145 the model actively drives emission: the NEAREST LATERAL-vs-window
form (``supports_lateral``) and the coordinate-canonicalization wrapper and
NEAREST/DISJOIN passthroughs (``supports_star_replace``) read from the active
target's capabilities. The INTERSECTS join strategy is instead selected by the
operator-expander registry (#169): DuckDB registers a ``(DuckDBTarget, Intersects)``
IEJoin override, and every other target falls back to the generic naive-predicate
expander — no capability flag is consulted.
"""
from dataclasses import dataclass
[docs]
@dataclass(frozen=True)
class Capabilities:
"""Feature set of a SQL target engine.
Each field is a portable choice that later steps of epic #137 turn into
a capability lookup instead of a hardcoded dialect branch.
Parameters
----------
supports_lateral : bool
Whether the engine supports ``LATERAL`` / correlated joins. Drives the
NEAREST LATERAL-vs-window-function strategy (#142): a correlated NEAREST
expands to a portable correlated ``LATERAL`` subquery where this holds
and to a decorrelated window-function form where it does not. This
capability is the single source of truth — the former generator-level
``SUPPORTS_LATERAL`` attribute has been removed.
supports_star_replace : bool
Whether the engine supports ``SELECT * REPLACE (...)``. Drives every
coordinate-canonicalization site — the canonicalizer wrapper CTE and the
DISJOIN / NEAREST passthroughs (#143/#145): ``* REPLACE`` where supported,
the portable ``* EXCEPT`` projection otherwise. Supported by
DuckDB / BigQuery / Snowflake / ClickHouse; not by PostgreSQL,
SQLite, or DataFusion.
supports_qualify : bool
Whether the engine supports the ``QUALIFY`` clause. Reserved: no
emission path consumes it yet (a future window-function operator
port would).
"""
supports_lateral: bool
supports_star_replace: bool
supports_qualify: bool
[docs]
@dataclass(frozen=True)
class Target:
"""A SQL target engine.
Subclasses declare the engine ``name``, the ``sqlglot_dialect`` used to
serialize AST for that engine (``None`` selects sqlglot's default
generic serialization), and the engine ``capabilities``.
Targets are frozen, value-equal, and hashable: two ``DuckDBTarget()``
instances compare equal and hash alike, so the operator-expander registry
(#138) can key on a resolved target by value. Equality is class-scoped —
``GenericTarget() != DataFusionTarget()`` even where their fields overlap.
"""
name: str
sqlglot_dialect: str | None
capabilities: Capabilities
[docs]
@dataclass(frozen=True)
class GenericTarget(Target):
"""Portable SQL-92-ish target with no engine-specific features.
This is the default target (``dialect=None``). Its capabilities are the
conservative, maximally portable baseline, serialized through sqlglot's
dialect-less default (``sqlglot_dialect = None``).
"SQL-92-ish", not strict SQL-92: because ``supports_star_replace=False``,
every coordinate-canonicalization site over a **non-canonical** target falls
back to a ``SELECT * EXCEPT (...)`` projection (re-appending the recomputed
interval columns) — the canonicalizer wrapper CTE and the DISJOIN / NEAREST
passthroughs alike. A star-projected CLUSTER is a further ``* EXCEPT`` site
(independent of canonicalization): it hides its synthesized
``__giql_is_new_cluster`` flag with ``* EXCEPT`` (#184). ``* EXCEPT`` is **not**
SQL-92 and is **not DuckDB-runnable** — it is a DataFusion-family extension — so
the generic target's output from any of these sites runs only on an
``* EXCEPT``-capable engine (transpile with ``dialect="duckdb"`` to execute on
DuckDB, where it spells the exclusion ``EXCLUDE``). A canonical (0-based
half-open) target with no star-projected CLUSTER passes the row through as a
plain, fully portable ``SELECT *``.
"""
name: str = "generic"
sqlglot_dialect: str | None = None
capabilities: Capabilities = Capabilities(
supports_lateral=True,
supports_star_replace=False,
supports_qualify=False,
)
[docs]
@dataclass(frozen=True)
class DuckDBTarget(Target):
"""DuckDB target.
Serializes through sqlglot's ``duckdb`` dialect and uses the IEJoin
per-partition plan for column-to-column INTERSECTS joins, registered as the
``(DuckDBTarget, Intersects)`` override in the operator-expander registry
(:mod:`giql.expanders.intersects_duckdb`, #169).
Serializing through the ``duckdb`` dialect makes null ordering **explicit** on
``ORDER BY`` / window terms, emitting ``NULLS FIRST`` where the generic
(dialect-less) serialization leaves it implicit. This is result-preserving for
every migrated operator because each sorts on a non-null coordinate key
(chromosome / position, or a same-chromosome-filtered distance), so the
explicit ``NULLS FIRST`` never reorders a NULL into or out of the top rows. A
future operator ordering on a nullable key would need to re-verify this before
relying on the DuckDB serialization being a semantic no-op.
"""
name: str = "duckdb"
sqlglot_dialect: str | None = "duckdb"
capabilities: Capabilities = Capabilities(
supports_lateral=True,
supports_star_replace=True,
supports_qualify=True,
)
[docs]
@dataclass(frozen=True)
class DataFusionTarget(Target):
"""Apache DataFusion target.
sqlglot has no DataFusion dialect, so serialization uses the generic form
(``sqlglot_dialect = None``); this is the finalized strategy (#145) — the
portable SQL the generic generator emits runs on DataFusion, verified
end-to-end by the cross-target oracle (``tests/integration/datafusion/``):
every operator at the default encoding, and the canonicalizing operators
(DISJOIN / NEAREST) across all four coordinate encodings plus custom-column
and strand schemas.
The capability values below are validated against a real DataFusion engine by
that oracle: ``supports_lateral=False`` (no correlated-LATERAL physical plan,
so NEAREST takes the decorrelated window fallback), ``supports_star_replace=
False`` (DataFusion supports ``* EXCEPT`` / ``* EXCLUDE`` but not
``* REPLACE``, so the canonicalizer and the NEAREST/DISJOIN passthroughs emit
the portable ``* EXCEPT`` form), and ``supports_qualify=False``. DataFusion
registers no INTERSECTS override, so a column-to-column INTERSECTS join falls
back to the generic naive overlap predicate — a plain ``ON`` condition
DataFusion plans as a hash join keyed on ``chrom`` with the position
inequalities as a residual join filter (#167).
A ``SELECT *`` / ``SELECT b.*`` over a correlated NEAREST would otherwise expose
the decorrelated fallback's reserved ``__giql_x_*`` rank/key columns; a statement
finalizer wraps the enclosing ``SELECT`` in ``SELECT * EXCEPT (...)`` on the
fallback path (#160), so the cross-target identity claim holds for star
projections over a correlated NEAREST unconditionally. The finalizer re-locates
its target join by a reserved ``meta`` tag and mints its reserved column names
per fallback (#172), so the claim now holds through the two former residual
compositions too: a correlated NEAREST re-surfaced by an enclosing ``SELECT *``
*outside* its own SELECT (e.g. a wrapping ``CLUSTER``, whose ``copy()`` +
``transplant`` the tag survives) and two correlated NEAREST fallbacks in one
query (whose now-distinct per-run reserved names each ``* EXCEPT`` independently).
"""
name: str = "datafusion"
sqlglot_dialect: str | None = None
capabilities: Capabilities = Capabilities(
supports_lateral=False,
supports_star_replace=False,
supports_qualify=False,
)
# Public dialect names only. ``generic`` is intentionally absent: ``None`` is
# the sole public way to select it (see :func:`resolve_target`).
_TARGETS_BY_NAME: dict[str, type[Target]] = {
DuckDBTarget.name: DuckDBTarget,
DataFusionTarget.name: DataFusionTarget,
}
def resolve_target(dialect: str | None) -> Target:
"""Resolve a ``dialect`` parameter to a :class:`Target` instance.
Parameters
----------
dialect : str | None
The target dialect name. ``None`` resolves to :class:`GenericTarget`;
``"duckdb"`` and ``"datafusion"`` resolve to their respective built-in
targets. Any other name is resolved against the plugin registry — a
custom :class:`Target` declared through
:meth:`giql.expander.ExpanderRegistry.register_target` (or as a side
effect of :func:`giql.expander.register`) is selectable by its ``name``.
Returns
-------
Target
The resolved target instance.
Raises
------
ValueError
If *dialect* is neither a built-in name nor a registered custom target.
"""
if dialect is None:
return GenericTarget()
target_cls = _TARGETS_BY_NAME.get(dialect)
if target_cls is not None:
return target_cls()
# A non-built-in name may be a custom target registered on the plugin hub.
# Imported lazily so this module stays import-cycle-free: ``giql.expander``
# imports ``Target`` / ``GenericTarget`` from here at module load.
from giql.expander import REGISTRY
registered = REGISTRY.target(dialect)
if registered is not None:
return registered
raise ValueError(
f"Unknown dialect: {dialect!r}. Supported: 'duckdb', 'datafusion', None, "
"or a custom target registered via "
"giql.expander.REGISTRY.register_target()."
)