Skip to content

Integrating Analytic SQL

Last Updated: January 18, 2026

This guide shows how to integrate Analytic SQL into your applications and workflows, whether as a Python library or integrated into other tools like dbt, BI platforms, or databases.

Using as a Python Library

Basic Usage

The simplest way to use Analytic SQL is as a Python library:

import asql

# Basic query
asql_query = """
from users
where status = "active"
group by country ( # as total_users )
order by -total_users
limit 10
"""

# transpile() returns a list of SQL strings (one per statement)
sql_list = asql.transpile(asql_query, write="postgres")
sql = sql_list[0]
print(sql)

API Reference

asql.transpile(sql, read="asql", write="duckdb", pretty=False, schema=None, **kwargs)

Transpiles ASQL to SQL for a target dialect.

Parameters: - sql (str): ASQL query string. Can contain multiple queries separated by semicolons. - read (str, optional): Source dialect. Default: "asql". Can also be "visual_asql" for JSON-based visual queries. - write (str, optional): Target SQL dialect. Default: "duckdb". Supported dialects include: - duckdb (default) - postgres / postgresql - mysql - bigquery - snowflake - redshift - sqlite - oracle - mssql / sqlserver - And more (see SQLGlot dialects) - pretty (bool): Whether to format SQL output with indentation. Default: False - schema (dict or Schema, optional): Schema information for column operators, join inference, etc. Pass Schema.from_graph(load_graph("graph.yaml")) to resolve metric names and relationship.attribute references; see Metrics and Relationships. - **kwargs: Additional settings passed to the ASQL dialect (e.g., week_start, infer_join_keys)

Returns: - List[str]: List of generated SQL strings (one per statement)

Raises: - ASQLSyntaxError: If ASQL syntax is invalid - ASQLCompilationError: If compilation fails - ASQLResolutionError: If a metric or relationship.attribute reference can't be resolved safely against the semantic graph

Example:

import asql

# Single query - transpile() returns a list, get first element
sql = asql.transpile("""
from users
where status == 'active'
""", write="postgres")[0]

# Query with CTE using stash as
multi_query = """
from users where status == 'active' stash as active_users
from active_users group by country ( # as total )
"""

sql = asql.transpile(multi_query, write="bigquery", pretty=True)[0]

SQL to ASQL Conversion

Convert SQL back to ASQL syntax using asql.transpile() with reversed dialects:

Example:

import asql

sql = "SELECT country, COUNT(*) AS total FROM users WHERE status = 'active' GROUP BY country"
asql_query = asql.transpile(sql, read='postgres', write='asql')[0]
# Returns:
# from users where status = 'active' group by country select country, COUNT(*) AS total

You can also specify the source dialect for better parsing:

# From BigQuery SQL
asql_query = asql.transpile(sql, read='bigquery', write='asql')[0]

# From PostgreSQL
asql_query = asql.transpile(sql, read='postgres', write='asql')[0]

Error Handling

import asql
from asql.errors import ASQLSyntaxError, ASQLCompilationError

try:
    sql = asql.transpile("""
from users
where invalid syntax
""", write="postgres")[0]
except ASQLSyntaxError as e:
    print(f"Syntax error: {e}")
except ASQLCompilationError as e:
    print(f"Compilation error: {e}")

Advanced Usage

Pretty Printing

sql = asql.transpile(asql_query, write="postgres", pretty=True)[0]
# Returns formatted SQL with indentation

Multiple Dialects

asql_query = """
from users
group by country ( # as total )
"""

dialects = ["postgres", "bigquery", "snowflake"]
for dialect in dialects:
    sql = asql.transpile(asql_query, write=dialect)[0]
    print(f"{dialect}: {sql}")

Batch Compilation

queries = [
    """
from users
where status == 'active'
""",
    """
from orders
where amount > 100
""",
    """
from products
where category == 'electronics'
"""
]

compiled = []
for query in queries:
    try:
        sql = asql.transpile(query, write="postgres")[0]
        compiled.append(sql)
    except ASQLSyntaxError as e:
        print(f"Error in query: {e}")

Integration Patterns

Integration with dbt

