Help us shape the future of Go! Take the 2026 Go Developer Survey

guardsql

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 7 Imported by: 0

README

GuardSQL

Go CI Go Lint Go SAST Coverage Docs Visualization License

GuardSQL is a small, application-neutral query language for Grokify analytics services. It is intended to provide a Jira Query Language style interface for SaaS reporting without passing customer-authored SQL directly to databases.

This repository is the Go engine for GuardSQL: parser, AST, schema validation, policy checks, a built-in SQL compiler (SQLite/PostgreSQL/MySQL with JSON extraction), a trusted mutation builder, and lightweight authorization adapters. Runtime integrations for other language stacks should live in separate repositories or modules.

GuardSQL is a transpiler: untrusted SQL in, trusted parameterized SQL out. It parses user-authored text into a Go AST, validates and policy-checks that AST, then regenerates dialect-specific parameterized SQL from it. The original text is never executed — only constructs the AST can represent and the compiler knows how to emit can reach the database, which makes the accepted surface default-deny by construction.

query text -> parse -> validate -> policy -> compile -> parameterized SQL + args

For an end-to-end walkthrough (schema, untrusted analytics with row scope, and trusted mutations), see Getting Started; for why the model is secure and what the host must still enforce, see Security Hardening.

Scope

The first parser is read-only and produces OperationRead queries:

SELECT id, name, priority
FROM roadmap_items
WHERE status IN ("planned", "in_progress") AND score >= 50
ORDER BY score DESC
LIMIT 25

Supported clauses:

  • SELECT ... FROM ...
  • WHERE with AND, OR, NOT, parentheses, =, !=, <, <=, >, >=, IN, CONTAINS, IS NULL, and IS NOT NULL
  • GROUP BY
  • HAVING
  • ORDER BY
  • LIMIT
  • WITH common table expressions
  • JOIN, INNER JOIN, LEFT JOIN, LEFT OUTER JOIN, RIGHT JOIN, RIGHT OUTER JOIN, FULL JOIN, FULL OUTER JOIN, and CROSS JOIN
  • nested query sources in FROM (...)

Supported aggregate functions:

  • COUNT(*)
  • COUNT(field)
  • SUM(field)
  • AVG(field)
  • MIN(field)
  • MAX(field)

The in-memory evaluator supports filters, grouping, aggregates, HAVING, ordering, limits, and projection for single-source queries. Joins, CTEs, and nested query sources are represented in the AST and should be executed by backend-specific compilers. Self joins are represented as normal joins and require aliases on both sources.

Compiling to SQL

GuardSQL ships a compiler that transpiles a validated AST into parameterized, dialect-specific SQL (SQLite, PostgreSQL, MySQL/Dolt). Values become bind parameters; identifiers are re-resolved against the schema and re-quoted.

out, err := guardsql.Compile(q, guardsql.CompileOptions{
    Dialect: guardsql.DialectPostgres, Schema: schema, StrictIdentifiers: true,
})
// out.SQL, out.Args -> db.Query(out.SQL, out.Args...)

A FieldJSON column can be addressed with a dotted path, compiled to dialect-specific JSON extraction:

SELECT payload.finding_info.title AS t, count(*) AS n
FROM events GROUP BY payload.finding_info.title
-- SQLite: json_extract("payload", ?) ...  args: ["$.finding_info.title", ...]

See the SQL Compiler docs.

Config & Trust Modes

Config is the recommended entry point: parse → validate → policy → compile in one call, with secure defaults. The same engine serves customer-supplied SQL and internal application SQL through two profiles:

// Customer SQL: read-only, secret/PII columns stripped, identifiers locked, LIMIT required.
cfg := guardsql.NewUntrustedConfig(schema, guardsql.DialectSQLite)
out, err := cfg.Compile(customerQuery)

// Internal SQL: reads + mutations, full column access, still parameterized and locked.
tcfg := guardsql.NewTrustedConfig(schema, guardsql.DialectPostgres)

Each schema field carries a data classification — ClassPublic (default), ClassPII, ClassInternal, or ClassSecret. Classified columns are stripped from the effective schema unless AllowPII / AllowInternal / AllowSecret are set, so an untrusted query that references one fails as an unknown field. See Config & Trust Modes.

The recommended source of classification is a datacatalog catalog:

schema, err := guardsql.FromCatalog(catalog) // validates, then maps visibility/pii/secret
cfg := guardsql.NewUntrustedConfig(schema, guardsql.DialectPostgres)

Field.Class remains settable by hand. See Schema from a Data Catalog.

For tenant/ownership isolation (IDOR defense), Config.RowScope injects a mandatory, app-supplied predicate as a top-level AND on every compiled SELECT/UPDATE/DELETE and is stamped onto INSERTs, so no user WHERE can widen scope and scope columns cannot be reassigned; RequireRowScope makes unscoped access fail closed. See Row Scope.

Identifier Safety

Table and column names cannot be bound as parameters, so GuardSQL restricts them to the unquoted identifier charset by default. IsSafeIdentifier and Schema.ValidateIdentifiers() reject quotes, whitespace, ;, --, and non-ASCII homoglyphs before any SQL is emitted, making identifier-based injection structurally impossible. Set AllowUnsafeIdentifiers only for a database that relies on quoted identifiers.

Mutations

