zoobzio December 12, 2025 Edit this page

Overview

SQL query builders in Go typically trust developers to provide valid identifiers. ASTQL takes a different approach: validate everything against a schema before rendering.

import "github.com/zoobzio/astql/postgres"

// Define your schema
project := dbml.NewProject("myapp")
users := dbml.NewTable("users")
users.AddColumn(dbml.NewColumn("id", "bigint"))
users.AddColumn(dbml.NewColumn("username", "varchar"))
users.AddColumn(dbml.NewColumn("email", "varchar"))
project.AddTable(users)

// Create a validated instance
instance, _ := astql.NewFromDBML(project)

// Build queries - tables and fields validated against schema
result, _ := astql.Select(instance.T("users")).
    Fields(instance.F("username"), instance.F("email")).
    Where(instance.C(instance.F("id"), astql.EQ, instance.P("user_id"))).
    Render(postgres.New())

// result.SQL: SELECT "username", "email" FROM "users" WHERE "id" = :user_id
// result.RequiredParams: []string{"user_id"}

Type-safe, schema-validated, injection-resistant.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                         ASTQL                               │
│                                                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │    DBML     │    │   Builder   │    │   Render    │     │
│  │   Schema    │───▶│     API     │───▶│   Engine    │     │
│  │             │    │             │    │             │     │
│  │  Validates  │    │  Constructs │    │  Generates  │     │
│  │  Tables     │    │  AST Nodes  │    │  SQL + Params│    │
│  │  Fields     │    │             │    │             │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  Internal AST Types                  │   │
│  │  Table, Field, Param, Condition, OrderBy, Join...   │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

The DBML schema validates identifiers. The Builder API constructs an Abstract Syntax Tree. The Render engine produces parameterized SQL.

Philosophy

ASTQL draws inspiration from the principle of defense in depth. Rather than trusting developers to avoid SQL injection, the library makes injection structurally difficult:

  1. Schema validation — Tables and fields must exist in your DBML schema
  2. Instance-based API — Types cannot be constructed directly
  3. Parameterized output — Values are never interpolated into SQL
  4. Quoted identifiers — All identifiers are properly escaped
// Injection attempt blocked at construction
instance.T("users; DROP TABLE users--")  // Panics: table not in schema
instance.F("id' OR '1'='1")              // Panics: field not in schema
instance.P("id; DELETE FROM users")      // Panics: invalid parameter name

The schema acts as an allowlist. If it's not in the schema, it can't be in the query.

Capabilities

A schema-validated query builder enables:

Type Safety — Fields, tables, and parameters are validated at construction time, not at execution time.

Complex Queries — JOINs, subqueries, aggregates, window functions, CASE expressions—all with the same validation guarantees.

PostgreSQL Features — RETURNING, ON CONFLICT, DISTINCT ON, row locking, pgvector operators.

Composability — Build queries programmatically. Combine conditions. Nest subqueries.

ASTQL provides the query building layer. Execution is handled by sqlx or your database driver of choice.

Priorities

Security

SQL injection is prevented through multiple layers:

LayerProtection
Schema validationTables/fields must exist in DBML
Identifier validationAlphanumeric + underscore only
Keyword blockingRejects ;, --, ', SQL keywords
Quoted identifiersPostgreSQL double-quote escaping
Parameterized queriesNamed parameters, never interpolation

Ergonomics

The fluent builder API reads naturally:

astql.Select(table).
    Fields(field1, field2).
    Where(condition).
    OrderBy(field1, astql.ASC).
    Limit(10).
    Render(postgres.New())

Method chaining. Compile-time errors for invalid operations. Clear separation between building and rendering.

Multi-Provider Architecture

ASTQL supports multiple SQL dialects through providers:

import (
    "github.com/zoobzio/astql/postgres"
    "github.com/zoobzio/astql/sqlite"
)

// PostgreSQL
result, _ := query.Render(postgres.New())

// SQLite
result, _ := query.Render(sqlite.New())

Each provider handles dialect-specific syntax: identifier quoting, date functions, string concatenation, and unsupported feature rejection. PostgreSQL supports RETURNING, ON CONFLICT, DISTINCT ON, and pgvector operators. SQLite gracefully rejects unsupported features with clear error messages.