We're building dbt-asql — a native dbt integration that lets you write .asql model files without any wrappers.

dbt-asql (Coming Soon)

⚠️ In Development — Not yet published to PyPI

With dbt-asql, you can write dbt models directly in ASQL:

-- models/marts/revenue.asql
SET materialized = incremental;
SET unique_key = id;

from stg_orders
  where status = 'completed'
  where created_at > {{ start_date || @2024-01-01 }}
  {% if is_incremental() %}
    where created_at > (select max(created_at) from {{ this }})
  {% endif %}
  group by region, month(created_at) (
    sum(amount) as revenue,
    # as order_count
  )
  order by -revenue

Features: - No wrappers — Use .asql file extension, no {% asql %} tags - Auto ref() detection — Just use table names, dbt-asql finds model dependencies - Simplified variables — {{ start_date }} instead of {{ var('start_date') }} - Native config — SET materialized = table; instead of {{ config(...) }}

See the dbt-asql README for more details.

Manual Integration Options

Until dbt-asql is released, you can integrate ASQL manually:

Option 1: Python Pre-processing

# scripts/compile_asql.py
from pathlib import Path
import asql

def compile_asql_files():
    """Compile .asql files to .sql files for dbt."""
    for asql_file in Path("models").rglob("*.asql"):
        asql_query = asql_file.read_text()
        sql = asql.transpile(asql_query, write="postgres")[0]
        sql_file = asql_file.with_suffix(".sql")
        sql_file.write_text(sql)

if __name__ == "__main__":
    compile_asql_files()

Run before dbt:

python scripts/compile_asql.py && dbt run

Option 2: dbt Python Model

# models/active_users.py
def model(dbt, session):
    import asql

    asql_query = """
    from users
    where status == 'active'
    group by country ( # as total_users )
    """

    sql = asql.transpile(asql_query, write=dbt.config.get('target_type', 'postgres'))[0]
    return session.sql(sql)

Integration with BI Tools

Tableau Custom SQL

Create a wrapper that converts ASQL to SQL for Tableau:

# tableau_asql_wrapper.py
import asql

def tableau_query(asql_query, dialect="postgres"):
    """Convert ASQL to SQL for Tableau Custom SQL."""
    sql = asql.transpile(asql_query, write=dialect)[0]
    return sql

# Usage in Tableau Custom SQL:
# Use Python script to generate SQL, then paste into Tableau

Looker (LookML) Integration

Create a LookML macro that uses ASQL:

# looker_asql_plugin.py
import asql

def compile_asql_for_looker(asql_query, dialect):
    """Compile ASQL for Looker SQL Runner."""
    return asql.transpile(asql_query, write=dialect)[0]

Then use in LookML:

sql: |
  {% raw %}
  {{ compile_asql("""
  from users
  where status == 'active'
  """) }}
  {% endraw %}

Power BI Integration

Use Python script in Power BI:

# powerbi_asql.py
import pandas as pd
import asql
import pyodbc

def execute_asql(asql_query, connection_string, dialect="mssql"):
    """Execute ASQL query in Power BI."""
    sql = asql.transpile(asql_query, write=dialect)[0]

    conn = pyodbc.connect(connection_string)
    df = pd.read_sql(sql, conn)
    conn.close()

    return df

Integration with Databases

PostgreSQL Function

Create a PostgreSQL function that compiles ASQL:

# postgres_asql_extension.py
import asql
import psycopg2

def create_asql_function(conn):
    """Create PostgreSQL function for ASQL compilation."""
    cursor = conn.cursor()

    # This would require a PostgreSQL extension
    # For now, use Python wrapper
    pass

# Alternative: Use PL/Python

Database Driver Wrapper

Create a database driver that accepts ASQL:

# asql_driver.py
import asql
import psycopg2

class ASQLConnection:
    """Database connection that accepts ASQL queries."""

    def __init__(self, connection_string, dialect="postgres"):
        self.conn = psycopg2.connect(connection_string)
        self.dialect = dialect

    def execute(self, asql_query):
        """Execute ASQL query."""
        sql = asql.transpile(asql_query, write=self.dialect)[0]
        cursor = self.conn.cursor()
        cursor.execute(sql)
        return cursor.fetchall()

    def close(self):
        self.conn.close()