Trusted, application-authored mutations use a programmatic builder — no mutation text is parsed. They are deny-by-default (the operation must be allowed by policy) and require a WHERE on UPDATE/DELETE unless AllowUnfilteredMutations is set.

guardsql.Update("users").
    Set("status", "active").
    Where(guardsql.Eq("id", 42)).
    Compile(tcfg)
// UPDATE "users" SET "status" = $1 WHERE "id" = $2   args: ["active", 42]

See Mutations.

Formatting

GuardSQL includes a dependency-free formatter for canonical query display, editor cleanup, logging, reviews, and tests.

formatted, err := guardsql.Format(input, guardsql.DefaultFormatOptions())

The formatter supports multiline and single-line output:

singleLine, err := guardsql.Format(input, guardsql.FormatOptions{
    Style: guardsql.FormatSingleLine,
})

The guardsqlfmt command reads a file or stdin:

go run ./cmd/guardsqlfmt query.gql
echo 'select id,name from items limit 10' | go run ./cmd/guardsqlfmt --style=singleline
echo 'select id from items' | go run ./cmd/guardsqlfmt --highlight=ansi

The TypeScript package exports matching browser-side helpers:

import { format, highlightANSI } from '@grokify/guardsql'

format('select id,name from items limit 10')

Request Pipeline

Hosted applications should process customer queries in this order:

  1. Parse: convert text into a Query AST. Anything that does not parse is a syntax error.
  2. Validate: check the AST against an allowlisted schema of entities, fields, field types, and allowed field capabilities.
  3. Analyze: resolve sources and fields into a RequirementSet for authorization and backend planning.
  4. Policy: enforce static product policy such as allowed CRUD operations, required limits, expression complexity, maximum IN list size, and field usage.
  5. Authorize: apply tenant, user, role, and row-level authorization outside the user query, or compile those decisions into Policy.
  6. Compile: convert the validated AST into backend-specific parameterized SQL, Ent predicates, API filters, or in-memory filters.
  7. Execute: run with service-owned timeout, row limit, audit logging, and resource controls.

For analytics/reporting endpoints, configure linting with read-only operation policy:

_, issues := guardsql.Lint(input, guardsql.LintConfig{
	Schema:     schema,
	AllowedOps: []guardsql.Operation{guardsql.OperationRead},
	MaxDepth:   8,
	MaxNodes:   64,
	MaxInValues: 100,
	RequireLimit: true,
})

New code should prefer parsing once, validating, analyzing requirements, and applying both structural and resolved policy checks:

q, err := guardsql.Parse(input)
if err != nil {
    return err
}

issues := guardsql.CheckPolicy(q, guardsql.Policy{
    AllowedOps: []guardsql.Operation{guardsql.OperationRead},
    Fields: map[string]map[string]guardsql.FieldPolicy{
        "roadmap_items": {
            "name":  {Selectable: true, Filterable: true, Sortable: true},
            "score": {Selectable: true, Filterable: true, Sortable: true},
        },
    },
    MaxDepth:    8,
    MaxNodes:    80,
    MaxInValues: 100,
})
if len(issues) > 0 {
    return fmt.Errorf("query not allowed: %s", issues[0].Message)
}

analysis, err := guardsql.Analyze(q, schema)
if err != nil {
    return err
}

requirements := analysis.Requirements()
_ = requirements

policy := guardsql.SafeAnalyticsPolicy(schema)
if issues := guardsql.CheckAnalysisPolicy(analysis, policy); len(issues) > 0 {
    return fmt.Errorf("query requirements not allowed: %s", issues[0].Message)
}

Security Model

GuardSQL is never executed as SQL. Applications parse it into an AST, validate that AST against an allowlisted schema, then compile it — with the built-in compiler or a backend-specific one — into parameterized queries, Ent predicates, API filters, or in-memory filters. Tenant scope and authorization should be injected by the hosting service, not trusted from the user query.

Input that does not parse is a normal syntax error, not an injection. Injection resistance comes from never executing the original string as SQL and from only compiling validated AST nodes to parameterized backend calls. The built-in compiler additionally binds every value as a parameter and locks identifiers to a safe charset (see Identifier Safety above).

For multi-tenant SaaS services, treat the AST as the trust boundary:

  • Use per-tenant and per-role schemas so users cannot reference unauthorized entities or fields.
  • Force tenant scope outside the query instead of accepting tenant_id from user text.
  • Allow only OperationRead on analytics endpoints.
  • Keep mutations behind separate endpoints, policies, audit logs, and permission checks.
  • Compile SQL with placeholders and separate arguments, never string-concatenated user values.
  • Apply service-side query timeouts and row limits even when the query includes LIMIT.
  • Use least-privileged read-only database roles, row-level security or approved tenant-scoped views, and deny database file/network/procedure/extension privileges for customer query execution.

Attack Coverage

Strength ratings assume the compiled path (Compile / Config.Compile). Using GuardSQL only as a parse/policy gate and then executing the original text verbatim weakens the injection guarantees to whatever the parser rejects.

  • Strong — prevented by construction.
  • Partial — reduced at the language layer; close fully with host/database controls.
  • Shared — GuardSQL enforces a decision the application must make.
  • App-layer — outside GuardSQL's scope.
