Skip to main content

Query Analysis Reference

This page covers the internals, database support, and API surface of Query Analysis.

What Gets Analyzed

Query Operations

// Full query with parameters and result codec
QueryAnalyzer.analyze(fragment.query(rowCodec.all()), conn).getFirst();

// Named query
QueryAnalyzer.analyze("findUsers", fragment.query(rowCodec.all()), conn).getFirst();

// Update-returning operations
QueryAnalyzer.analyze(fragment.updateReturning(rowCodec), conn).getFirst();

Update Operations (Parameters Only)

// Updates have no result columns, only parameters
QueryAnalyzer.analyze(fragment.update(), conn).getFirst();

// Named update
QueryAnalyzer.analyze("updateUser", fragment.update(), conn).getFirst();

Low-Level Analysis

// Analyze a fragment + codec directly
QueryAnalyzer.analyzeFragmentAndParser(fragment, resultSetParser, conn);

How It Works

  1. Extract declared types — The Fragment knows the DbType of each parameter. The RowCodec knows the DbType of each column.

  2. Prepare the statement — We call connection.prepareStatement(sql) to get JDBC metadata.

  3. Extract vendor type names — ParameterMetaData and ResultSetMetaData provide vendor-specific type names (e.g., int4, varchar, timestamptz).

  4. Normalize and compare — Type names are normalized (lowercased, precision stripped) and compared against the declared type's vendor type names. For example, VARCHAR(255) and VARCHAR both normalize to varchar.

  5. Report errors — Any mismatches become detailed error messages explaining exactly what's wrong and how to fix it.

Database Support

Query Analysis works with all supported databases, with some caveats:

DatabaseParameter MetadataColumn MetadataNullability
PostgreSQLFullFullReliable
DuckDBLimitedFullAll nullable*
OracleFullFullReliable
SQL ServerFullFullReliable
MariaDB/MySQLLimited**FullReliable
DB2FullFullReliable

* DuckDB reports all columns as nullable; nullability checks are skipped.

** MariaDB/MySQL parameter metadata is not always reliable; parameter type checks may be skipped.

Tips

Use Meaningful Test Data

Analysis only checks types, not data. You don't need real data in your tables — just the schema:

conn.createStatement().execute("""
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
created_at TIMESTAMPTZ DEFAULT now()
)
""");

Check During Development

Run query analysis as you develop, not just in CI. Catch errors early:

// Add a quick check in your main during development
public static void main(String[] args) {
try (var conn = getConnection()) {
var analysis = QueryAnalyzer.analyze(myQuery, conn).getFirst();
System.out.println(analysis.report());
}
}

Analysis is Cheap

Preparing a statement and reading metadata is fast — milliseconds per query. You can check hundreds of queries in a single test.

API Reference

QueryAnalyzer

// Analyze a query operation
static <T> List<QueryAnalysis> analyze(Operation.Query<T> query, Connection conn)

// Analyze a named query operation
static <T> List<QueryAnalysis> analyze(String name, Operation.Query<T> query, Connection conn)

// Analyze an update-returning operation
static <T> List<QueryAnalysis> analyze(Operation.UpdateReturning<T> op, Connection conn)
static <T> List<QueryAnalysis> analyze(String name, Operation.UpdateReturning<T> op, Connection conn)

// Analyze an update operation (parameters only)
static List<QueryAnalysis> analyze(Operation.Update update, Connection conn)
static List<QueryAnalysis> analyze(String name, Operation.Update update, Connection conn)

// Analyze all SQL in a composed operation tree
static List<QueryAnalysis> analyze(Operation<?> operation, Connection conn)

// Low-level: analyze fragment + parser directly
static QueryAnalysis analyzeFragmentAndParser(
Fragment fragment,
ResultSetParser<?> parser,
Connection conn)

QueryAnalysis

// Did the analysis pass?
boolean succeeded()

// Get all errors
List<AlignmentError> allErrors()
List<AlignmentError> parameterErrors()
List<AlignmentError> columnErrors()