# Usage
conn = ASQLConnection("postgresql://user:pass@localhost/db")
results = conn.execute("from users where status == 'active'")
conn.close()

Building Custom Integrations

REST API Wrapper

Create a REST API that compiles ASQL:

# api.py
from flask import Flask, request, jsonify
import asql
from asql.errors import ASQLSyntaxError, ASQLCompilationError

app = Flask(__name__)

@app.route('/api/compile', methods=['POST'])
def compile_asql():
    """Compile ASQL to SQL via REST API."""
    data = request.json
    asql_query = data.get('asql')
    dialect = data.get('dialect', 'postgres')
    pretty = data.get('pretty', False)

    try:
        sql = asql.transpile(asql_query, write=dialect, pretty=pretty)[0]
        return jsonify({'sql': sql})
    except ASQLSyntaxError as e:
        return jsonify({'error': str(e)}), 400
    except ASQLCompilationError as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)

Command-Line Tool

Create a CLI tool:

# asql_cli.py
import argparse
import sys
import asql
from asql.errors import ASQLSyntaxError

def main():
    parser = argparse.ArgumentParser(description='Compile ASQL to SQL')
    parser.add_argument('query', help='ASQL query string')
    parser.add_argument('--dialect', default='postgres', help='SQL dialect')
    parser.add_argument('--pretty', action='store_true', help='Pretty print SQL')
    parser.add_argument('--file', help='Read ASQL from file')

    args = parser.parse_args()

    if args.file:
        with open(args.file) as f:
            asql_query = f.read()
    else:
        asql_query = args.query

    try:
        sql = asql.transpile(asql_query, write=args.dialect, pretty=args.pretty)[0]
        print(sql)
    except ASQLSyntaxError as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()

Jupyter Notebook Integration

Use in Jupyter notebooks:

# jupyter_asql.py
import asql
from IPython.display import display, Markdown

def asql_query(query, dialect="postgres", show_sql=True):
    """Execute ASQL query in Jupyter."""
    sql = asql.transpile(query, write=dialect, pretty=True)[0]

    if show_sql:
        display(Markdown(f"**Generated SQL:**\n```sql\n{sql}\n```"))

    return sql

# Usage in notebook
sql = asql_query("""
from users
where status == 'active'
group by country ( # as total )
""")

VS Code Extension

Create a VS Code extension that compiles ASQL:

// extension.ts
import * as vscode from 'vscode';
import { exec } from 'child_process';

export function activate(context: vscode.ExtensionContext) {
    let compileCommand = vscode.commands.registerCommand('asql.compile', () => {
        const editor = vscode.window.activeTextEditor;
        if (!editor) return;

        const asqlQuery = editor.document.getText();
        const dialect = vscode.workspace.getConfiguration('asql').get('dialect', 'postgres');

        exec(`python -c "import asql; print(asql.transpile('''${asqlQuery}''', write='${dialect}')[0])"`,
            (error, stdout, stderr) => {
                if (error) {
                    vscode.window.showErrorMessage(`Compilation error: ${stderr}`);
                    return;
                }

                // Open new document with SQL
                vscode.workspace.openTextDocument({
                    content: stdout,
                    language: 'sql'
                }).then(doc => {
                    vscode.window.showTextDocument(doc);
                });
            });
    });

    context.subscriptions.push(compileCommand);
}

Best Practices

Error Handling

Always wrap compilation in try-except blocks:

import asql
from asql.errors import ASQLSyntaxError, ASQLCompilationError

def safe_transpile(asql_query, dialect="postgres"):
    """Safely transpile ASQL with error handling."""
    try:
        return asql.transpile(asql_query, write=dialect)[0]
    except ASQLSyntaxError as e:
        # Log syntax errors
        logger.error(f"ASQL syntax error: {e}")
        raise
    except ASQLCompilationError as e:
        # Log compilation errors
        logger.error(f"ASQL compilation error: {e}")
        raise

Caching Compiled Queries

Cache compiled SQL for frequently used queries:

from functools import lru_cache
import asql