Attack class Strength GuardSQL posture
SQL injection — values Strong Every literal is a bind parameter; values are never concatenated into SQL.
SQL injection — identifiers Strong Table/column names must resolve to a schema field and pass IsSafeIdentifier, then are quoted per dialect.
Blind / boolean / timing injection Strong Parameterized values plus no scalar functions or arithmetic in the grammar leave no oracle to build.
Stacked queries, DDL, procedures, file/network access Strong Not representable in the AST, so the compiler cannot emit them.
Unicode / homoglyph identifier spoofing Strong IsSafeIdentifier is ASCII-only and rejects non-identifier characters.
Second-order injection App-layer Values are stored safely as parameters; if other code later concatenates a stored value into SQL, that path is outside GuardSQL.
Query-complexity denial of service Partial Policy bounds depth, node count, IN size, LIMIT, and select/join/group/CTE counts; expensive-but-legal queries still need DB timeouts, row caps, and a least-privileged role.
Column-level data exposure (secrets / PII) Strong Entity/field allowlist plus ClassSecret/ClassInternal/ClassPII stripping; denied columns read as "unknown field" without disclosing they exist.
Mass assignment via mutations Strong Assignment targets must be columns explicitly marked Writable in the schema (deny-by-default), so a caller cannot write a column such as role or is_admin unless permitted; mutations are also trusted-only and reject unknown columns.
Full-table UPDATE / DELETE Strong A WHERE clause is required by default; unfiltered mutations are refused unless explicitly allowed.
Error-based information disclosure Strong Errors are structural; denied/secret columns surface as unknown fields rather than confirming existence.
Broken object-level authorization (IDOR) Strong * The app supplies the trusted scope (session principal → row filter); GuardSQL injects it as a mandatory top-level AND on every SELECT/UPDATE/DELETE via RowScope, so no user WHERE can widen it. RequireRowScope makes unscoped access refuse to compile (fail-closed).
Multi-tenant data leakage Strong * Same mechanism: a forced tenant_id predicate is ANDed into every statement, and per-tenant schemas add column-level isolation.
Authorization bypass Shared External authz engines compile decisions into guardsql.Policy (authz.PolicyFromDecision); GuardSQL enforces the compiled decision against the AST.

* Strong when the application configures RowScope from trusted session context — GuardSQL enforces the scope but cannot know the session tenant/owner on its own. Without it, row-level isolation is not enforced.

The database account remains the final boundary for resource and RCE risk: run untrusted queries against a read-only, least-privileged identity with row-level security or approved tenant-scoped views.

Authorization Engines

The core module is intentionally dependency-light. It defines the policy shape and AST checks, but it does not import Cedar, Rego, SpiceDB, Casbin, or other authorization engines.

External authorization systems should make decisions from trusted application context and compile those decisions into guardsql.Policy. The lightweight github.com/grokify/guardsql/authz package provides a Decision helper for that conversion.

For SystemForge and SpiceDB integrations, use the optional nested module:

go get github.com/grokify/guardsql/authzsystemforge

authzsystemforge.PolicyBuilder accepts a SystemForge authz.Authorizer, a principal, a GuardSQL schema, and an optional resource mapper. It checks entities and fields through SystemForge, then produces a read-only guardsql.Policy.

Production SpiceDB deployments should provide a custom resource mapper so GuardSQL entities and fields map to concrete product resources such as analytics_dataset:<id> and analytics_field:<id>.

Dialects

AQL for Aha Studio and ORQL for OmniRoadmap can be exposed as product dialect names while using this shared package underneath.

Documentation

This repo includes a MkDocs site in docs/ (user guides in docs/guides/, contributor docs in docs/contributing/, release notes in docs/releases/).

mkdocs serve
mkdocs build --strict

TypeScript Package

The browser-side package lives in ts/ and is intended for UI feedback such as syntax errors, catalog validation, field policy warnings, and query-builder helpers. The Go parser and backend policy checks remain authoritative.

cd ts
npm test

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Format

func Format(input string, opts FormatOptions) (string, error)

Format parses input and returns canonical GuardSQL text.

func FormatQuery

func FormatQuery(q *Query, opts FormatOptions) string

FormatQuery formats a parsed GuardSQL query.

func HighlightANSIText

func HighlightANSIText(input string) string

HighlightANSIText applies terminal ANSI colors to GuardSQL keywords and literals. It is best-effort and intended for display, not parsing.

func IsSafeIdentifier added in v0.3.0

func IsSafeIdentifier(s string) bool

IsSafeIdentifier reports whether s is a valid unquoted SQL identifier: an ASCII letter or underscore, followed by ASCII letters, digits, or underscores, up to 128 bytes.

Identifiers (table and column names) cannot be bound as query parameters — most drivers only parameterize values — so they must be emitted into SQL text. Restricting them to this charset (and rejecting quotes, whitespace, and non-ASCII homoglyphs) makes identifier-based SQL injection structurally impossible, independent of quoting. Databases whose schema uses only unquoted-style identifiers can therefore be locked down completely.

func Lint

func Lint(input string, cfg LintConfig) (*Query, []LintIssue)

Lint parses and validates a query, then applies static SaaS safety checks.

func OutputName

func OutputName(item SelectItem) string

OutputName returns the result column name for a SELECT item.

func Validate

func Validate(q *Query, schema Schema) error

Validate checks a query against a schema allowlist.

Types

type AggregateExpr

type AggregateExpr struct {
	Func  AggregateFunc
	Field string
	Star  bool
}

AggregateExpr is an aggregate function call in SELECT.

type AggregateFunc

type AggregateFunc string

