SQL Compiler¶
GuardSQL includes a built-in compiler that transpiles a validated AST into parameterized, dialect-specific SQL. This realizes the core security principle: the executed SQL is regenerated from the validated AST, never the original user text.
Because only constructs the AST can represent — and the emitter knows how to write — can ever reach the database, the compiler is default-deny by construction. Anything else (comments, stacked statements, unknown functions, encoding tricks) is structurally impossible to emit.
Usage¶
q, err := guardsql.Parse(input)
if err != nil {
return err
}
if err := guardsql.Validate(q, schema); err != nil {
return err
}
out, err := guardsql.Compile(q, guardsql.CompileOptions{
Dialect: guardsql.DialectSQLite,
Schema: schema,
StrictIdentifiers: true,
})
if err != nil {
return err
}
rows, err := db.Query(out.SQL, out.Args...)
CompiledSQL carries the SQL string and its positional bind arguments:
Most callers use the higher-level Config, which runs parse → validate → policy → compile in one call with secure defaults.
Guarantees¶
- Parameterized values. Every literal becomes a bind parameter (
?/$1), never string-concatenated. Value-level injection is impossible. - Resolved identifiers. Every table and column is re-resolved against the schema and re-quoted per dialect. Raw user identifier text is never passed through; unknown identifiers are an error.
- Strict identifiers. With
StrictIdentifiers(the Config default), every schema identifier must match the unquoted identifier charset. See Trust Modes.
Dialects¶
| Dialect | Constant | Identifier quoting | Placeholders |
|---|---|---|---|
| SQLite | DialectSQLite |
"col" |
? |
| PostgreSQL | DialectPostgres |
"col" |
$1, $2, … |
| MySQL / Dolt | DialectMySQL |
`col` |
? |
JSON Extraction¶
A field typed FieldJSON in the schema can be addressed with a dotted path. The
compiler emits dialect-specific JSON extraction; the path segments are
validated as bare identifiers (letters, digits, underscore).
Given payload typed as FieldJSON:
SELECT payload.finding_info.title AS t, count(*) AS n
FROM events
GROUP BY payload.finding_info.title
compiles to:
JSON path segments may be numeric to address array elements (for example
resources.0.uid). JSON values are treated as dynamically typed, so static
value-type checks are relaxed for path references. Path segments are limited
to ASCII letters, digits, and underscore, matching identifier lockdown.
Postgres extracts JSON values as text, so the compiler casts the extraction to
match the query: a numeric literal in a comparison (or SUM/AVG) casts to
numeric, and a boolean literal casts to boolean. String comparisons and
MIN/MAX are left as text.
SQLite and MySQL coerce implicitly and need no cast. In Postgres, a JSON value that is not convertible to the target type makes the query fail at execution time; filter such rows out or keep the path's type consistent in your data.
Supported Query Shape¶
The compiler currently supports single-source read queries: SELECT (columns,
aliases, and the COUNT/SUM/AVG/MIN/MAX aggregates), WHERE,
GROUP BY, HAVING, ORDER BY, and LIMIT. Joins, CTEs, and nested sources
are represented in the AST but are not yet emitted by the built-in compiler;
compile them with a backend-specific compiler if needed.
For trusted mutations (INSERT/UPDATE/DELETE), see Mutations.