// Generate human-readable report
String report() // plain text
String reportColored() // with ANSI color codes

// Access raw alignment data
List<Alignment<DbType<?>, JdbcMeta.ParameterMeta>> parameterAlignment()
List<Alignment<DbType<?>, JdbcMeta.ColumnMeta>> columnAlignment()

QueryChecker (Test Interface)

interface QueryChecker {
Transactor transactor();

// Check any operation
void check(Operation<?> op)
void check(String name, Operation<?> op)

// Check fragments with codecs
void check(Fragment fragment, ResultSetParser<?> parser)
void check(Fragment fragment, RowCodec<?> codec)

// Batch check — returns report with per-query results
CheckReport checkAll(Analyzable... analyzables)

// Routine analysis
void checkRoutine(Procedure<?> procedure)
}

AnalysisOptions

// Escape hatches on any DbType
PgTypes.text.unchecked() // skip all type checking for this column/parameter
PgTypes.text.nullableOk() // suppress nullability mismatch warnings

AnalyzableScanner

// Scan a package and return all discovered analyzables
static List<Analyzable> scan(String packageName)
static List<Analyzable> scan(String packageName, Transactor transactor)
static List<Analyzable> scan(String packageName, ScanDirective... directives)
static List<Analyzable> scan(String packageName, Transactor transactor, ScanDirective... directives)

// Scan with full metadata (class name, field/method name)
static List<Result> scanDetailed(String packageName)
static List<Result> scanDetailed(String packageName, Transactor transactor)
static List<Result> scanDetailed(String packageName, ScanDirective... directives)
static List<Result> scanDetailed(String packageName, Transactor transactor, ScanDirective... directives)

// Describe an analyzable's operation structure
static String describe(Analyzable analyzable)

// Result record
record Result(String className, String fieldName, Analyzable analyzable)

ScanDirective (Java)

sealed interface ScanDirective {
// Skip a method — pass a method reference
static ScanDirective skip(Ref0<?> ref)
static <A> ScanDirective skip(Ref1<A, ?> ref)

// Provide manual invocation — pass a method reference, variant name, and arguments
static <R> ScanDirective manual(Ref0<R> ref, String variantName)
static <A, R> ScanDirective manual(Ref1<A, R> ref, String variantName, A a)
static <A, B, R> ScanDirective manual(Ref2<A, B, R> ref, String variantName, A a, B b)

// Add an external object to the scan
static ScanDirective instance(Object obj)
static <T> ScanDirective instance(T obj, BiConsumer<T, InstanceConfig<T>> config)
}

ScanDirective (Kotlin)

// Top-level functions in dev.typr.foundationskt
fun skip(clazz: Class<*>, methodName: String): ScanDirective
fun manual(clazz: Class<*>, methodName: String, variantName: String, result: Analyzable): ScanDirective
fun instance(obj: Any): ScanDirective
fun <T : Any> instance(obj: T, block: InstanceScope<T>.() -> Unit): ScanDirective

ScanDirective (Scala)

// Methods on AnalyzableScanner object
def skip(clazz: Class[?], methodName: String): ScanDirective
def instance(obj: Any): ScanDirective
def instance[T](obj: T)(config: InstanceScope[T] => Unit): ScanDirective

AlignmentError Types

sealed interface AlignmentError {
int position();
String message();
Str styledMessage();

// Parameter errors
record ExtraParameter(int position, DbType<?> type)
record MissingParameter(int position, JdbcMeta.ParameterMeta meta)
record ParameterTypeMismatch(int position, DbType<?> declared,
JdbcMeta.ParameterMeta expected, Set<String> declaredTypeNames, String reason)

// Column errors
record ExtraColumn(int position, DbType<?> type)
record MissingColumn(int position, JdbcMeta.ColumnMeta meta)
record ColumnTypeMismatch(int position, String columnName, DbType<?> declared,
JdbcMeta.ColumnMeta returned, Set<String> declaredTypeNames, String reason)
record NullabilityMismatch(int position, String columnName, DbType<?> type)
}