AggregateFunc identifies a supported aggregate function.

const (
	AggCount AggregateFunc = "COUNT"
	AggSum   AggregateFunc = "SUM"
	AggAvg   AggregateFunc = "AVG"
	AggMin   AggregateFunc = "MIN"
	AggMax   AggregateFunc = "MAX"
)

type Analysis

type Analysis struct {
	Operation     Operation
	Sources       []ResolvedSource
	Fields        []FieldRequirement
	Joins         []JoinRequirement
	Functions     []AggregateFunc
	CTEs          []Analysis
	NestedSources []Analysis
	HasAggregate  bool
}

Analysis is the resolved, read-only summary of a parsed query.

func Analyze

func Analyze(q *Query, schema Schema) (Analysis, error)

Analyze resolves a parsed query against a schema and returns the resources, fields, functions, and joins the query requires. It does not mutate the query.

func (Analysis) Requirements

func (a Analysis) Requirements() RequirementSet

Requirements returns a de-duplicated requirement set for authorization and policy adapters.

type Assignment added in v0.3.0

type Assignment struct {
	Column string
	Value  any
}

Assignment sets a column to a value in an INSERT or UPDATE.

type CTE

type CTE struct {
	Name  string
	Query *Query
}

CTE is a named common table expression.

type CatalogOption added in v0.3.0

type CatalogOption func(*catalogOptions)

CatalogOption customizes FromCatalog.

func WithNonFilterable added in v0.3.0

func WithNonFilterable(entity string, fields ...string) CatalogOption

WithNonFilterable marks the named fields of an entity as not Filterable, so they cannot appear in WHERE. FromCatalog returns an error if the entity or a field is not in the catalog.

func WithWritable added in v0.3.0

func WithWritable(entity string, fields ...string) CatalogOption

WithWritable marks the named fields of an entity as Writable, making them valid INSERT/UPDATE assignment targets. Fields are read-only by default. FromCatalog returns an error if the entity or a field is not in the catalog.

type CompareExpr

type CompareExpr struct {
	Field  string
	Op     CompareOp
	Values []Value
}

CompareExpr compares one field to one or more literal values.

type CompareOp

type CompareOp string

CompareOp is a field comparison operator.

const (
	OpEq       CompareOp = "="
	OpNotEq    CompareOp = "!="
	OpLT       CompareOp = "<"
	OpLTE      CompareOp = "<="
	OpGT       CompareOp = ">"
	OpGTE      CompareOp = ">="
	OpIn       CompareOp = "IN"
	OpContains CompareOp = "CONTAINS"
	OpIsNull   CompareOp = "IS NULL"
	OpNotNull  CompareOp = "IS NOT NULL"
)

type CompileOptions added in v0.3.0

type CompileOptions struct {
	// Dialect selects the target SQL dialect. Required.
	Dialect Dialect
	// Schema resolves and re-quotes every identifier, and identifies JSON
	// columns. Required: the compiler emits only identifiers present in the
	// schema, so an unknown field is an error rather than passthrough.
	Schema Schema
	// ScopeSchema, when set, is the schema used to resolve the columns of the
	// RowScope predicate. It lets a scope reference a column that Schema hides
	// from the caller (for example an internal-class tenant_id). It defaults to
	// Schema. Only the scope predicate resolves against it; the user's query
	// never does. Config sets it to the full, unstripped schema.
	ScopeSchema Schema
	// StrictIdentifiers, when true, requires every schema identifier to be a
	// safe unquoted identifier (see IsSafeIdentifier) before any SQL is emitted.
	// Because identifiers cannot be parameterized, this makes identifier-based
	// injection structurally impossible. Config sets this by default.
	StrictIdentifiers bool
	// RowScope, when set, returns a mandatory scope predicate for the queried
	// entity (derived from trusted session context, e.g. tenant_id = <session>).
	// The predicate is ANDed at the top level of the compiled WHERE, so no
	// user-supplied filter can widen it. Returning (nil, nil) explicitly exempts
	// an entity; returning an error refuses compilation. See ScopeFunc.
	RowScope ScopeFunc
	// RequireRowScope, when true, refuses to compile unless RowScope is set,
	// making unscoped access impossible (fail-closed). RowScope may still exempt
	// individual entities by returning (nil, nil).
	RequireRowScope bool
}

CompileOptions configures compilation.

type CompiledSQL added in v0.3.0

type CompiledSQL struct {
	SQL  string
	Args []any
}

CompiledSQL is parameterized SQL emitted from a validated AST. The SQL string never contains inlined user literals: every value is a bind parameter in Args, in positional order.

func Compile added in v0.3.0

func Compile(q *Query, opts CompileOptions) (CompiledSQL, error)

Compile transpiles a validated read query into parameterized SQL for the target dialect. It regenerates SQL from the AST rather than passing user text through: only constructs the AST can represent and the emitter supports are produced, and every literal becomes a bind parameter. The query should have passed Validate and CheckPolicy first; Compile independently re-resolves all identifiers against the schema as defense in depth.

Joins, CTEs, and nested query sources are not yet supported.

type Config added in v0.3.0

