sqlglot SQL transpilation between SQLite, Postgres and MySQL: working versions (tested 2026-10-16)
Parse, transpile and rewrite SQL across SQLite, Postgres and MySQL dialects with sqlglot, and inspect the resulting AST offline.
passed9 passed · 0 failed2026-10-16 21:57 UTC- Status
- passed
- Last passed
- 2026-10-16 21:57 UTC
- Tests
- 9 / 9
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
sqlglot@30.23.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sqlglot-transpile-dialects") (install), or see them on GitHub.
requirements.txt
sqlglot==30.23.0 pytest==9.1.1
transpile.py
import sqlglot
from sqlglot import exp
DIALECTS = {"sqlite", "postgres", "mysql"}
def transpile(sql: str, read: str, write: str) -> str:
"""Translate one statement from one dialect to another."""
(out,) = sqlglot.transpile(sql, read=read, write=write)
return out
def parse(sql: str, read: str = "sqlite") -> exp.Expression:
return sqlglot.parse_one(sql, read=read)
def tables(sql: str, read: str = "sqlite") -> list[str]:
return [t.name for t in parse(sql, read).find_all(exp.Table)]
def columns(sql: str, read: str = "sqlite") -> list[str]:
return [c.sql() for c in parse(sql, read).find_all(exp.Column)]
def rename_table(
sql: str, old: str, new: str, read: str = "sqlite", write: str | None = None
) -> str:
"""Rename a table and every qualified reference to it.
A plain Table-node rename leaves `users.id` style qualifiers pointing at the
old name, so rewrite Column qualifiers as well.
"""
tree = parse(sql, read)
for table in tree.find_all(exp.Table):
if table.name == old:
table.set("this", exp.to_identifier(new))
for column in tree.find_all(exp.Column):
if column.table == old:
column.set("table", exp.to_identifier(new))
return tree.sql(dialect=write or read)
tests/test_transpile.py
import pytest
from sqlglot import exp
from transpile import columns, parse, rename_table, tables, transpile
def test_mysql_limit_swaps_to_offset():
# MySQL "LIMIT offset, count" becomes Postgres "LIMIT count OFFSET offset".
sql = transpile("SELECT id FROM users LIMIT 2, 5", read="mysql", write="postgres")
assert sql == "SELECT id FROM users LIMIT 5 OFFSET 2"
def test_sqlite_concat_becomes_mysql_concat():
# SQLite / Postgres use ||, MySQL has no || operator and needs CONCAT().
sql = transpile("SELECT first || last FROM people", read="sqlite", write="mysql")
assert sql == "SELECT CONCAT(first, last) FROM people"
def test_group_concat_gains_default_separator():
# sqlglot maps GROUP_CONCAT to STRING_AGG and supplies the trailing
# separator Postgres requires, even when the input omitted one.
sql = transpile("SELECT GROUP_CONCAT(tag) FROM posts", read="sqlite", write="postgres")
assert sql == "SELECT STRING_AGG(tag, ',') FROM posts"
def test_sqlite_strftime_becomes_to_char():
# A date function with no direct equivalent is rewritten to a cast plus
# the target dialect's formatting function.
sql = transpile("SELECT strftime('%Y', created) FROM posts", read="sqlite", write="postgres")
assert sql == "SELECT TO_CHAR(CAST(created AS TIMESTAMP), 'YYYY') FROM posts"
def test_ast_lists_tables_and_columns():
sql = "SELECT u.id, o.total FROM users AS u JOIN orders o ON o.uid = u.id"
assert isinstance(parse(sql, read="sqlite"), exp.Select)
assert tables(sql) == ["users", "orders"]
assert columns(sql) == ["u.id", "o.total", "o.uid", "u.id"]
def test_rename_table_rewrites_qualified_columns():
renamed = rename_table("SELECT id, users.email FROM users WHERE users.active = 1", "users", "people")
assert renamed == "SELECT id, people.email FROM people WHERE people.active = 1"
@pytest.mark.parametrize("dialect", ["sqlite", "postgres", "mysql"])
def test_unknown_select_still_parses(dialect):
assert isinstance(parse("SELECT 1 AS one", read=dialect), exp.Select)
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.