Skip to main content

ql

@webda/ql — WebdaQL​

A structured query language (WQL) for filtering and paginating Webda Store results. WQL queries are parsed by an ANTLR4 grammar and translated to native backend queries by each Store implementation (in-memory, MongoDB, PostgreSQL, DynamoDB).

When to use it​

You need WQL when you call store.query() or a relation's .query() method. Every Webda Store accepts a WQL string; no raw SQL, MongoDB filter objects, or DynamoDB conditions needed.

// Query the User repository
const { results } = await User.getRepository().query(`email = 'alice@example.com' LIMIT 1`);

// Query a relation
const { results: posts } = await user.posts.query(`status = 'published' ORDER BY createdAt DESC LIMIT 10`);

Install​

npm install @webda/ql

This package is a dependency of @webda/core — you rarely need to install it directly.

Syntax overview​

expression? orderExpression? limitExpression? offsetExpression? -- a filter query (implicit SELECT)
SELECT f1, f2 [WHERE expression] [ORDER BY ...] [LIMIT n] [OFFSET t]
SELECT g, COUNT(*) AS n, SUM(f) AS s [WHERE expression] [GROUP BY g] [ORDER BY ...] [LIMIT n]
DELETE [WHERE expression] [LIMIT n]
UPDATE SET a = v, b.c = v [WHERE expression] [LIMIT n]

Keywords are uppercase only: select, delete, set, where... in lowercase are field names. Since the aggregation syntax, uppercase COUNT, SUM, AVG, MIN, MAX, AS, DISTINCT and GROUP BY are reserved too (breaking for attributes with those uppercase names).

Filter expressions support:

  • Comparison: field = value, field != value, field > value, field >= value, field < value, field <= value
  • Time function: field > NOW(), field < NOW('-10m'), field >= NOW('+1d') (millisecond timestamp resolution)
  • Pattern match: field LIKE "pattern" (_ = single char, % = any chars)
  • Set membership: field IN [value, value, ...]
  • Array contains: field CONTAINS value
  • Null checks: field IS NULL, field IS NOT NULL (missing, undefined or null)
  • Logic: AND, OR, ( ... )

Pagination / ordering:

  • ORDER BY field [ASC|DESC], ...
  • LIMIT <integer>
  • OFFSET "<continuationToken>"

Quick examples​

import * as WebdaQL from "@webda/ql";

// Parse and evaluate against an in-memory object
const validator = new WebdaQL.QueryValidator(`status = 'published' AND viewCount >= 100`);
const post = { status: "published", viewCount: 150 };
console.log(validator.eval(post)); // true

// Prepend a mandatory condition to a user-supplied query
const merged = WebdaQL.PrependCondition(`status = 'published' ORDER BY title LIMIT 10`, `authorId = 'u-123'`);
// => 'status = "published" AND authorId = "u-123" ORDER BY title ASC LIMIT 10'

Parameters​

Never concatenate user input into a query. Pass values as parameters instead, with ? and an array or :name and an object:

await Task.query("owner = ? AND status IN ?", [user, ["open", "late"]]);
await Task.query("owner = :owner OR reviewer = :owner", { owner: user });

WebdaQL.bind("priority >= ? LIMIT ?", [2, 10]); // priority >= 2 LIMIT 10

Placeholders are only allowed where a value is expected, values are escaped by type, and = ? / != ? with null become IS NULL / IS NOT NULL. Template literals passed straight to a query method are escaped the same way at compile time by webdac. See Parameters.

NOW() time function​

WebdaQL supports NOW() to produce dynamic numeric timestamps (milliseconds since Unix epoch):

  • NOW(): current timestamp (Date.now()).
  • NOW('-10m') / NOW('+1d'): current timestamp offset by duration string.
  • Units supported: ms, s/sec, m/min, h/hr, d/day, mo/mon/month, y/yr/year. Multi-part durations like NOW('-1d2h') are supported.
  • Numeric millisecond offsets: NOW(-60000) or NOW(60000).
  • Parameters: NOW(?) and NOW(:offset) can be bound using bind().
  • Updates: UPDATE SET lastLoginAt = NOW().
  • Dynamic evaluation: Expressions with NOW() evaluate dynamically at query execution or access time, allowing cached queries and operation permissions to track real time:
@Operation({ permission: "mfa = 'verified' AND mfaAt > NOW('-10m')" })

Statements: SELECT, DELETE, UPDATE​

WebdaQL parses SELECT, DELETE and UPDATE statements; the parsed Query carries type ("SELECT" for a plain filter), fields (SELECT) and assignments (UPDATE), and toString() round-trips them.

  • Field lists are parse-level only: the Query operations (REST, gRPC, MCP, GraphQL) refuse a SELECT field list with a 400, and repositories refuse it in query().
  • DELETE and UPDATE are repository-level operations: useRepository(Model).deleteMany("DELETE WHERE ...") and updateMany("UPDATE SET ... WHERE ...") run them in bulk and return the affected count. They bypass events, hooks, validation and canAct. The Query operations refuse them with a 400, and query() / iterate() refuse them.
const q = WebdaQL.parse("UPDATE SET status = 'archived' WHERE owner = 'bob' LIMIT 10");
q.type; // "UPDATE"
q.assignments; // [{ field: "status", value: "archived" }]
await useRepository(Task).updateMany("UPDATE SET status = ? WHERE owner = ?", ["archived", "bob"]); // count

See Statements.

Aggregations​

A SELECT with aggregate functions or GROUP BY parses into an AggregationQuery (parse(q).aggregation), the same AST toAggregationQuery() builds from the object form used by Model.aggregate(). Stores translate it natively, or stream objects through the in-memory Aggregator.

const q = WebdaQL.parse("SELECT status, COUNT(*) AS n, SUM(points) AS total GROUP BY status ORDER BY total DESC");
q.aggregation.metrics; // { n: { fn: "COUNT" }, total: { fn: "SUM", field: "points" } }

See Statements and Repositories.

Code completion​

WebdaQL provides grammar-driven code completion powered by antlr4-c3. getCompletions returns candidates for fields, keywords, operators, enum/boolean values, and parameters for a query and caret offset:

import { getCompletions, fieldsFromSchema } from "@webda/ql";

const fields = [
{ name: "status", type: "string", enum: ["draft", "published"] },
{ name: "viewCount", type: "integer" }
];

const { items, prefix } = getCompletions("status = 'dr", 12, { fields });
// => items offer "'draft'", replacing range [9, 13]

See Code Completion.

API reference​

ExportDescription
QueryValidatorParses a WQL string; eval(obj) evaluates it, toString() normalizes it
bind(query, params)Binds ? / :name placeholders to escaped values
escape(parts, values)Escapes template literal values (used by the compile-time rewrite)
validateSyntax(query)Checks a query against the grammar without evaluating it; placeholders allowed
PrependCondition(query, condition)Merges a condition in front of an existing query, preserving ORDER BY / LIMIT / OFFSET
ExpressionBuilderANTLR visitor that builds the optimized expression AST
AndExpressionLogic AND node
OrExpressionLogic OR node
ComparisonExpressionComparison leaf node
parse(query, allowedFields?)Parses a filter or statement; allowedFields checks SELECT fields and SET targets
validateQueryFields(query, allowed)Checks SELECT fields and SET targets against a field list
assertFilterQuery(query)Refuses DELETE, UPDATE, SELECT field lists and aggregations
toAggregationQuery(input, params?)Converts the object form of an aggregation to a validated AggregationQuery
validateAggregation(query)Validates an AggregationQuery (paths, aliases, functions, ORDER BY, LIMIT)
AggregatorIn-memory aggregation engine: add(item) then rows()
compareValues(a, b)The cross-backend ordering of aggregated values (null < numbers < strings)
AggregationErrorRuntime aggregation error with a code (AGGREGATION_NOT_NATIVE, AGGREGATION_TOO_MANY_GROUPS)
QueryParsed query result: { type, filter, fields?, assignments?, aggregation?, orderBy?, limit?, continuationToken? }
OrderBy{ field: string; direction: "ASC" | "DESC" }
getCompletions(query, caret, opts)Returns grammar-driven code completion candidates at a caret offset
fieldsFromSchema(schema, maxDepth?)Flattens a JSON schema into completion fields with dotted paths

See also​

Classes​

Interfaces​

Type Aliases​

Variables​

Functions​