From 439c066ee4bc5bfae93dd5bae412c93674e04eea Mon Sep 17 00:00:00 2001 From: Ting-Hong Shieh <32212900+ting-hong-shieh@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:21:19 +0800 Subject: [PATCH] docs: Add docstring examples to the unparser module Every public function in `Dialect` and `Unparser` lacked the usage example required by AGENTS.md. Each now carries a doctest built on the same input table, so the examples run under pytest's --doctest-modules. The examples show what actually differs between dialects: the default dialect leaves identifiers unquoted, MySQL and SQLite quote with backticks, PostgreSQL and DuckDB quote with double quotes. Also correct the `Dialect` class summary, which described it as "DataFusion data catalog", and describe what `with_pretty` does rather than restating its name. --- python/datafusion/unparser.py | 102 +++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/python/datafusion/unparser.py b/python/datafusion/unparser.py index 7ca5b9190..226474323 100644 --- a/python/datafusion/unparser.py +++ b/python/datafusion/unparser.py @@ -25,7 +25,12 @@ class Dialect: - """DataFusion data catalog.""" + """The SQL dialect an :py:class:`Unparser` writes. + + The dialect decides how the generated SQL is spelled - most visibly how + identifiers are quoted - so the same logical plan produces different SQL + text for each dialect. + """ def __init__(self, dialect: unparser_internal.Dialect) -> None: """This constructor is not typically called by the end user.""" @@ -33,43 +38,122 @@ def __init__(self, dialect: unparser_internal.Dialect) -> None: @staticmethod def default() -> "Dialect": - """Create a new default dialect.""" + """Create a new default dialect. + + This dialect leaves identifiers unquoted. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a FROM t").logical_plan() + >>> Unparser(Dialect.default()).plan_to_sql(plan) + 'SELECT t.a FROM t' + """ return Dialect(unparser_internal.Dialect.default()) @staticmethod def mysql() -> "Dialect": - """Create a new MySQL dialect.""" + """Create a new MySQL dialect. + + This dialect quotes identifiers with backticks. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a FROM t").logical_plan() + >>> Unparser(Dialect.mysql()).plan_to_sql(plan) + 'SELECT `t`.`a` FROM `t`' + """ return Dialect(unparser_internal.Dialect.mysql()) @staticmethod def postgres() -> "Dialect": - """Create a new PostgreSQL dialect.""" + """Create a new PostgreSQL dialect. + + This dialect quotes identifiers with double quotes. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a FROM t").logical_plan() + >>> Unparser(Dialect.postgres()).plan_to_sql(plan) + 'SELECT "t"."a" FROM "t"' + """ return Dialect(unparser_internal.Dialect.postgres()) @staticmethod def sqlite() -> "Dialect": - """Create a new SQLite dialect.""" + """Create a new SQLite dialect. + + This dialect quotes identifiers with backticks. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a FROM t").logical_plan() + >>> Unparser(Dialect.sqlite()).plan_to_sql(plan) + 'SELECT `t`.`a` FROM `t`' + """ return Dialect(unparser_internal.Dialect.sqlite()) @staticmethod def duckdb() -> "Dialect": - """Create a new DuckDB dialect.""" + """Create a new DuckDB dialect. + + This dialect quotes identifiers with double quotes. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a FROM t").logical_plan() + >>> Unparser(Dialect.duckdb()).plan_to_sql(plan) + 'SELECT "t"."a" FROM "t"' + """ return Dialect(unparser_internal.Dialect.duckdb()) class Unparser: - """DataFusion unparser.""" + """Converts a :py:class:`~datafusion.plan.LogicalPlan` back into SQL text.""" def __init__(self, dialect: Dialect) -> None: """This constructor is not typically called by the end user.""" self.unparser = unparser_internal.Unparser(dialect.dialect) def plan_to_sql(self, plan: LogicalPlan) -> str: - """Convert a logical plan to a SQL string.""" + """Convert a logical plan to a SQL string. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a, b FROM t WHERE a > 1").logical_plan() + >>> Unparser(Dialect.default()).plan_to_sql(plan) + 'SELECT t.a, t.b FROM t WHERE (t.a > 1)' + """ return self.unparser.plan_to_sql(plan._raw_plan) def with_pretty(self, pretty: bool) -> "Unparser": - """Set the pretty flag.""" + """Set the pretty flag. + + When set, redundant parentheses are omitted from the generated SQL. + The unparser is modified in place and returned, so the call can be + chained. + + Example usage: + + >>> ctx = dfn.SessionContext() + >>> _ = ctx.from_pydict({"a": [1, 2, 3], "b": [10, 20, 30]}, name="t") + >>> plan = ctx.sql("SELECT a, b FROM t WHERE a > 1").logical_plan() + >>> Unparser(Dialect.default()).plan_to_sql(plan) + 'SELECT t.a, t.b FROM t WHERE (t.a > 1)' + >>> Unparser(Dialect.default()).with_pretty(True).plan_to_sql(plan) + 'SELECT t.a, t.b FROM t WHERE t.a > 1' + """ self.unparser = self.unparser.with_pretty(pretty) return self