from __future__ import annotations
import abc
from collections.abc import Generator
from textwrap import dedent, indent
from typing import (
TYPE_CHECKING,
Any,
NamedTuple,
NewType,
)
from ..models.dialect import SQLDialect
from ..models.query import ParameterPlaceholder, Query, RenderedQuery
from .parameters import ParameterRenderer
if TYPE_CHECKING:
import csql
import csql.render.param
SQLBit = NewType("SQLBit", str)
DepNames = dict[int, str] # dict of id(query) to query name
[docs]
class QueryRenderer(abc.ABC):
ParamRenderer: type[csql.render.param.ParameterRenderer]
# mutable, replaced every render()
paramRenderer: ParameterRenderer
def __init__(
self,
ParamRenderer: type[csql.render.param.ParameterRenderer],
dialect: SQLDialect,
):
# param renderer is stateful and should only be used once.
# todo: refactor .render into a closure() or something.
self.ParamRenderer = ParamRenderer
def render(self, query: Query) -> RenderedQuery:
# this guy is only good for a single use...
self.paramRenderer = self.ParamRenderer()
return self._render(query)
@abc.abstractmethod
def _render(self, query: Query) -> RenderedQuery:
pass
[docs]
class BoringSQLRenderer(QueryRenderer):
"""Render a Query. Referenced other Queries are all assembled with this one into a CTE/with expression."""
def __renderSingleQuery(
self, query: Query, depNames: DepNames
) -> Generator[SQLBit, None, None]:
for part in query.queryParts:
if isinstance(part, str):
yield SQLBit(part)
elif isinstance(part, Query):
# isinstance(part, Query)
depName = depNames[id(part)]
yield SQLBit(depName)
elif isinstance(part, ParameterPlaceholder):
sql = self.paramRenderer.render(part)
yield SQLBit(sql)
class RenderedSingleQuery(NamedTuple):
sql: str
paramValues: list[Any]
def _renderSingleQuery(self, query: Query, depNames: DepNames) -> SQLBit:
queryBits = self.__renderSingleQuery(query, depNames)
return SQLBit("".join(queryBits))
def _render(self, query: csql.Query) -> csql.RenderedQuery:
"""Renders a query and all its dependencies into a CTE expression."""
cteParts: list[tuple[str, Query]] = []
depNames: DepNames = {}
for i, dep in enumerate(query._getDeps()):
subName = f"_subQuery{i}"
depNames[id(dep)] = subName
cteParts.append((subName, dep))
tab = "\t"
depSqls: list[SQLBit] = []
for depName, dep in cteParts:
renderedDep = self._renderSingleQuery(dep, depNames)
dedented = dedent(renderedDep).strip()
depSql = SQLBit(
f"""\
{depName} as (
{indent(dedented, tab)}
)"""
)
depSqls.append(depSql)
cteString = "with\n" + ",\n".join(depSqls)
renderedSelf = self._renderSingleQuery(query, depNames)
fullSql = (
f"{cteString}\n{dedent(renderedSelf).strip()}"
if len(cteParts) >= 1
else renderedSelf
)
paramValues, paramNames = self.paramRenderer.renderList()
return RenderedQuery(
sql=fullSql, parameters=paramValues, parameter_names=paramNames
)