type Config struct {
	// Schema describes the entities and fields, including each field's Class.
	Schema Schema `json:"schema"`
	// Dialect is the target SQL dialect for Compile.
	Dialect Dialect `json:"dialect,omitempty"`
	// AllowPII permits querying fields classed ClassPII. Default false.
	AllowPII bool `json:"allowPII,omitempty"`
	// AllowInternal permits querying fields classed ClassInternal (platform
	// data not meant for customers). Default false.
	AllowInternal bool `json:"allowInternal,omitempty"`
	// AllowSecret permits querying fields classed ClassSecret. Reserve this for
	// trusted, application-authored SQL; never enable it for customer-supplied
	// SQL. Default false.
	AllowSecret bool `json:"allowSecret,omitempty"`
	// AllowUnsafeIdentifiers disables the default requirement that every entity
	// and field identifier match the unquoted SQL identifier charset. Leave
	// false (the default) to lock identifiers down; set true only for a database
	// that genuinely relies on quoted identifiers.
	AllowUnsafeIdentifiers bool `json:"allowUnsafeIdentifiers,omitempty"`
	// AllowUnfilteredMutations permits UPDATE and DELETE without a WHERE clause
	// (a full-table write). Default false: unfiltered mutations are rejected
	// even for trusted callers, since they are almost always a mistake.
	AllowUnfilteredMutations bool `json:"allowUnfilteredMutations,omitempty"`
	// RowScope enforces tenant/ownership isolation (IDOR defense): it returns a
	// mandatory predicate for each queried entity, derived from trusted session
	// context, that is ANDed into every compiled SELECT/UPDATE/DELETE. It is
	// per-request and not serialized. See ScopeFunc and ScopeEquals.
	RowScope ScopeFunc `json:"-"`
	// RequireRowScope refuses to compile unless RowScope is set (fail-closed).
	RequireRowScope bool `json:"-"`
	// Policy holds optional additional AST-level limits (depth, node count,
	// required LIMIT, allowed operations, and so on) applied during Check and
	// used to authorize mutations.
	Policy Policy `json:"-"`
}

Config is a declarative description of a queryable database together with the rules for safely querying it: the schema (entities, fields, types, and per-field data classification), the target SQL dialect, and safety defaults.

It is the recommended high-level entry point; the lower-level Parse, Validate, CheckPolicy, and Compile primitives remain available. Config applies secure defaults: secret columns are never queryable, internal columns are queryable only when AllowInternal is set, PII columns only when AllowPII is set, and every identifier must be a safe unquoted identifier unless AllowUnsafeIdentifiers is set.

Config is JSON-serializable so an application can load "what the database is and how it may be queried" from a file.

func NewTrustedConfig added in v0.3.0

func NewTrustedConfig(schema Schema, dialect Dialect) Config

NewTrustedConfig returns a Config for internal, application-authored SQL: it may read and mutate, and may access secret, internal and PII columns. Identifier lockdown and value parameterization still apply, and mutation targets must still be columns marked Writable — trust removes access restrictions, not the safety of regenerating parameterized SQL from the validated AST or the write-allowlist. Never use this profile for SQL that originates outside the application.

func NewUntrustedConfig added in v0.3.0

func NewUntrustedConfig(schema Schema, dialect Dialect) Config

NewUntrustedConfig returns a Config hardened for third-party or customer-supplied SQL (the Metabase-style case): read-only, secret and PII columns stripped from the schema, identifier lockdown on, and a required LIMIT. Tighten further via the returned Config's Policy (MaxLimit, MaxDepth, and so on).

func (Config) Check added in v0.3.0

func (c Config) Check(input string) (*Query, error)

Check parses input, enforces identifier safety (unless AllowUnsafeIdentifiers), validates it against the classification-filtered schema, and applies Policy. It returns the validated AST, ready for Compile.

func (Config) Compile added in v0.3.0

func (c Config) Compile(input string) (CompiledSQL, error)

Compile parses, checks, and compiles input to parameterized SQL for the configured dialect in one step. It applies the same classification and identifier-safety rules as Check.

type DataClass added in v0.3.0

type DataClass string

DataClass classifies the sensitivity of a field.

const (
	// ClassPublic is ordinary, queryable data (the zero value).
	ClassPublic DataClass = ""
	// ClassPII is personal data, queryable only when Config.AllowPII is set.
	ClassPII DataClass = "pii"
	// ClassInternal is platform-only data that customer-facing queries must
	// not see. It is queryable only when Config.AllowInternal is set.
	ClassInternal DataClass = "internal"
	// ClassSecret is a credential/secret column that is never queryable.
	ClassSecret DataClass = "secret" //nolint:gosec // G101: classification label, not a credential
)

Data classifications.

type DeleteBuilder added in v0.3.0

type DeleteBuilder struct {
	// contains filtered or unexported fields
}

DeleteBuilder builds a parameterized DELETE. Construct with Delete.

func Delete added in v0.3.0

func Delete(table string) *DeleteBuilder

Delete starts a DELETE from table.

func (*DeleteBuilder) Compile added in v0.3.0

func (b *DeleteBuilder) Compile(cfg Config) (CompiledSQL, error)

Compile validates and compiles the DELETE to parameterized SQL.

func (*DeleteBuilder) Where added in v0.3.0

func (b *DeleteBuilder) Where(expr Expr) *DeleteBuilder

Where sets the filter. Required unless Config.AllowUnfilteredMutations is set.

type Dialect added in v0.3.0

type Dialect string

Dialect identifies a target SQL dialect for compilation.

const (
	DialectSQLite   Dialect = "sqlite"
	DialectPostgres Dialect = "postgres"
	DialectMySQL    Dialect = "mysql"
)

