Lambda Query Language

A functional pipeline language for database queries, migration logic, RLS predicates, and portable SQL generation.

tenant_orders.lql
-- Join, filter, aggregate, and emit dialect-specific SQL let completed_orders = users |> join(orders, on = users.id = orders.user_id) |> filter(fn(row) => row.orders.status = 'completed') completed_orders |> group_by(users.id, users.name) |> select( users.name, count(*) as order_count, sum(orders.total) as revenue ) |> order_by(revenue desc)

What LQL Is For

Use one expression language for query pipelines, database-portable SQL, and migration-time predicates.

|>

Pipeline Queries

Start from a table or binding and pass rows through select, filter, join, grouping, ordering, pagination, and set operations.

SQL

Dialect Output

The same LQL statement emits PostgreSQL, SQL Server, or SQLite SQL through the dialect packages.

RLS

Migrations

Migration YAML can use LQL for RLS policies and PostgreSQL function bodies instead of embedding raw platform SQL.

LSP

Editor Tooling

The Rust language server powers diagnostics, formatting, schema-aware completions, hovers, and optional AI suggestions.

F#

Type Provider

F# projects can validate LQL at compile time and expose generated SQL through the type provider.

DX

Transpiler Demo

The Blazor page is kept as a focused playground for running client-side transpilation only.

Examples

The core language is small: compose table pipelines, lambda predicates, and expression columns.

Simple Selection

Select explicit columns from a source table. Fully qualify columns when that keeps intent clear.

  • Table-first query shape
  • Column projection with `select`
  • Portable output SQL
users |> select( users.id, users.name, users.email )

Lambda Filtering

Use `fn(row) => ...` predicates for filtering. The row parameter exposes fields through table-qualified paths.

  • Comparison operators: `=`, `!=`, `>`, `<`, `>=`, `<=`
  • Logical operators: `and`, `or`, `not`
  • Arithmetic and function calls inside predicates
employees |> filter(fn(row) => row.employees.salary > 50000 and row.employees.department = 'Engineering' ) |> select(employees.id, employees.name)

Joins And Aggregation

Join tables, group rows, compute aggregate columns, filter groups, and order the result.

  • `join`, `left_join`, `right_join`, `full_join`, `cross_join`
  • `count`, `sum`, `avg`, `min`, `max`
  • `having` filters grouped rows
users |> join(orders, on = users.id = orders.user_id) |> group_by(users.id, users.name) |> having(fn(group) => count(*) > 2) |> select(users.name, sum(orders.total) as revenue)

LQL Documentation

Everything needed to install, write, transpile, use in migrations, and wire into editor tooling.

Install

Add the core parser plus the dialect package that will emit SQL for your target database.

dotnet add package Nimblesite.Lql.Core
dotnet add package Nimblesite.Lql.Postgres
dotnet add package Nimblesite.Lql.SqlServer
dotnet add package Nimblesite.Lql.SQLite
PackageUse
Nimblesite.Lql.CoreParser, AST, statement conversion.
Nimblesite.Lql.PostgresToPostgreSql() extension.
Nimblesite.Lql.SqlServerToSqlServer() extension.
Nimblesite.Lql.SQLiteToSQLite() extension.

Programmatic API

Parse once, then call the dialect extension. Errors are returned as `Result<T,E>` values.

using Nimblesite.Lql.Core;
using Nimblesite.Lql.Postgres;
using Nimblesite.Sql.Model;
using Outcome;

var lql = "Users |> filter(fn(row) => row.Users.Age > 21) |> select(Users.Name)";
var statementResult = LqlStatementConverter.ToStatement(lql);

if (statementResult is Result<LqlStatement, SqlError>.Ok<LqlStatement, SqlError> ok)
{
    Result<string, SqlError> sql = ok.Value.ToPostgreSql();
}

Syntax

A query starts with a table or binding and flows left-to-right through operations.

ConstructShape
Tableusers
Pipelineusers |> select(users.id)
Columnorders.total
Lambdafn(row) => row.users.active = true
Aliasusers.name as display_name
Let bindinglet active = users |> filter(...)
let active_users =
    users |> filter(fn(row) => row.users.status = 'active')

