Type safety
Explains how to type-check SqlFun data access modules with unit tests and handle composite query testing with FsCheck.
What this file does
Explains how to type-check SqlFun data access modules with unit tests and handle composite query testing with FsCheck.
When to use it
- You use SqlFun and want to catch SQL typos or type mismatches early
- You need to test composite queries that are not checked during module initialization
- You want a full error report instead of stopping at the first compilation error
- You are looking for a pattern to generate test data for database queries
Assumes this stack
Type safety
Simple queries
SqlFun relies on hand-written SQL and runtime code generation. It's not type-safe in a usual meaning. But there is quite nice equivalent. Since all query functions are generated during their module initialization, we need only one test for each data access module. E.g. to type-check the module:
module Blogging =
let getBlog: int -> Blog AsyncDb = ...
let getNumberOfPosts: int -> int AsyncDb = ...
let getPostAndItsComments: int -> (Post * Comment list) AsyncDb = ...
the followin test is enough:
[<Test>]
member this.``Blogging module passes type checks``() =
let x = Blogging.getBlog
Accessing one module member triggers initialization of remaining members. During code generation SqlFun executes query in SchemaOnly mode and tries to generate all needed type conversions. Typos in SQL, incorrect parameters or return types result in TypeInitializationException.
Unfortunately, the information about failing function is somewhere in the stack trace of the inner exception. To make it easier to find, the code accessing module can be wrapped in the testQueries function:
[<Test>]
member this.``Blogging module passes type checks``() =
Testing.testQueries <| fun () -> Blogging.getBlog
The downside of this technique is, that null checks cannot be performed this way.
Error reports
Another downside of this approach is, that we cannot obtain full error report, since code generation breaks after first error.
To improve it, SqlFun has logCompilationErrors function, that intercepts code generation exceptions and writes them to mutable variable.
We can use it by configuring sql function differently.
let compilationErrors = ref []
let sql command = Diagnostics.logCompilationErrors compilationErrors sql command
The test can use it to launch code generation as well, as to obtain error report:
[<Test>]
member this.``Blogging module passes type checks``() =
let report = Diagnostics.buildReport Blogging.compilationErrors
Assert.IsEmpty(report, report)
Composite queries
Unfortunately, composite queries are not checked during module initialization, since they must be defined as functions, not variables. Each of them should have its own test, sometimes even more, than one. My recommendation is to use FsCheck for testing them:
type Arbs =
static member strings() =
Arb.filter ((<>) null) <| Arb.Default.String()
[<Test>]
member this.``Composie queries can be tested with FsCheck``() =
let property criteria ordering =
buildQuery
|> filterPosts criteria
|> sortPostsBy (ordering |> List.distinctBy fst) // FsCheck generates duplicates
|> selectPosts
|> runAsync
|> Async.RunSynchronously
|> ignore
let cfg = { Config.QuickThrowOnFailure with Arbitrary = [ typeof<Arbs> ] }
Check.One(cfg, property)
The example above uses custom generator, since FsCheck produces nulls by default. When testing with FsCheck, the best way to define criteria is record with optional fields:
type PostCriteria = {
TitleContains: string option
ContentContains: string option
AuthorIs: string option
HasTag: string option
HasOneOfTags: string list
HasAllTags: string list
CreatedAfter: DateTime option
CreatedBefore: DateTime option
}
It's good for application logic as well. You can, for example define criteria on the client and pass them through the network without any intermediate structures.
What's inside
3 sections covering simple query testing, error reporting, and composite query testing with 4 code examples
Change this for your project
- Replace
Bloggingmodule name with your own data access module - Replace
PostCriteriarecord fields with your own query criteria - Replace
filterPosts,sortPostsBy,selectPostswith your own query functions
Where it goes
Load as policy context for the agent, or keep beside the code enforcing the rules.
Worth borrowing
- Using a single test per module to trigger code generation and catch type errors
- Collecting compilation errors into a mutable list for a full report
- Testing composite queries with property-based testing using optional record fields for criteria
Related Documents
DeepSeek R1: Case Study in Failed Extrinsic Alignment
Compiles public security research and independent findings to argue that extrinsic alignment methods are insufficient for AI safety, using DeepSeek R1 as a case study.
Guardrails, Safety & Content Filtering
Implements a layered guardrail system with input validation, output validation, and content filtering to protect LLM applications from prompt injection, jailbreaks, and data leaks.
My Hyprland Rice Setup - Context for Claude Code
Documents a Hyprland rice setup with system specs, aesthetic preferences, workflow safety practices, and a list of current issues to solve.
AI Safety & Guardrails for Voice Assistants
Defines a multi-layer safety architecture for voice assistants, covering input filtering, deterministic FAQ routing, RAG-grounded AI responses, and output guardrails.