Supported compilation dialects.

type Entity

type Entity struct {
	Name   string           `json:"name"`
	Fields map[string]Field `json:"fields"`
}

Entity describes a queryable object type.

func (Entity) NormalizeName

func (e Entity) NormalizeName(fallback string) Entity

func (Entity) WithName

func (e Entity) WithName(name string) Entity

type Expr

type Expr interface {
	// contains filtered or unexported methods
}

Expr is implemented by all filter expression nodes.

func And added in v0.3.0

func And(exprs ...Expr) Expr

And combines expressions with AND. Nil expressions are ignored; And of none returns nil.

func Eq added in v0.3.0

func Eq(field string, value any) Expr

Eq builds field = value.

func Gt added in v0.3.0

func Gt(field string, value any) Expr

Gt builds field > value.

func Gte added in v0.3.0

func Gte(field string, value any) Expr

Gte builds field >= value.

func In added in v0.3.0

func In(field string, values ...any) Expr

In builds field IN (values...).

func IsNotNull added in v0.3.0

func IsNotNull(field string) Expr

IsNotNull builds field IS NOT NULL.

func IsNull added in v0.3.0

func IsNull(field string) Expr

IsNull builds field IS NULL.

func Lt added in v0.3.0

func Lt(field string, value any) Expr

Lt builds field < value.

func Lte added in v0.3.0

func Lte(field string, value any) Expr

Lte builds field <= value.

func Ne added in v0.3.0

func Ne(field string, value any) Expr

Ne builds field != value.

func Not added in v0.3.0

func Not(expr Expr) Expr

Not negates an expression.

func Or added in v0.3.0

func Or(exprs ...Expr) Expr

Or combines expressions with OR. Nil expressions are ignored; Or of none returns nil.

type Field

type Field struct {
	Name       string    `json:"name"`
	Type       FieldType `json:"type"`
	Selectable bool      `json:"selectable"`
	Filterable bool      `json:"filterable"`
	Sortable   bool      `json:"sortable"`
	// Writable governs whether the field may be a target of an INSERT or UPDATE
	// assignment. Like the read capabilities, it is deny-by-default: a field is
	// writable only when explicitly marked, so mutation targets form an explicit
	// allowlist. This is the mass-assignment defense — a caller cannot write a
	// column (for example role or is_admin) unless the schema permits it.
	Writable bool `json:"writable,omitempty"`
	// Class is the field's data classification. Secret fields are never
	// queryable by untrusted callers, internal fields are queryable only when
	// a Config sets AllowInternal, and PII fields only when it sets AllowPII.
	// The zero value (ClassPublic) is ordinary, queryable data. A field has a
	// single Class; when several sensitivities apply, choose the most
	// restrictive: secret, then internal, then PII.
	Class DataClass `json:"class,omitempty"`
}

Field describes a queryable/projectable field.

type FieldPolicy

type FieldPolicy struct {
	Selectable     bool
	Filterable     bool
	Sortable       bool
	Groupable      bool
	Aggregatable   bool
	AggregateFuncs []AggregateFunc
	Joinable       bool
}

FieldPolicy controls how one field may be used in a query.

type FieldRef added in v0.3.0

type FieldRef struct {
	Field    Field
	Name     string
	JSONPath []string
}

FieldRef is a field name resolved against a schema entity. For a plain column JSONPath is empty; for a dotted reference into a FieldJSON column (e.g. payload.finding_info.title) Name is the base column ("payload") and JSONPath is the extraction path (["finding_info","title"]).

func (FieldRef) IsJSONPath added in v0.3.0

func (r FieldRef) IsJSONPath() bool

IsJSONPath reports whether the reference extracts a path from a JSON column.

func (FieldRef) Key added in v0.3.0

func (r FieldRef) Key() string

Key returns the canonical identity of the reference, used to compare SELECT/GROUP BY/ORDER BY references. Two references are the same field iff their keys match, so distinct JSON paths into the same column are distinct.

type FieldRequirement

type FieldRequirement struct {
	Entity     string
	Field      string
	Source     string
	Usage      FieldUsage
	Aggregate  AggregateFunc
	OutputName string
}

FieldRequirement describes a concrete field operation in a query.

type FieldType

type FieldType string

FieldType is a coarse type used by validators and backends.

const (
	FieldString FieldType = "string"
	FieldNumber FieldType = "number"
	FieldBool   FieldType = "bool"
	FieldTime   FieldType = "time"
	// FieldJSON is a JSON/JSONB document column. A JSON field may be addressed
	// with a dotted path (e.g. payload.finding_info.title); the compiler emits
	// dialect-specific JSON extraction for the path. Any path under a
	// selectable JSON field is permitted.
	FieldJSON FieldType = "json"
)

type FieldUsage

type FieldUsage string

FieldUsage describes how a query uses a field.

const (
	FieldUsageSelect    FieldUsage = "select"
	FieldUsageFilter    FieldUsage = "filter"
	FieldUsageSort      FieldUsage = "sort"
	FieldUsageGroup     FieldUsage = "group"
	FieldUsageAggregate FieldUsage = "aggregate"
	FieldUsageJoin      FieldUsage = "join"
	FieldUsageHaving    FieldUsage = "having"
)

type FormatOptions

type FormatOptions struct {
	Style     FormatStyle
	Indent    string
	Highlight HighlightStyle
}