active_users
|> select(active_users.id, active_users.name)
|> order_by(active_users.name asc)
|> limit(50)

Pipeline Operations

OperationPurpose
select(cols...)Project columns or computed expressions.
filter(fn(row) => ...)Filter rows before grouping.
join(table, on = ...)Inner join to another source.
left_join(table, on = ...)Preserve rows from the left source.
group_by(cols...)Group result rows.
having(fn(group) => ...)Filter grouped rows.
order_by(col asc|desc)Sort the result.
limit(n) / offset(n)Page the result.
union(query) / union_all(query)Combine compatible result sets.
insert(target)Insert query output into another table.

Expressions And Functions

Expressions can compare values, combine predicates, compute columns, and call mapped SQL functions.

GroupExamples
Comparison=, !=, >, <, >=, <=
Logicaland, or, not
Math+, -, *, /, round, abs
Aggregatecount, sum, avg, min, max
Windowrow_number, rank, dense_rank, lag, lead
Sessioncurrent_setting('app.tenant_id')::uuid for PostgreSQL migration/RLS shapes.

SQL Dialects

LQL keeps query intent stable while the dialect package handles SQL differences such as `LIMIT`, `TOP`, booleans, and function names.

users
|> filter(fn(row) => row.users.age > 18)
|> select(users.name, users.email)
|> order_by(users.name asc)
|> limit(10)
DialectGenerated paging shape
PostgreSQLORDER BY users.name ASC LIMIT 10
SQLiteORDER BY users.name ASC LIMIT 10
SQL ServerSELECT TOP 10 ... ORDER BY users.name ASC

Migrations, RLS, And `bodyLql`

DataProvider migrations use LQL where schema logic must be portable or generated consistently. Use `usingLql` and `withCheckLql` for RLS policies, and `bodyLql` for PostgreSQL function bodies that should be transpiled instead of handwritten.

tables:
  - name: documents
    columns:
      - name: id
        type: uuid
        primaryKey: true
      - name: tenant_id
        type: uuid
      - name: owner_id
        type: uuid
    rls:
      enabled: true
      policies:
        - name: tenant_documents
          command: all
          usingLql: tenant_id = current_setting('app.tenant_id')::uuid
          withCheckLql: owner_id = current_setting('app.user_id')::uuid

functions:
  - name: current_tenant_id
    schema: app
    returns: uuid
    language: sql
    bodyLql: current_setting('app.tenant_id')::uuid

For PostgreSQL, `current_setting('key')` is rewritten to the null-tolerant `current_setting('key', true)` form before SQL is emitted. `body` and `bodyLql` are mutually exclusive.

VS Code, LSP, Database Schema, And AI

The VS Code extension runs the Rust `lql-lsp` language server for `.lql` files.

FeatureBehavior
DiagnosticsANTLR parse errors, unmatched parentheses, pipe spacing warnings, and unknown function hints.
CompletionsPipeline operations, keywords, functions, `let` bindings, table names, and columns.
Database configSet `lql.database.connectionString`, `LQL_CONNECTION_STRING`, or `DATABASE_URL` for schema-aware completions and hovers.
AI completionsOptional model completions are merged after schema and keyword results and bounded by timeout.
{
  "lql.database.connectionString": "Host=localhost;Database=app;Username=postgres;Password=secret",
  "lql.ai.provider": "ollama",
  "lql.ai.endpoint": "http://localhost:11434/api/generate",
  "lql.ai.model": "qwen2.5-coder:1.5b"
}

F# Type Provider

The type provider validates LQL at compile time and exposes the original query plus generated SQLite SQL.

<PackageReference Include="Nimblesite.Lql.TypeProvider.FSharp" Version="*" />
open Nimblesite.Lql.Core.TypeProvider

type ActiveUsers =
    LqlCommand<"Users |> filter(fn(row) => row.Users.Status = 'active') |> select(*)">

let query = ActiveUsers.Query
let sql = ActiveUsers.Sql

Run The Transpiler

The Blazor app is now scoped to the interactive transpilation demo. Use it to paste LQL and inspect PostgreSQL or SQL Server output.