@lru_cache(maxsize=100)
def cached_transpile(asql_query, dialect="postgres"):
    """Cache compiled ASQL queries."""
    return asql.transpile(asql_query, write=dialect)[0]

Validation

Validate ASQL before compilation:

import asql
from asql.errors import ASQLSyntaxError

def validate_asql(asql_query):
    """Validate ASQL query syntax."""
    try:
        asql.transpile(asql_query, write="postgres")
        return True
    except ASQLSyntaxError:
        return False

Performance Considerations

  • Compile queries once and reuse SQL strings
  • Use connection pooling for database integrations
  • Consider caching for frequently used queries
  • Batch compile multiple queries when possible

Examples

Complete Integration Example

Here's a complete example of integrating ASQL into a data pipeline:

# data_pipeline.py
import asql
import psycopg2
from typing import List, Dict

class ASQLPipeline:
    """Data pipeline using ASQL."""

    def __init__(self, connection_string, dialect="postgres"):
        self.conn = psycopg2.connect(connection_string)
        self.dialect = dialect

    def execute_query(self, asql_query: str) -> List[Dict]:
        """Execute ASQL query and return results."""
        sql = asql.transpile(asql_query, write=self.dialect)[0]
        cursor = self.conn.cursor()
        cursor.execute(sql)

        columns = [desc[0] for desc in cursor.description]
        results = [dict(zip(columns, row)) for row in cursor.fetchall()]

        cursor.close()
        return results

    def close(self):
        """Close database connection."""
        self.conn.close()

# Usage
pipeline = ASQLPipeline("postgresql://user:pass@localhost/db")

results = pipeline.execute_query("""
from users
where status == 'active'
group by country ( # as total_users )
order by -total_users
limit 10
""")

for row in results:
    print(f"{row['country']}: {row['total_users']} users")

pipeline.close()

Visual ASQL (JSON)

Visual query builders render from the visual_asql dialect and write back through asql.json_schema.json_to_asql:

import json
import sqlglot
import asql
from asql.json_schema import json_to_asql

pipelines = json.loads(
    sqlglot.transpile("from main.orders where `Last Name` like 'A%'", read="asql", write="visual_asql")[0]
)
asql_text = json_to_asql(pipelines)

The round trip is lossless: for any ASQL query q and target dialect d, asql.transpile(q, write=d) and asql.transpile(json_to_asql(visual(q)), write=d) produce the same SQL (up to parentheses and comments). tests/test_visual_roundtrip.py checks this over a corpus for DuckDB, PostgreSQL and Snowflake.

Names and expressions. Fields that hold a name (from.table, join.table, column nodes, aliases, dimension strings, order-by columns, aggregate columns, except/rename columns) are raw identifiers; json_to_asql backtick-quotes any that is not a plain identifier (Last Name → `Last Name`, each dotted part of a table name separately). Fields that hold an expression (expression keys, raw nodes) are ASQL text pasted verbatim.

Structured nodes. Conditions use column, literal, binary (comparisons, AND/OR, arithmetic, ||), paren, unary_op, null_check (is null / is not null), in / not_in, between, like / ilike (with negate), matches, the string operators (contains, starts_with, ends_with and their i variants), function (with distinct for count(distinct x)) and star. Transforms cover where, having, qualify, join, select, extend, except, rename, replace, group_by (dimensions as names or {"expression", "alias"}; aggregates as {"function", "column", "alias"} or {"expression", "alias"}), order_by (column or expression, direction, optional nulls), limit, offset, distinct, sample (size, optional per), explode, pivot and unpivot.

Unsupported constructs stay explicit. Anything the visual form cannot model is carried verbatim rather than dropped:

  • {"type": "raw", "asql": "..."} for an expression or an order-by key (window functions, CASE, intervals, subqueries, ...)
  • {"from": {"table": "", "raw": "..."}} for a FROM source that is not a table
  • {"raw": "..."} on a pipeline for a whole body with no pipeline form (joins other than table joins, WITH RECURSIVE, a CTE that is a set operation, DISTINCT ON, ...)

A named pipeline becomes stash as name; a later pipeline that does not continue it starts with its own from, which ASQL reads as a new pipeline over the stashed CTEs.

Next Steps