-
Notifications
You must be signed in to change notification settings - Fork 2
OQL Asserts
Runtime validators for OQL queries.
- Overview
- Installation
- Validator Pattern
- Top-Level Query Validator
- DML Validators
- DDL Validators
- Filter Validators
- Aggregate Validators
- Expression Validators
- Column Identifier Validators
- Query Common Validators
- Usage Examples
The asserts module provides runtime validation for OQL queries. All validators follow the Guardian pattern with two variants:
-
Assert functions - Throw
TypeErroron validation failure -
Type guard functions - Return
booleanwithout throwing
This ensures queries are structurally valid before translation and execution.
Deno:
deno add @tundralibs/oqlBun:
bunx jsr add @tundralibs/oqlNode.js:
npx jsr add @tundralibs/oqlEach query type has both assert and type guard variants:
import { assertSelect, isSelect } from '@tundralibs/oql/asserts';
const query = {
type: 'SELECT',
table: 'users',
columns: ['id', 'email'],
projection: { '@id': true, '@email': true },
};
// Assert variant - throws on failure
try {
assertSelect(query);
console.log('Query is valid');
} catch (error) {
console.error('Validation failed:', (error as Error).message);
}
// Type guard variant - returns boolean
if (isSelect(query)) {
console.log('Query is valid');
// TypeScript now knows query is SelectQuery
} else {
console.log('Query is invalid');
}import { assertQuery, isQuery } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertQuery(query); // Throws TypeError if invalid
const valid = isQuery(query); // Returns booleanassertQuery / isQuery are the top-level type-dispatching validators. They validate the top-level shape (a non-null object with a string type) and then delegate to the matching specific validator based on the type discriminator.
Validates:
- Value is an object (else
Invalid Query: Expected object, got ...) -
typeis a string (elseInvalid Query: Expected 'type' property to be a string) - Dispatches by
typeto the appropriate validator:- DML:
SELECT,INSERT,INSERT_FROM_QUERY,UPDATE,DELETE,UPSERT,COUNT - DDL — Table:
CREATE_TABLE,ALTER_TABLE,DROP_TABLE,TRUNCATE - DDL — Schema:
CREATE_SCHEMA,DROP_SCHEMA - DDL — Index:
CREATE_INDEX,DROP_INDEX - DDL — View:
CREATE_VIEW,ALTER_VIEW,DROP_VIEW,REFRESH_MATERIALIZED_VIEW
- DML:
- An unrecognized
typethrowsInvalid Query: Unknown query type '<type>'
import { assertSelect, isSelect } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertSelect(query); // Throws TypeError if invalid
const valid = isSelect(query); // Returns booleanValidates:
- Query type is
'SELECT'. - Table name is a non-empty string.
-
columnsis a non-empty array; every column referenced anywhere in the query (WHERE, joins, aggregates, expressions, orderBy, …) must be listed here. -
projectionmaps@-prefixed column identifiers (or@expressionKey/@aggregateKey/@JoinAlias.@col) to alias strings ortruefor passthrough. Alias values must not start with@— the alias is the output column name, not a column reference. - WHERE uses valid filter operators; aggregates are rejected (use HAVING).
- WHERE may reference pre-declared
expressionsby their@key; the translator substitutes the expression body. - JOIN conditions reference declared join columns.
- Aggregates have valid
$$_aggregatediscriminators and column refs. - ORDER BY keys must exist in projection or be joined columns.
-
limit, if provided, must be a positive integer (>= 1;0is rejected). -
offset, if provided, must be a non-negative integer (>= 0). -
distinct, if provided, must be a boolean.distinct: trueis rejected when combined withaggregatesor a join-alias projection (JSON_ROWauto-expand) — the automatic GROUP BY those trigger already deduplicates rows.
import { assertInsert, isInsert } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertInsert(query);
const valid = isInsert(query);Validates:
- Query type is 'INSERT'
- Table and columns are specified
- Data keys are in the column list
- Data values are:
null/undefined, a primitive (string/number/boolean/bigint), aDate, a validExpression(object with a$$_expressiondiscriminator), or a literal object payload (object without a$$_expressionfield — typical for JSON / JSONB columns) - Expressions (values with a
$$_expressionfield) reject@colreferences on INSERT
import {
assertInsertFromQuery,
isInsertFromQuery,
} from '@tundralibs/oql/asserts';
declare const query: unknown;
assertInsertFromQuery(query);
const valid = isInsertFromQuery(query);Validates:
- Query type is
'INSERT_FROM_QUERY' - Table and target
columnsare specified -
query(the source SELECT) is required and must itself passassertSelectQuery -
columns.lengthmust equal the source SELECT'sprojectionkey count — the match is positional, by count, not by name. A target/source pair with the same number of entries but different names still passes; a mismatched count throwsInvalid INSERT_FROM_QUERY query: target 'columns' has N entries, but source SELECT projection has M entries — they must match. Ordercolumnsto line up with the sourceprojection's key order, or the insert silently writes values into the wrong target columns. See the worked example in Type System (theINSERT_FROM_QUERYbranch).
import { assertUpdate, isUpdate } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertUpdate(query);
const valid = isUpdate(query);Validates:
- Query type is 'UPDATE'
- Table and columns are specified
- Data keys are in the column list
- Data values follow the INSERT rules above (same literal-vs-Expression disambiguation); column references inside Expressions are permitted (UPDATE can reference the row being modified)
- WHERE clause is valid
import { assertDelete, isDelete } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertDelete(query);
const valid = isDelete(query);Validates:
- Query type is 'DELETE'
- Table is specified
-
columnsis a non-empty array — this is the table's column list used to resolve@colreferences inside the optionalwherefilter (and any pre-declaredexpressions), not a projection (DELETE returns no rows). A missing or empty array throwsInvalid DELETE query: 'columns' must be a non-empty array. - Optional
expressionsare well-formed - WHERE clause is valid (syntactically optional — DELETE without WHERE removes every row)
import { assertUpsert, isUpsert } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertUpsert(query);
const valid = isUpsert(query);Validates:
- Query type is 'UPSERT'
- Table and columns are specified
-
datais a non-empty object, or a non-empty array of non-empty objects; each entry's keys/values follow the same rules as INSERT (literal-vs-Expression disambiguation), except@colreferences inside Expressions are permitted (like UPDATE — an upsert's UPDATE branch can reference the row being modified) -
conflictKeysis a non-empty array of declared column identifiers - Optional
updateOnConflict, if present, must be an array (empty means copy nothing) where every entry is disjoint fromconflictKeys(throwsupdateOnConflict should not include conflictKey '<id>'— there's no point updating the column you matched the conflict on) and every entry must name a key present in the supplieddata(throwsupdateOnConflict column '<id>' (<col>) must exist in data). With arraydata, this existence check runs against only the first row — a later row silently missing that key is not caught here (it surfaces per-dialect at translation/execution instead: on MongoDB a data row missing a conflict key value throwsDialectUnsupportedError, see Compatibility Matrix; on the SQL dialects a missing non-conflict column just takes its DEFAULT/NULL). - Optional
updateSet, if present, must be a plain object whose keys are declared columns, none of them a conflict key or also listed inupdateOnConflict(throws'@<col>' is in both updateOnConflict and updateSet). Values follow the same rules as UPDATE data, so an expression may reference@col(the existing row).
import { assertCount, isCount } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertCount(query);
const valid = isCount(query);Validates:
- Query type is 'COUNT'
- Table is specified
-
columnsis a non-empty array — this is the table's column list used to resolve@colreferences inside the optionalwherefilter andjoins, not a projection (COUNT returns just a number). A missing or empty array throwsInvalid COUNT query: 'columns' must be a non-empty array. - Optional
distinctis an array of exactly one plain column name (no@prefix) that must appear incolumns— emitted asCOUNT(DISTINCT col); multi-column DISTINCT counts are rejected as dialect-fragile. - Optional
expressionsare well-formed; optionaljoinsextend the filter scope with@<Alias>.@<col>references - WHERE clause is valid
- A
havingclause is rejected — a COUNT has no GROUP BY and no aggregate alias to filter on. ThrowsInvalid COUNT query: 'having' is not supported. Use aSELECTwithaggregates+havinginstead.
Every DDL validator below rejects a query object carrying a property
outside its known set — even where the per-type list further down
doesn't call it out separately. This is a typo-catcher ({ tabel: 'x' }
throws instead of silently doing nothing) verified against the
rejectExtraProps calls in packages/oql/asserts/query/DDL/{table,view,schema,index}.ts.
import { assertCreateTable, isCreateTable } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertCreateTable(query);
const valid = isCreateTable(query);Validates:
- Table name and columns
- Column definitions with types
- Primary key constraints
- Foreign key constraints
- Unique constraints
import { assertAlterTable, isAlterTable } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertAlterTable(query);
const valid = isAlterTable(query);Validates:
-
typeis'ALTER_TABLE';table(and optionalschema) are valid identifiers - At least one modification operation (
addColumns,alterColumns,dropColumns,renameColumns,addForeignKeys,dropForeignKeys,renameTo) is present — an empty ALTER throws -
addColumns/alterColumns, if present, are non-empty column-definition maps (each key a valid column identifier, each value a validColumnDefinition) -
alterColumnsentries MUST setnullableexplicitly (a boolean). The definition replaces the column, and the dialects disagree on the default for an omittednullable: MariaDB'sMODIFY COLUMNresets omitted attributes (silently clearingNOT NULL) while Postgres preserves them — so a missingnullablewould mean opposite outcomes per dialect. A missing or non-booleannullablethrowsInvalid ALTER_TABLE query: alterColumns.<col> must set nullable explicitly (boolean) — dialects disagree on the default. -
dropColumns/dropForeignKeys, if present, are non-empty arrays of plain (non-@-prefixed) identifier strings -
renameColumns, if present, is a non-empty map ofoldName → newName, both valid column identifiers -
addForeignKeys, if present, is a non-empty object of valid FK constraints — an empty object throwsInvalid ALTER_TABLE query: addForeignKeys cannot be empty. The FK columns are not checked against a column list (they may be added in the same statement, so existence is enforced at execution) - Optional
renameTois a valid table-name identifier - No unexpected properties
import { assertCreateIndex, isCreateIndex } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertCreateIndex(query);
const valid = isCreateIndex(query);Validates:
- Index name and table
- Column list with @ prefixes
- Index method (BTREE, HASH, etc.)
- Unique/partial index options
import { assertCreateView, isCreateView } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertCreateView(query);
const valid = isCreateView(query);Validates:
- View name
- Source SELECT query
- Materialized view options
import { assertAlterView, isAlterView } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertAlterView(query);
const valid = isAlterView(query);Validates:
-
typeis'ALTER_VIEW' -
viewis a valid identifier; optionalschemais a valid identifier - Optional
renameTois a valid view-name identifier - Optional
queryis a valid SELECT query - At least one of
renameToorquerymust be present (an empty ALTER is meaningless) - No unexpected properties
import { assertDropView, isDropView } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertDropView(query);
const valid = isDropView(query);Validates:
-
typeis'DROP_VIEW' -
viewis a valid identifier; optionalschemais a valid identifier - Optional
materialized/ifExists/cascadeare booleans (materialized: truemarks the target as a materialized view — Postgres needsDROP MATERIALIZED VIEW; emulating dialects ignore it) - No unexpected properties
import {
assertRefreshMaterializedView,
isRefreshMaterializedView,
} from '@tundralibs/oql/asserts';
declare const query: unknown;
assertRefreshMaterializedView(query);
const valid = isRefreshMaterializedView(query);Validates:
-
typeis'REFRESH_MATERIALIZED_VIEW' -
viewis a valid identifier; optionalschemais a valid identifier - Optional
concurrentlyis a boolean
import { assertDropTable, isDropTable } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertDropTable(query);
const valid = isDropTable(query);Validates:
-
typeis'DROP_TABLE' -
tableis a valid identifier; optionalschemais a valid identifier - Optional
ifExists/cascadeare booleans - No unexpected properties
import { assertTruncate, isTruncate } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertTruncate(query);
const valid = isTruncate(query);Validates:
-
typeis'TRUNCATE' -
tableis a valid identifier; optionalschemais a valid identifier - Optional
cascadeis a boolean - No unexpected properties
import { assertCreateSchema, isCreateSchema } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertCreateSchema(query);
const valid = isCreateSchema(query);Validates:
-
typeis'CREATE_SCHEMA' -
schemais required and a valid identifier - No other properties are allowed
import { assertDropSchema, isDropSchema } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertDropSchema(query);
const valid = isDropSchema(query);Validates:
-
typeis'DROP_SCHEMA' -
schemais required and a valid identifier - Optional
cascadeis a boolean
import { assertDropIndex, isDropIndex } from '@tundralibs/oql/asserts';
declare const query: unknown;
assertDropIndex(query);
const valid = isDropIndex(query);Validates:
-
typeis'DROP_INDEX' -
indexis required and a valid identifier; optionalschemais a valid identifier - Optional
ifExists/cascadeare booleans
import { assertQueryFilter, isQueryFilter } from '@tundralibs/oql/asserts';
const filter = {
'@age': { $gte: 18 },
'@email': { $like: '%@company.com' },
};
assertQueryFilter(filter, ['age', 'email']);
const valid = isQueryFilter(filter, ['age', 'email']);Signature: assertQueryFilter(x, columnList?, depth?, maxDepth?) / isQueryFilter(x, columnList?, depth?, maxDepth?). depth defaults to 0 and maxDepth to 10; both are internal recursion controls.
Validates:
- Column identifiers start with @
- Operator types are valid
- Logical operators (
$and,$or) — each must be a non-empty array of nested QueryFilters -
$exists/$nexistsvalues are valid ExistsFilter specs (validated viaassertExistsFilter, counting against the same depth budget) - Column references exist in provided list
- Nesting beyond
maxDepth(default 10) throws to guard against runaway recursion
import { assertExistsFilter, isExistsFilter } from '@tundralibs/oql/asserts';
assertExistsFilter({
table: 'orders',
on: { '@userId': '@id' },
where: { '@status': 'paid' },
});Signature: assertExistsFilter(x, depth?, maxDepth?) / isExistsFilter(x, depth?, maxDepth?). Validates the $exists / $nexists payload of a QueryFilter.
Validates:
-
tableis a non-empty string; optionalschemais a non-empty string -
onis a non-empty object; keys are single-segment@columnidentifiers into the subquery table (no alias prefix — the internal__exists__alias is implicit) -
onvalues arenull, primitives,Dates, or@-ref strings — expression objects are rejected (their embedded column refs cannot be qualified reliably inside the subquery) - Optional
whereis a structurally valid QueryFilter (validated without a column list — the subquery table's columns are not declared); its nesting counts against the sharedmaxDepthbudget
import {
assertFilterOperator,
isFilterOperator,
} from '@tundralibs/oql/asserts';
assertFilterOperator({ '@price': { $gt: 100 } }, ['price']);
const valid = isFilterOperator({ '@age': 25 }, ['age']);Signature: assertFilterOperator(x, columnList?) / isFilterOperator(x, columnList?). This is the column-to-operators map without the logical-operator ($and / $or) layer that QueryFilter adds.
Validates:
- Value is a non-empty object (not an array)
- Each key is a valid column identifier (and, if
columnListis provided, exists in it) - Each value is valid Operators (a direct value, an array, or an operator object). An invalid value throws
Invalid FilterOperator: Value for '<key>' must be valid Operators (direct value, array, or operator object)
import { assertOperators, isOperators } from '@tundralibs/oql/asserts';
const operators = { $gte: 18, $lt: 65 };
assertOperators(operators, 'number');
const valid = isOperators(operators);Validates:
- Operator keys are valid (
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$between,$null,$like,$nlike,$ilike,$nilike,$startsWith,$endsWith,$contains). - Operator values match expected types per slot.
- Array operators (
$in,$nin,$between) require non-empty arrays. -
$betweenrequires exactly two elements. -
nullis rejected in operator-form values:$eq: null,$ne: null,$in: [null, …],$nin: [null, …], and comparison-op values like$gt: nullall throw. Use$null: true/$null: falsefor null comparisons. -
Expressions accepted in comparison/string operator values: any non-Date object with a
$$_expressiondiscriminator passes ($eq: { $$_expression: 'MULTIPLY', args: [...] }). -
$like/$nlike/etc. only valid on string-typed columns. -
$gt/$gte/$lt/$lte/$betweenonly valid on numeric/bigint/date columns.
The two rules above are enforced only when
assertOperatorsis called directly with acolumnTypeargument — they are NOT enforced on a real query's WHERE/HAVING clause.assertFilterOperator(and thereforeassertQueryFilter, and thereforeassertSelect/assertUpdate/ … for every DML type) callsisOperators(value)with nocolumnTypeargument — verified against the single production call site inpackages/oql/asserts/filters/filterOperator.ts. Column identifiers are just strings at runtime; nothing in the assert layer tracks a schema mapping column name → TS type, so there is no value it could pass. In practice this meansassertSelectwill happily accept{ '@price': { $like: '%x%' } }on anumbercolumn, or{ '@name': { $gt: 5 } }on astringcolumn, at runtime — only TypeScript'sOperators<T>conditional type catches that, and only for a query whoseQuery<QT, PT, LT>generic is accurately typed. A query assembled dynamically (built fromunknowninput, cast past the type parameter, or run throughisOperators/assertOperatorswithout acolumnType) gets none of that protection. If you need this checked at runtime, callassertOperators(value, columnType)yourself per field before it reachesassertQuery.
import { assertJoinDetails, isJoinDetails } from '@tundralibs/oql/asserts';
// `table` and `columns` are REQUIRED. The missing-`table` check fires
// first, so an object with only `type`/`on` throws
// "Invalid JoinDetails: Missing required 'table' property".
const join = {
table: 'Profile',
columns: ['userId'],
type: 'LEFT',
on: { '@Profile.@userId': '@User.@id' },
};
assertJoinDetails(join, ['User.id', 'Profile.userId']);
const valid = isJoinDetails(join, ['User.id', 'Profile.userId']);Signature: assertJoinDetails(x, columnList?) / isJoinDetails(x, columnList?).
Validates:
-
tableis required and must be a string or symbol -
columnsis required and must be a non-empty array of plain (non-@-prefixed) names — the columns the joined table exposes - Optional
schemais a string if present -
on(the JoinFilter / ON clause) is required and references valid columns - Optional
typeis one ofINNER,LEFT,RIGHT,FULL
import { assertJoinFilter, isJoinFilter } from '@tundralibs/oql/asserts';
// Column-to-column join (ON clause)
assertJoinFilter(
{ '@Profile.@userId': '@User.@id' },
['User.id', 'Profile.userId'],
);
const valid = isJoinFilter(
{ '@Profile.@isActive': true },
['Profile.isActive'],
);Signature: assertJoinFilter(x, columnList?) / isJoinFilter(x, columnList?). JoinFilter is the ON clause of a join.
Validates:
- Value is a non-empty object (not an array)
- Each key is a valid column identifier (and, if
columnListis provided, exists in it) - Each value is
null(NULL check), a primitive (string / number / boolean / bigint), aDate, a column reference, or an expression
import { assertJoins, isJoins } from '@tundralibs/oql/asserts';
declare const value: unknown;
assertJoins(
{
Profile: {
table: 'Profile',
columns: ['userId'],
on: { '@Profile.@userId': '@User.@id' },
},
},
['User.id', 'Profile.userId'],
);
const valid = isJoins(value, ['User.id', 'Profile.userId']);Signature: assertJoins(x, columnList?) / isJoins(x, columnList?). Joins is a record mapping each join alias to its JoinDetails.
Validates:
- Value is a non-empty object (not an array)
- Each entry's value is a valid JoinDetails (see Join Details)
import { assertAggregates, isAggregates } from '@tundralibs/oql/asserts';
assertAggregates({ $$_aggregate: 'SUM', column: '@amount' }, ['amount']);
const valid = isAggregates({ $$_aggregate: 'COUNT' });assertAggregates / isAggregates are the public aliases of assertAggregate / isAggregate. They dispatch by the $$_aggregate discriminator (COUNT, SUM, AVG, MIN, MAX, STRING_AGG, ARRAY_AGG, JSON_ROW) to the matching per-type validator. All aggregate validators share the signature (x, columnList?).
Validates:
- Value is a non-null object with a string
$$_aggregatediscriminator that is one of the recognized aggregate names - Delegates the rest of the shape to the per-type validator
import {
assertArrayAggAggregate,
assertAvgAggregate,
assertCountAggregate,
assertJsonRowAggregate,
assertMaxAggregate,
assertMinAggregate,
assertStringAggAggregate,
assertSumAggregate,
isArrayAggAggregate,
isAvgAggregate,
isCountAggregate,
isJsonRowAggregate,
isMaxAggregate,
isMinAggregate,
isStringAggAggregate,
isSumAggregate,
} from '@tundralibs/oql/asserts';Each pair has the signature (x, columnList?).
-
assertCountAggregate/isCountAggregate—columnis optional. Without it the aggregate isCOUNT(*)anddistinctmust be absent; with it, optionaldistinct: truemakesCOUNT(DISTINCT column). -
assertSumAggregate/isSumAggregate,assertAvgAggregate/isAvgAggregate,assertMinAggregate/isMinAggregate,assertMaxAggregate/isMaxAggregate— require acolumn(column identifier or a nested numeric expression); optional booleandistinct. -
assertStringAggAggregate/isStringAggAggregate— requires acolumn; optionalseparatormust be a string; optional booleandistinct. -
assertArrayAggAggregate/isArrayAggAggregate— requires acolumn; optional booleandistinct. -
assertJsonRowAggregate/isJsonRowAggregate— requires acolumns: Record<string, ColumnOrExpression>map (each key becomes a JSON property);distinctis not supported.
import { assertExpressions, isExpressions } from '@tundralibs/oql/asserts';
assertExpressions(
{ $$_expression: 'MULTIPLY', args: ['@price', '@quantity'] },
['price', 'quantity'],
);
const valid = isExpressions({ $$_expression: 'NOW' });assertExpressions / isExpressions are the public aliases of assertExpression / isExpression. They dispatch to the matching category validator (numeric, string, or date) based on the $$_expression discriminator.
Signature: assertExpressions(x, columnList?, depth?, maxDepth?) / isExpressions(x, columnList?, depth?, maxDepth?). depth defaults to 0 and maxDepth to 10 (internal recursion controls).
Validates:
- Value is a valid base expression with a recognized
$$_expressiontype - Delegates the category-specific shape (numeric / string / date) to the matching validator
- An unrecognized type throws
Invalid Expression type: Unknown expression type '<type>' - Nesting beyond
maxDepththrows to guard against runaway recursion
import {
assertColumnIdentifier,
isColumnIdentifier,
} from '@tundralibs/oql/asserts';
assertColumnIdentifier('@id', ['id', 'name', 'email']);
assertColumnIdentifier('@user.@id', ['user.id']); // nested / joined column
const valid = isColumnIdentifier('@userName');Signature: assertColumnIdentifier(x, columnList?) / isColumnIdentifier(x, columnList?).
Validates:
- Value is a string
- Each
.-separated segment starts with@and, after the@, is a valid identifier (alphanumeric/underscore, starting with a letter or underscore) - If
columnListis provided, the column (with@prefixes stripped) must exist in it. Note: an emptycolumnList([]) rejects every reference
These shared property validators are used internally by both DML and DDL asserts and are also exported. They take a context label (and, for assertQueryType, an expectedType) that is interpolated into the error messages. They have no is* variants.
import {
assertColumns,
assertQueryType,
assertSchemaName,
assertTableName,
} from '@tundralibs/oql/asserts';-
assertTableName(query, context): void— assertsquery.tableexists and is a non-empty string. -
assertSchemaName(query, context): void— assertsquery.schema, if present, is a non-empty string (absent is allowed). -
assertColumns(query, context): string[]— assertsquery.columnsis a non-empty array of plain (non-@-prefixed) strings and returns the validated array. -
assertQueryType(query, expectedType, context): void— assertsquery.typestrictly equalsexpectedType.
import type { Query } from '@tundralibs/oql';
import { assertSelect } from '@tundralibs/oql/asserts';
import { PostgresTranslator } from '@tundralibs/oql/translator';
declare const database: {
query(sql: string, params: Record<string, unknown>): unknown;
};
function executeQuery(query: Query<'SELECT', any>) {
// Validate structure
assertSelect(query);
// Safe to translate
const translator = new PostgresTranslator();
const { sql, params } = translator.select(query);
// Execute with driver
return database.query(sql, params);
}import { isInsert, isSelect } from '@tundralibs/oql/asserts';
function handleQuery(query: unknown) {
if (isSelect(query)) {
// TypeScript knows query is SelectQuery
console.log('Processing SELECT from', query.table);
} else if (isInsert(query)) {
// TypeScript knows query is InsertQuery
console.log('Processing INSERT into', query.table);
} else {
console.error('Unknown query type');
}
}import { assertSelect } from '@tundralibs/oql/asserts';
declare function executeQuery(query: unknown): unknown;
function validateAndExecute(query: unknown) {
try {
assertSelect(query);
// Query is valid, proceed
return executeQuery(query);
} catch (error) {
if (error instanceof TypeError) {
// Validation error
console.error('Query validation failed:', error.message);
return { error: error.message };
}
throw error;
}
}import { assertSelect } from '@tundralibs/oql/asserts';
const complexQuery = {
type: 'SELECT',
table: 'orders',
columns: ['userId', 'total', 'createdAt'],
joins: {
// A JOIN spec needs its own `table` and `columns` (the columns the
// joined table exposes) in addition to `on`.
'users': {
table: 'users',
columns: ['id', 'email'],
type: 'INNER',
on: { '@users.@id': '@userId' },
},
},
aggregates: {
'totalRevenue': { $$_aggregate: 'SUM', column: '@total' },
},
projection: {
'@userId': 'customerId',
'@users.@email': 'customerEmail',
'@totalRevenue': true,
},
having: {
'@totalRevenue': { $gte: 1000 },
},
};
// Validates entire query structure including:
// - Table and columns
// - Join conditions
// - Aggregate definitions
// - Projection mapping
// - HAVING clause filters
assertSelect(complexQuery);Validators provide detailed error messages:
import { assertQueryFilter, assertSelect } from '@tundralibs/oql/asserts';
// Missing required field
assertSelect({ type: 'SELECT' });
// TypeError: Invalid SELECT query: 'table' is required
// Invalid column reference
assertSelect({
type: 'SELECT',
table: 'users',
columns: ['id'],
projection: { '@email': true }, // email not in columns
});
// TypeError: Invalid SELECT query: projection key '@email' does not exist in
// columns, expressions, aggregates, or joined columns. Available: @id
// Invalid operator
assertQueryFilter({ '@age': { $invalid: 18 } }, ['age']);
// TypeError: Invalid QueryFilter: Filter properties are invalid - Invalid
// FilterOperator: Value for '@age' must be valid Operators (direct value,
// array, or operator object)- Optimized for speed - Validators use early returns and minimal allocations
- Type narrowing - Type guard functions provide TypeScript type narrowing
- No dependencies - Pure TypeScript with no external dependencies
- 98%+ test coverage - Thoroughly tested for reliability
- Type System - Query type definitions
- Translators - SQL/NoSQL translation