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
| Scenario | Use |
|---|---|
| Static field names in code | instance.F("email") |
| User-provided field names | instance.TryF(userInput) |
| Configuration-driven queries | instance.TryT(config.TableName) |
| Tests | Either 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
| Error | Cause |
|---|---|
table 'X' not found in schema | Table doesn't exist in DBML |
field 'X' not found in schema | Field doesn't exist in any table |
invalid parameter name: X | Parameter contains invalid characters |
alias must be single lowercase letter | Table 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
}
}