FormatOptions configures GuardSQL formatting.

func DefaultFormatOptions

func DefaultFormatOptions() FormatOptions

DefaultFormatOptions returns conservative multiline formatting options.

type FormatStyle

type FormatStyle string

FormatStyle controls whether a query is emitted on one line or as a human-readable multiline statement.

const (
	FormatMultiline  FormatStyle = "multiline"
	FormatSingleLine FormatStyle = "singleline"
)

type HighlightStyle

type HighlightStyle string

HighlightStyle controls optional syntax highlighting.

const (
	HighlightNone HighlightStyle = "none"
	HighlightANSI HighlightStyle = "ansi"
)

type InsertBuilder added in v0.3.0

type InsertBuilder struct {
	// contains filtered or unexported fields
}

InsertBuilder builds a parameterized INSERT. Construct with Insert.

func Insert added in v0.3.0

func Insert(table string) *InsertBuilder

Insert starts an INSERT into table.

func (*InsertBuilder) Compile added in v0.3.0

func (b *InsertBuilder) Compile(cfg Config) (CompiledSQL, error)

Compile validates and compiles the INSERT to parameterized SQL.

func (*InsertBuilder) Set added in v0.3.0

func (b *InsertBuilder) Set(column string, value any) *InsertBuilder

Set adds a column assignment.

type Join

type Join struct {
	Type      JoinType
	Entity    string
	Query     *Query
	Alias     string
	Condition Expr
}

Join represents a relational join against an entity or nested query.

type JoinRequirement

type JoinRequirement struct {
	Type        JoinType
	LeftEntity  string
	RightEntity string
	RightSource string
}

JoinRequirement describes a source-to-source join used by a query.

type JoinType

type JoinType string

JoinType identifies the join variant.

const (
	JoinInner JoinType = "inner"
	JoinLeft  JoinType = "left"
	JoinRight JoinType = "right"
	JoinFull  JoinType = "full"
	JoinCross JoinType = "cross"
)

type LintConfig

type LintConfig struct {
	Schema       Schema
	AllowedOps   []Operation
	MaxDepth     int
	MaxNodes     int
	MaxInValues  int
	RequireLimit bool
}

LintConfig defines static policy checks for user-authored GuardSQL.

type LintIssue

type LintIssue struct {
	Message string
}

LintIssue is a syntax, schema, or policy problem found in a query.

type LogicalExpr

type LogicalExpr struct {
	Op          LogicalOp
	Left, Right Expr
}

LogicalExpr combines two expressions with AND or OR.

type LogicalOp

type LogicalOp string

LogicalOp is a boolean operator.

const (
	LogicalAnd LogicalOp = "AND"
	LogicalOr  LogicalOp = "OR"
)

type NotExpr

type NotExpr struct {
	Expr Expr
}

NotExpr negates an expression.

type Operation

type Operation string

Operation identifies the CRUD operation represented by a query.

const (
	OperationRead   Operation = "read"
	OperationCreate Operation = "create"
	OperationUpdate Operation = "update"
	OperationDelete Operation = "delete"
)

type Order

type Order struct {
	Field string
	Desc  bool
}

Order defines a sort field and direction.

type ParseError

type ParseError struct {
	Message string
	Offset  int
}

ParseError is returned for invalid syntax.

func (*ParseError) Error

func (e *ParseError) Error() string

type Policy

type Policy struct {
	AllowedOps      []Operation
	AllowedEntities []string
	// StrictAllowlists makes AllowedEntities and Fields strict allowlists
	// even when empty: an entity not in AllowedEntities is refused, and an
	// entity with no entry in Fields has no usable fields.
	StrictAllowlists   bool
	AllowedJoinTypes   []JoinType
	AllowedFunctions   []AggregateFunc
	Fields             map[string]map[string]FieldPolicy
	AllowStar          bool
	AllowCTEs          bool
	AllowNestedSources bool
	RequireLimit       bool
	MaxLimit           int
	MaxDepth           int
	MaxNodes           int
	MaxInValues        int
	MaxSelectItems     int
	MaxOrderFields     int
	MaxGroupFields     int
	MaxJoins           int
	MaxCTEs            int
	MaxSubqueryDepth   int
}

Policy defines AST-level safety rules for a GuardSQL query. It is intended for structural query controls; application authorization systems can compile their decisions into this policy shape before execution.

By default an empty AllowedEntities or Fields adds no restriction, so a zero Policy only applies the checks that are set. An authorization result must not be read that way: set StrictAllowlists (authz.PolicyFromDecision does) so that nothing allowed means nothing is allowed.

func SafeAnalyticsPolicy

func SafeAnalyticsPolicy(schema Schema) Policy

SafeAnalyticsPolicy returns a deny-by-default read-only policy derived from a schema. Host applications should tighten limits and authorization further for each deployment.

type PolicyIssue

type PolicyIssue struct {
	Message string
}

PolicyIssue is a policy violation found in a parsed query.

func CheckAnalysisPolicy

func CheckAnalysisPolicy(analysis Analysis, policy Policy) []PolicyIssue

CheckAnalysisPolicy applies policy to resolved analysis requirements. It is preferred for operational authorization because aliases, CTEs, and nested sources have already been resolved into concrete resource and field usage.

func CheckPolicy

func CheckPolicy(q *Query, policy Policy) []PolicyIssue

