guardsql

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: MIT Imports: 5 Imported by: 0

README

GuardSQL

Go CI Go Lint Go SAST 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, lightweight authorization adapters, and backend compiler contracts. Runtime integrations for other language stacks should live in separate repositories or modules. A Hibernate integration, for example, should belong in a Java-focused project such as github.com/grokify/guardsql-java or github.com/grokify/guardsql-hibernate, not in this root Go module.

The core contract is a parser that converts user-authored text into Go structs. Applications should pass the parsed AST through schema validation, static lint policy, and backend-specific compilers. The original text should not be executed directly.

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.

Future parsers may support create, update, or delete operations. Those operations are represented explicitly in the AST and must be allowed by policy before execution.

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 should not be executed as SQL. Applications parse it into an AST, validate that AST against an allowlisted schema, then compile it into backend-specific 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.

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.

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/.

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 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 CTE

type CTE struct {
	Name  string
	Query *Query
}

CTE is a named common table expression.

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 Entity

type Entity struct {
	Name   string
	Fields map[string]Field
}

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.

type Field

type Field struct {
	Name       string
	Type       FieldType
	Selectable bool
	Filterable bool
	Sortable   bool
}

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 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"
)

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 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
	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.

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
	MaxLimit int
}

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

func (Schema) Normalize

func (s Schema) Normalize() Schema

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

type SelectItem

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

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

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