zoobzio December 12, 2025 Edit this page

Schema Validation

ASTQL validates queries against a DBML schema. This guide covers schema setup, validation behavior, and error handling.

Defining a Schema

Create a DBML project with tables and columns:

import "github.com/zoobzio/dbml"

project := dbml.NewProject("myapp")

// Users table
users := dbml.NewTable("users")
users.AddColumn(dbml.NewColumn("id", "bigint"))
users.AddColumn(dbml.NewColumn("username", "varchar"))
users.AddColumn(dbml.NewColumn("email", "varchar"))
users.AddColumn(dbml.NewColumn("active", "boolean"))
users.AddColumn(dbml.NewColumn("created_at", "timestamp"))
project.AddTable(users)

// Posts table
posts := dbml.NewTable("posts")
posts.AddColumn(dbml.NewColumn("id", "bigint"))
posts.AddColumn(dbml.NewColumn("user_id", "bigint"))
posts.AddColumn(dbml.NewColumn("title", "varchar"))
posts.AddColumn(dbml.NewColumn("content", "text"))
posts.AddColumn(dbml.NewColumn("published", "boolean"))
project.AddTable(posts)

Creating an Instance

Bind the schema to an ASTQL instance:

instance, err := astql.NewFromDBML(project)
if err != nil {
    return err
}

The instance caches table and field lookups for fast validation.

Validation Behavior

Table Validation

Tables must exist in the schema:

instance.T("users")        // Valid: table exists
instance.T("nonexistent")  // Panics: table not found

Field Validation

Fields must exist in at least one table:

instance.F("email")        // Valid: exists in users table
instance.F("title")        // Valid: exists in posts table
instance.F("foo")          // Panics: field not found

Fields are validated by name only, not by table association. This allows using the same field name across tables in JOINs.

Parameter Validation

Parameters must be valid SQL identifiers:

instance.P("user_id")      // Valid
instance.P("email123")     // Valid
instance.P("invalid-name") // Panics: contains hyphen
instance.P("1starts")      // Panics: starts with number

Panic vs Error

By default, validation failures panic. This catches errors early during development:

// These panic on invalid input
table := instance.T("users")
field := instance.F("email")
param := instance.P("value")

Try Variants

For runtime validation with user input, use Try variants:

table, err := instance.TryT(tableName)
if err != nil {
    return fmt.Errorf("invalid table: %w", err)
}

field, err := instance.TryF(fieldName)
if err != nil {
    return fmt.Errorf("invalid field: %w", err)
}

param, err := instance.TryP(paramName)
if err != nil {
    return fmt.Errorf("invalid param: %w", err)
}

When to Use Each

ScenarioUse
Static field names in codeinstance.F("email")
User-provided field namesinstance.TryF(userInput)
Configuration-driven queriesinstance.TryT(config.TableName)
TestsEither works

Table Aliases

Table aliases enable JOINs with field disambiguation:

users := instance.T("users", "u")  // Alias: u
posts := instance.T("posts", "p")  // Alias: p

Alias Restrictions

Aliases must be single lowercase letters (a-z):

instance.T("users", "u")     // Valid
instance.T("users", "x")     // Valid
instance.T("users", "ab")    // Panics: not single letter
instance.T("users", "U")     // Panics: not lowercase
instance.T("users", "1")     // Panics: not a letter

This restriction prevents SQL injection through alias names.

Using Aliases with Fields

Prefix fields with their table alias:

users := instance.T("users", "u")
posts := instance.T("posts", "p")

// Fields with table prefix
userID := instance.WithTable(instance.F("id"), "u")      // u."id"
postUserID := instance.WithTable(instance.F("user_id"), "p") // p."user_id"

Schema Organization

Single Schema Instance

Create one instance per schema and reuse it:

// In a package-level variable or dependency injection
var db *astql.ASTQL

func init() {
    project := buildSchema()
    instance, err := astql.NewFromDBML(project)
    if err != nil {
        panic(err)
    }
    db = instance
}

func GetUsers() (*astql.QueryResult, error) {
    return astql.Select(db.T("users")).
        Fields(db.F("username"), db.F("email")).
        Render(postgres.New())
}

Multiple Schemas

For microservices with different database schemas:

var (
    usersDB    *astql.ASTQL  // User service schema
    ordersDB   *astql.ASTQL  // Order service schema
    analyticsDB *astql.ASTQL // Analytics schema
)

Each instance validates against its own schema independently.

Dynamic Queries

Safe Dynamic Field Selection

func SelectFields(instance *astql.ASTQL, tableName string, fieldNames []string) (*astql.QueryResult, error) {
    table, err := instance.TryT(tableName)
    if err != nil {
        return nil, err
    }

    // Use instance.Fields() to get a typed slice
    fields := instance.Fields()
    for _, name := range fieldNames {
        field, err := instance.TryF(name)
        if err != nil {
            return nil, fmt.Errorf("invalid field %q: %w", name, err)
        }
        fields = append(fields, field)
    }

    return astql.Select(table).Fields(fields...).Render(postgres.New())
}

Dynamic Conditions

func BuildFilter(instance *astql.ASTQL, filters map[string]string) (astql.ConditionItem, error) {
    // Use instance.ConditionItems() to get a typed slice
    conditions := instance.ConditionItems()

    for fieldName, paramName := range filters {
        field, err := instance.TryF(fieldName)
        if err != nil {
            return nil, err
        }
        param, err := instance.TryP(paramName)
        if err != nil {
            return nil, err
        }
        conditions = append(conditions, instance.C(field, astql.EQ, param))
    }

    if len(conditions) == 0 {
        return nil, nil
    }
    if len(conditions) == 1 {
        return conditions[0], nil
    }
    return instance.And(conditions...), nil
}

Validation Errors

Error Types

ErrorCause
table 'X' not found in schemaTable doesn't exist in DBML
field 'X' not found in schemaField doesn't exist in any table
invalid parameter name: XParameter contains invalid characters
alias must be single lowercase letterTable alias is not a-z

Handling Errors

result, err := query.Render(postgres.New())
if err != nil {
    switch {
    case strings.Contains(err.Error(), "not found in schema"):
        // Schema mismatch - likely a bug or migration issue
        log.Error("schema validation failed", "error", err)
    case strings.Contains(err.Error(), "invalid"):
        // Invalid input
        return nil, fmt.Errorf("invalid query: %w", err)
    default:
        return nil, err
    }
}