CheckPolicy applies AST-level policy checks to a parsed GuardSQL query. It does not mutate the query.

type Query

type Query struct {
	Operation  Operation
	With       []CTE
	Select     []SelectItem
	From       string
	FromQuery  *Query
	FromAlias  string
	Joins      []Join
	Where      Expr
	GroupBy    []string
	Having     Expr
	Prioritize []string
	OrderBy    []Order
	Limit      int
}

Query is the parsed representation of a GuardSQL query.

func Parse

func Parse(input string) (*Query, error)

Parse parses a read-only GuardSQL statement into an AST.

type RequirementSet

type RequirementSet struct {
	Operations []Operation
	Entities   []string
	Fields     []FieldRequirement
	Joins      []JoinRequirement
	Functions  []AggregateFunc
}

RequirementSet is a compact authorization and policy input derived from Analysis.

type ResolvedSource

type ResolvedSource struct {
	Name     string
	Alias    string
	Entity   string
	Nested   bool
	CTE      bool
	JoinType JoinType
}

ResolvedSource identifies a query source after schema and CTE resolution.

type Row

type Row map[string]any

Row is a generic record used by the in-memory evaluator and tests.

func Eval

func Eval(q *Query, rows []Row) ([]Row, error)

Eval filters, sorts, limits, and projects rows using a validated query.

type Schema

type Schema struct {
	Entities map[string]Entity `json:"entities"`
	MaxLimit int               `json:"maxLimit,omitempty"`
}

Schema describes the entities and fields a user is allowed to query.

func FromCatalog added in v0.3.0

func FromCatalog(c *datacatalog.Catalog, opts ...CatalogOption) (Schema, error)

FromCatalog builds a Schema from a datacatalog Catalog, mapping the catalog's classification onto Field.Class so that Config.effectiveSchema blocks exactly what datacatalog's AllowedInQuery blocks.

The catalog is validated first and its validation error is returned unchanged, so an unclassified field can never enter a schema. Every field is Selectable, Filterable, and Sortable and not Writable unless an option says otherwise (see WithWritable and WithNonFilterable).

A guardsql field has a single Class, so when several datacatalog attributes apply the most restrictive for query purposes wins: secret, then internal visibility, then PII, then public. Use AllowSecret, AllowInternal, and AllowPII on Config to admit each class (NewUntrustedConfig admits none, NewTrustedConfig admits all).

Entity and field names are lower-cased as by Schema.Normalize; two names that differ only in case are an error.

func (Schema) Normalize

func (s Schema) Normalize() Schema

Normalize returns a copy with lower-case entity and field map keys.

func (Schema) ValidateIdentifiers added in v0.3.0

func (s Schema) ValidateIdentifiers() error

ValidateIdentifiers reports an error if any entity or field name is not a safe unquoted SQL identifier (see IsSafeIdentifier). Because identifiers are emitted into SQL text rather than bound as parameters, enforcing this over the whole schema removes identifier-based injection as a possibility.

type ScopeFunc added in v0.3.0

type ScopeFunc func(entity string) (Expr, error)

ScopeFunc returns a mandatory row-scope predicate for a queried entity. It is how an application enforces tenant/ownership isolation (IDOR defense) at the GuardSQL layer: the predicate is built from trusted session context and ANDed into every compiled statement so a user's own filter cannot widen scope.

Return an Expr (typically built with Eq/In/And) to require it, (nil, nil) to exempt the entity, or an error to refuse compilation.

func ScopeEquals added in v0.3.0

func ScopeEquals(column string, value any) ScopeFunc

ScopeEquals returns a ScopeFunc that requires column = value on every queried entity, using value from trusted session context (e.g. the tenant ID). If a queried entity has no such column, compilation fails (fail-closed), so a missing scope column can never silently return unscoped data. For per-entity exemptions or multiple scope columns, write a ScopeFunc directly.

type SelectItem

type SelectItem struct {
	Field     string
	Star      bool
	Aggregate *AggregateExpr
	Alias     string
}

SelectItem is a projected field. Star is true for SELECT *.

type UpdateBuilder added in v0.3.0

type UpdateBuilder struct {
	// contains filtered or unexported fields
}

UpdateBuilder builds a parameterized UPDATE. Construct with Update.

func Update added in v0.3.0

func Update(table string) *UpdateBuilder

Update starts an UPDATE of table.

func (*UpdateBuilder) Compile added in v0.3.0

func (b *UpdateBuilder) Compile(cfg Config) (CompiledSQL, error)

Compile validates and compiles the UPDATE to parameterized SQL.

func (*UpdateBuilder) Set added in v0.3.0

func (b *UpdateBuilder) Set(column string, value any) *UpdateBuilder

Set adds a column assignment.

func (*UpdateBuilder) Where added in v0.3.0

func (b *UpdateBuilder) Where(expr Expr) *UpdateBuilder

Where sets the filter. Required unless Config.AllowUnfilteredMutations is set. Build conditions with Eq, In, And, Or, Not, and the comparison helpers.

type Value

type Value struct {
	Type ValueType
	Raw  string
	Any  any
}

Value is a typed query literal.

type ValueType

type ValueType string

ValueType is the literal kind.

const (
	ValueString ValueType = "string"
	ValueNumber ValueType = "number"
	ValueBool   ValueType = "bool"
	ValueNull   ValueType = "null"
)

Directories

Path Synopsis
cmd
guardsqlfmt command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL