Skip to content

cwida/lpts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

527 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LPTS

A DuckDB extension for optimized-plan inspection and cross-system SQL transpilation. LPTS takes DuckDB's post-optimizer logical plan and reconstructs equivalent SQL as a sequence of named CTEs.

PRAGMA Syntax

PRAGMA lpts('<query>');

Example:

INSTALL lpts FROM community;
LOAD lpts;

SET lpts_input_dialect = 'duckdb';
SET lpts_dialect = 'duckdb';

CREATE TABLE users (id INTEGER, name VARCHAR, age INTEGER);
INSERT INTO users VALUES (1, 'Alice', 30), (2, 'Bob', 22), (3, 'Carol', 28);

PRAGMA lpts('SELECT name FROM users WHERE age > 25');
WITH
t0_scan (t0_name) AS (
    SELECT  "name"
    FROM    memory.main.users
    WHERE   (age>25)
)
SELECT  t0_name AS "name"
FROM    t0_scan;

LPTS plans the query through DuckDB, optimizes it, then serializes the optimized logical plan. For more details, check the related report.

EXPLAIN (FORMAT SQL)

LPTS extends DuckDB's EXPLAIN with a SQL format. EXPLAIN (FORMAT SQL) <query> returns the optimized logical plan rendered as equivalent CTE SQL — the same output as PRAGMA lpts, but exposed as a first-class EXPLAIN statement (similar to the plan-as-SQL output in Umbra).

EXPLAIN (FORMAT SQL) SELECT name FROM users WHERE age > 25;
┌─────────────────────────────┐
│┌───────────────────────────┐│
││  Optimized Logical Plan   ││
│└───────────────────────────┘│
└─────────────────────────────┘
WITH
t0_scan (t0_name) AS (
    SELECT  "name"
    FROM    memory.main.users
    WHERE   (age>25)
)
SELECT  t0_name AS "name"
FROM    t0_scan;

Because it is a genuine EXPLAIN statement, every client treats it like any other EXPLAIN: the CLI prints the SQL as plain multi-line text (no result box), while JDBC, Python, and other clients receive the standard two-column (explain_key, explain_value) EXPLAIN result with the SQL in explain_value. It honors lpts_dialect just like PRAGMA lpts, and a query that cannot be planned or converted raises a normal error.

Supported Dialects

The dialect settings accept these values:

Dialect Accepted values
DuckDB duckdb
PostgreSQL postgres, postgresql
Spark SQL spark
Hive hive
Trino / Presto trino, presto
Snowflake snowflake
BigQuery bigquery, bq
Redshift redshift
MySQL / MariaDB mysql, mariadb

Pragmas and Functions

Function Description
EXPLAIN (FORMAT SQL) query Explain a query as equivalent CTE SQL, via a real EXPLAIN statement
PRAGMA lpts('query') Return generated CTE SQL
lpts_query('query') Table-function form of PRAGMA lpts
PRAGMA print_ast('query') Print the AST to stdout
print_ast_query('query') Table-function form of PRAGMA print_ast
lpts_normalize_query('query') Return input-dialect SQL normalized to DuckDB SQL

To verify round-trip correctness, turn on the lpts_check session setting (see Settings). Once on, every top-level SELECT runs normally and is transparently compared against its LPTS rewrite. Nondeterministic queries — for example when row order or tie-breaking is not fully specified — are detected and pass without error.

Use Cases

  • Inspect optimized DuckDB plans as SQL.
  • Debug optimizer rewrites such as filter pushdown, join reordering, top-N, materialized CTEs, and subquery decorrelation.
  • Generate a CTE program that communicates the optimized execution shape.
  • Emit SQL for another engine with lpts_dialect.
  • Convert other SQL dialect syntax to DuckDB SQL with lpts_input_dialect, then execute or inspect it.

Supported Operators

LPTS is intended to cover all logical operators produced by optimized DuckDB SELECT plans. The regression suite round-trips all 22 TPC-H queries, the SQLStorm TPC-H corpus (17k queries, zero incorrect results), and DuckDB's own sqllogic test corpus (~3300 files run under lpts_check, gated in CI-style via make test: zero wrong translations and zero invalid-SQL failures). It exercises joins, aggregates, windows, set operations, CTEs, recursive CTEs, lateral joins, table functions, DuckLake scans, and inserts.

Anything LPTS cannot faithfully reproduce fails explicitly with an LPTS_<CODE>: ... "not supported" error (a NotImplementedException) — it never silently emits wrong SQL.

Settings

Setting Type Default Description
lpts_dialect VARCHAR duckdb Output dialect for generated SQL
lpts_input_dialect VARCHAR duckdb Input dialect to normalize before DuckDB parses and plans the query
lpts_check BOOLEAN false Transparently verify round-trip correctness of every top-level SELECT (see below)
lpts_enable_data_dependent_optimizers BOOLEAN false Allow LPTS planning to use optimizers that depend on current data, statistics, cardinality estimates, row groups, or runtime dynamic filters

By default, LPTS avoids data-dependent optimizers so generated SQL does not bake in snapshot-specific facts such as WHERE false from current table statistics. Enable lpts_enable_data_dependent_optimizers when you want DuckDB's full optimized plan shape and accept that the SQL may depend on planning-time data.

Round-trip checking with lpts_check

Turn on lpts_check to verify LPTS transparently:

SET lpts_check = true;

Once on, every top-level SELECT you run is intercepted. LPTS runs the original query and its LPTS rewrite side by side and compares their result bags with an order-independent hash. The query returns its normal rows unchanged.

By default the check is strict. If the rewrite's result bag differs, LPTS raises Invalid Input Error: LPTS check failed: .... If LPTS cannot rewrite the query at all, it raises Invalid Input Error: LPTS check: unsupported query (LPTS could not check it): .... Nondeterministic queries — no fully specified order, random(), unordered aggregates, and the like — are detected and pass without error.

Queries that read LPTS's own table functions (lpts_query, print_ast_query, lpts_normalize_query) are skipped, as are queries running under statement verification (for example PRAGMA enable_verification).

Set the environment variable LPTS_CHECK_LOG to a file path to switch to log mode. With lpts_check on and LPTS_CHECK_LOG set, LPTS never raises; instead it appends one line per intercepted SELECT tagging the outcome: <n> OK (bags matched), <n> WRONG (bags differed), <n> UNSUPPORTED (LPTS deliberately refused the query with an LPTS_<CODE>: ... "not supported" error), <n> FAIL (LPTS could not rewrite it and the error was not a deliberate refusal — a translation bug), or <n> NONDETERMINISTIC: <reason> (rewritten but nondeterministic, so the comparison is not trusted, with the heuristic's explanation). <n> is the 1-based interception index. This is how DuckDB's own sqllogic corpus is run through LPTS (see docs/test.md).

Examples

CREATE TABLE events (id INTEGER, ts TIMESTAMP, name VARCHAR, "order" INTEGER);
INSERT INTO events VALUES
    (1, TIMESTAMP '2024-01-15 08:09:10', 'alpha', 10),
    (11, TIMESTAMP '2024-01-16 11:12:13', 'beta', 20);

SET lpts_dialect = 'postgres';

-- Return generated CTE SQL directly in the shell.
PRAGMA lpts(
    'SELECT strftime(ts, ''%Y-%m-%d'') AS day
     FROM events
     WHERE id > 10
     ORDER BY day'
);

-- Return generated CTE SQL as a table row, useful in scripts and tests.
SELECT sql
FROM lpts_query(
    'SELECT strftime(ts, ''%Y-%m-%d'') AS day
     FROM events
     WHERE id > 10
     ORDER BY day'
);
WITH
t0_scan (ts) AS (
    SELECT  ts
    FROM    events
    WHERE   (id>10)
)
SELECT    to_char(ts, 'YYYY-MM-DD') AS "day"
FROM      t0_scan
ORDER BY  to_char(ts, 'YYYY-MM-DD') ASC NULLS LAST;
SET lpts_dialect = 'duckdb';

-- Turn on transparent round-trip checking once per session.
SET lpts_check = true;

-- Runs normally and returns its rows; raises only if LPTS rewrites it wrong.
SELECT name FROM events WHERE id > 10 ORDER BY name;
name
----
beta
-- Print the AST tree to stdout for interactive debugging.
PRAGMA print_ast('SELECT name FROM events WHERE id > 10 ORDER BY name');

-- Return the AST tree as a table row, useful for tools and regression tests.
SELECT ast
FROM print_ast_query('SELECT name FROM events WHERE id > 10 ORDER BY name');
SET lpts_input_dialect = 'mysql';

-- Normalize source-dialect SQL to DuckDB SQL before planning or execution.
SELECT sql
FROM lpts_normalize_query(
    'SELECT `order`, DATE_FORMAT(ts, ''%Y-%m-%d %H:%i:%s'') AS formatted FROM events LIMIT 5, 10'
);
SELECT "order", strftime(ts, '%Y-%m-%d %H:%M:%S') AS formatted FROM events LIMIT 10 OFFSET 5

Documentation

  • Building - build, local loading, updating, and CLion setup
  • Tests - SQLLogicTest conventions and the DuckDB-corpus coverage gate (make test)
  • Benchmark - SQLStorm benchmark runner

Maintainer: ila

About

Logical Plan To SQL DuckDB Extension

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages