Palm.SurrealDB.Net.SurrealQL 0.2.1

dotnet add package Palm.SurrealDB.Net.SurrealQL --version 0.2.1
                    
NuGet\Install-Package Palm.SurrealDB.Net.SurrealQL -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Palm.SurrealDB.Net.SurrealQL" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Palm.SurrealDB.Net.SurrealQL" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Palm.SurrealDB.Net.SurrealQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Palm.SurrealDB.Net.SurrealQL --version 0.2.1
                    
#r "nuget: Palm.SurrealDB.Net.SurrealQL, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Palm.SurrealDB.Net.SurrealQL@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Palm.SurrealDB.Net.SurrealQL&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Palm.SurrealDB.Net.SurrealQL&version=0.2.1
                    
Install as a Cake Tool

Palm.SurrealDB.Net.SurrealQL

The SurrealQL abstract syntax tree, emitter, parser, formatter, and statement builders for the Palm.SurrealDB .NET ecosystem. This is the only sanctioned way to produce SurrealQL query text anywhere in the ecosystem — never build query strings by concatenation. Everything downstream (the LINQ provider, the F# DSL, the client's raw-query escape hatch) routes through the AST defined here and emits via SurqlEmitter.

The project targets net10.0 only and depends on exactly one sibling package, Palm.SurrealDB.Net.Abstractions (for SurrealValue, RecordId, SurrealDuration, and related value types). It has no other dependencies, and nothing in SurrealQL may reference the client or the transport layers — dependencies flow downward only.

Why this project exists

Hand-built SurrealQL strings are an injection vector and a maintenance trap: escaping rules for identifiers, record ids, durations, and geometry literals are easy to get wrong, and string templates silently drift from the server's actual grammar. This project makes parameterized, injection-safe query construction a first-class concern:

  • Every AST node validates its own invariants at construction (non-empty targets, mutually exclusive clauses, valid identifier/parameter name shapes, etc.) — malformed queries fail fast, before any text is emitted.
  • SurqlEmitter.Emit binds every literal to a generated $pN parameter, so query text sent to the server never embeds untrusted values directly.
  • The parser is round-trip verified against the emitter (see tests/Palm.SurrealDB.Net.SurrealQL.Tests) so the AST and the grammar it claims to represent stay in sync — the emitter and parser precedence tables are required to agree.

Installation

dotnet add package Palm.SurrealDB.Net.SurrealQL

This project is packable and ships as the Palm.SurrealDB.Net.SurrealQL NuGet package. An earlier version of this section said the opposite, citing a true observation — the .csproj sets neither PackageId nor IsPackable — in support of a false conclusion: packability comes from the SDK default and Directory.Build.props, and the only projects that opt out are the three that set IsPackable=false. PackableSetGuardTests pins all 25 packable ids, and this is one of them.

Targets net10.0 only. The package is proprietary and sets PackageRequireLicenseAcceptance, so NuGet prompts before installing — see LICENSE.

Nothing is on NuGet.org until the first release runs (see docs/RELEASING.md); until then, reference the project directly:

<ItemGroup>
  <ProjectReference Include="path/to/src/Palm.SurrealDB.Net.SurrealQL/Palm.SurrealDB.Net.SurrealQL.csproj" />
</ItemGroup>

Building queries with the AST + builders

The typical path is: use Surql's static factory methods to build expressions, hand them to a fluent statement builder (SelectBuilder, CreateBuilder, UpdateBuilder, DeleteBuilder, InsertBuilder, RelateBuilder), then either call Build() to get the immutable SelectStatement/CreateStatement/… AST node, or call ToSurql() directly on the builder to get parameterized text.

using Palm.SurrealDB.Net.SurrealQL;
using static Palm.SurrealDB.Net.SurrealQL.Surql;

// SELECT name, age FROM person WHERE age >= 18 ORDER BY name LIMIT 10
SurqlText query = Select("name", "age")
    .From(Table("person"))
    .Where(Ge(Field("age"), Value(18)))
    .OrderBy(new OrderByField(Field("name")))
    .Limit(Value(10))
    .ToSurql();

// query.Text       == "SELECT name, age FROM person WHERE age >= $p0 ORDER BY name LIMIT $p1"
// query.Parameters == { ["p0"] = SurrealValue.From(18), ["p1"] = SurrealValue.From(10) }

await client.QueryAsync(query.Text, query.Parameters);

Every builder is an immutable record; every fluent method returns a new builder (this with { ... }), so builders can be safely reused as templates and branched:

var baseQuery = Surql.SelectAll().From(Table("person"));
var adults = baseQuery.Where(Ge(Field("age"), Value(18)));
var minors = baseQuery.Where(Lt(Field("age"), Value(18)));

Build() on any builder runs the same construction-time validation as calling the AST node's constructor directly — invalid combinations (e.g. SELECT VALUE with more than one field, SPLIT and GROUP together, a CREATE with a MERGE data clause) throw ArgumentException immediately rather than producing malformed SurrealQL.

Surql static factory reference

Surql (in Palm.SurrealDB.Net.SurrealQL) is the single entry point for building expressions and starting statement builders — used uniformly by this project, the LINQ provider, and the F# DSL.

Category Members
Expression construction Field(params string[] path), Param(string name), Table(string name), Value(SurrealValue\|long\|double\|decimal\|string\|bool), Null, None, Record(RecordId), Record(string table, string key), Array(params SurqlExpression[]), Object(params (string Key, SurqlExpression Value)[]), Function(string name, params SurqlExpression[] args)
Operator helpers Not, Negate, And, Or, Eq, ExactEq, Ne, Lt, Le, Gt, Ge, Add, Sub, Mul, Div, Contains, In
SET-operation helpers Assign(IdiomExpression target, SurqlExpression value), Incr(...), Decr(...)
Statement builder entry points SelectAll(), Select(params string[] fieldNames), Select(IEnumerable<SelectField>), SelectValue(SurqlExpression\|string), Create(params SurqlExpression[] targets), Update(params SurqlExpression[] targets), Upsert(params SurqlExpression[] targets), Delete(params SurqlExpression[] targets), Insert(string table, SurqlExpression values), Relate(SurqlExpression from, SurqlExpression edge, SurqlExpression to)

The operator helper list above covers the common cases; for the remaining SurqlBinaryOperator / SurqlUnaryOperator values (see The AST below) construct new BinaryExpression(...) / new UnaryExpression(...) directly.

Statement builders

All six builders are sealed record types with internal constructors — obtain one only via Surql's entry points. Every builder exposes Build() (returns the immutable statement AST node), ToSurql() (build + SurqlEmitter.Emit, parameterized), and ToSurqlInline() (build + SurqlEmitter.EmitInline — see the Emitting section for when this is and is not safe).

Builder Entry point Notable fluent methods
SelectBuilder Surql.SelectAll(), Surql.Select(...), Surql.SelectValue(...) From, Only, Omit, WithIndex, WithNoIndex, Where, SplitOn, GroupAll, GroupBy, OrderBy, OrderByRand, Limit, Start, Fetch, Timeout, Explain, ExplainFull
CreateBuilder Surql.Create(targets) Only, Content, Set, Return, Timeout
UpdateBuilder Surql.Update(targets), Surql.Upsert(targets) Only, Content, Merge, Patch, Replace, Set, Unset, Where, Return, Timeout
DeleteBuilder Surql.Delete(targets) Only, Where, Return, Timeout
InsertBuilder Surql.Insert(table, values) Relation, Ignore, OnDuplicateKeyUpdate, Return
RelateBuilder Surql.Relate(from, edge, to) Only, Content, Set, Return, Timeout

CreateBuilder/RelateBuilder restrict their Content/Set data clause to CONTENT/SET only (matching the server grammar); UpdateBuilder accepts all six data-clause kinds. CreateStatement and RelateStatement never emit PARALLEL; DeleteStatement and UpdateStatement never emit PARALLEL either — that keyword is not currently modeled.

Longer example — building each statement kind:

using static Palm.SurrealDB.Net.SurrealQL.Surql;

// CREATE person CONTENT { name: $p0, age: $p1 } RETURN AFTER
Create(Table("person"))
    .Content(Object(("name", Value("Ada")), ("age", Value(30))))
    .Return(ReturnClause.After)
    .ToSurql();

// UPDATE person SET age += $p0 WHERE name = $p1
Update(Table("person"))
    .Set(Incr(Field("age"), Value(1)))
    .Where(Eq(Field("name"), Value("Ada")))
    .ToSurql();

// DELETE person WHERE age < $p0
Delete(Table("person")).Where(Lt(Field("age"), Value(18))).ToSurql();

// INSERT INTO person { name: $p0 } RETURN AFTER
Insert("person", Object(("name", Value("Grace"))))
    .Return(ReturnClause.After)
    .ToSurql();

// RELATE person:ada->knows->person:grace SET since = $p0
Relate(Record("person", "ada"), Table("knows"), Record("person", "grace"))
    .Set(Assign(Field("since"), Value(2024)))
    .ToSurql();

The AST

The AST lives in Palm.SurrealDB.Net.SurrealQL.Ast. Every node is an immutable record deriving (directly or indirectly) from SurqlNode, the visitor root:

public abstract record SurqlNode
{
    public abstract TResult Accept<TResult>(ISurqlVisitor<TResult> visitor);
}

public abstract record SurqlExpression : SurqlNode;
public abstract record SurqlStatement : SurqlNode;

SurqlNode constructors are private protected — only the types defined in this project can derive new node kinds. Node families are dispatched through ISurqlVisitor<TResult>, which SurqlEmitter implements internally; it is a public interface so other projects (the LINQ provider's translator, future tooling) can also visit the tree. Note: the interface is expected to grow additional Visit* members while the library is pre-1.0 — treat it as unstable across minor versions.

Statements (SurqlStatement)

Type Emits Key members
SelectStatement SELECT ... Targets, Fields, SelectAll, IsValue, Omit, IsOnly, With, Where, Split, Group, OrderBy, Limit, Start, Fetch, Timeout, Explain
CreateStatement CREATE ... Targets, IsOnly, Data (CONTENT/SET only), Return, Timeout
UpdateStatement UPDATE/UPSERT ... (Kind: UpdateKind) Targets, IsOnly, Data (any of the six kinds), Where, Return, Timeout
DeleteStatement DELETE ... Targets, IsOnly, Where, Return, Timeout
InsertStatement INSERT [RELATION] [IGNORE] INTO table values [ON DUPLICATE KEY UPDATE ...] Table, Values, IsRelation, IsIgnore, OnDuplicateKeyUpdate, Return
RelateStatement RELATE [ONLY] from->edge->to [CONTENT\|SET] [RETURN] [TIMEOUT] From, Edge, To, IsOnly, Data (CONTENT/SET only), Return, Timeout
ReturnStatement RETURN expr Value
LetStatement LET $name = expr Name, Value
TransactionBlock BEGIN; stmt; ...; COMMIT Statements (non-empty)

InsertStatement supports only the object / array-of-objects value form (INSERT INTO table {...} or INSERT INTO table [{...}, {...}]); the columnar (columns) VALUES (tuples) form is deferred to a later phase.

Expressions (SurqlExpression)

Type Emits Key members
LiteralExpression a bound $pN parameter (or inline literal) Value: SurrealValue
ParameterExpression $name Name (validated [A-Za-z_][A-Za-z0-9_]*)
UnaryExpression !x, -x, +x Operator: SurqlUnaryOperator, Operand
BinaryExpression x OP y Operator: SurqlBinaryOperator, Left, Right
CastExpression <type> expr TypeName (validated), Operand
RangeExpression start..end, start>.., ..=end, etc. Start, IsStartExclusive, End, IsEndInclusive
ArrayExpression [a, b, ...] Items: ImmutableArray<SurqlExpression>
ObjectExpression { k: v, ... } (write order preserved) Entries: ImmutableArray<ObjectEntry>
FunctionCallExpression segment::segment(args) Name (validated), Arguments
IdiomExpression field/index/graph path, e.g. address.city, ->knows->person Head (optional), Parts: ImmutableArray<IdiomPart>
TableExpression a table name (never parameterized — syntax, not a value) Name
RecordIdRangeExpression table:start..end (bounds must be literals) Table, Range: RangeExpression
RecordIdGenerateExpression table:rand() / table:ulid() / table:uuid() Table, Generator: RecordIdGenerator
SubqueryExpression (SELECT ...) — a statement used as an expression Statement: SurqlStatement

ObjectExpression intentionally preserves write order, unlike SurrealValue.FromObject's unordered dictionary — object literals in query text are syntax, not a value type.

Idiom parts (IdiomPart, used inside IdiomExpression.Parts)

Type Emits Notes
FieldPart(string Name) .field (or bare, if leading) Escaped via SurqlIdent when not a bare identifier
IndexPart(SurqlExpression Index) [expr]
LastPart [$] Last array element
AllPart [*]
FilterPart(SurqlExpression Condition) [WHERE cond]
OptionalPart .? Safe navigation — yields NONE if the preceding segment is absent
DestructurePart(IEnumerable<string> fields) .{ f1, f2 } Requires at least one field
GraphPart(GraphDirection Direction, string EdgeTable) ->edge, <-edge, <->edge GraphDirection: Out, In, Both

Operators

SurqlUnaryOperator: Not (!), Negate (-), Plus (+).

SurqlBinaryOperator (32 values) groups into: logical (Or, And, NullCoalesce ??, TernaryFallback ?:), equality (Equal =, NotEqual !=, ExactEqual ==, AnyEqual ?=, AllEqual *=), relational (LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual), arithmetic (Add, Subtract, Multiply, Divide, Remainder, Power), containment (Contains, ContainsNot, ContainsAll, ContainsAny, ContainsNone, Inside (IN), NotInside, AllInside, AnyInside, NoneInside), spatial (Outside, Intersects), and pattern matching (Matches, emitted as @@).

Clauses and supporting records

These are not SurqlNodes themselves (no Accept), but appear as members of statements:

Type Purpose
SelectField(SurqlExpression Expression, string? Alias) A projected field, optionally AS alias
OrderByField(SurqlExpression Field, bool Descending, bool Numeric, bool Collate) One ORDER BY entry
OrderByClause ORDER BY RAND() (.Rand) or ORDER BY field, ... (.ByFields(...))
GroupClause GROUP ALL (.All) or GROUP BY field, ... (.ByFields(...))
WithClause WITH NOINDEX (.NoIndex) or WITH INDEX name, ... (.Index(...))
ExplainMode (enum) Explain, ExplainFull
DataClause (abstract) Base of the six DML data-clause kinds below
ContentClause(SurqlExpression Value) CONTENT value
MergeClause(SurqlExpression Value) MERGE value
PatchClause(SurqlExpression Value) PATCH value (a JSON-Patch operations array)
ReplaceClause(SurqlExpression Value) REPLACE value
SetClause(IEnumerable<SetOperation> Operations) SET target op value, ...
UnsetClause(IEnumerable<IdiomExpression> Fields) UNSET field, ...
SetOperation(IdiomExpression Target, SetOperator Operator, SurqlExpression Value) One assignment; SetOperator: Assign (=), Add (+=), Subtract (-=)
ReturnClause RETURN NONE\|BEFORE\|AFTER\|DIFF\|NULL\|fields\|VALUE expr — factories .None, .Before, .After, .Diff, .Null, .OfFields(...), .OfValue(...)
ObjectEntry(string Key, SurqlExpression Value) One ObjectExpression entry
RecordIdGenerator (enum) Rand, Ulid, Uuid

A statement holds at most one DataClause, which is what makes the server's "SET and UNSET are mutually exclusive" rule structural rather than something callers must remember. CreateStatement and RelateStatement further restrict their Data to ContentClause/SetClause at construction (any other kind throws ArgumentException).

Emitting

SurqlEmitter (Palm.SurrealDB.Net.SurrealQL) is a static partial class with two public entry points:

public static SurqlText Emit(SurqlNode node);       // parameterized — safe for untrusted input
public static string EmitInline(SurqlNode node);     // literals embedded inline

Emit walks the tree once, binding every LiteralExpression to a generated $p0, $p1, … parameter (in order of first appearance) and returning a SurqlText — a record SurqlText(string Text, IReadOnlyDictionary<string, SurrealValue> Parameters) — ready to pass straight to client.QueryAsync(text.Text, text.Parameters). Generated pN names never collide with user-supplied $pN parameters already present in the tree (the emitter pre-scans the tree for reserved names).

EmitInline embeds every literal and identifier directly into the returned string via SurqlLiteralWriter/SurqlIdent. EmitInline (and ToSurqlInline() on the builders) is tooling/tests only — never call it with untrusted input. This is a hard project rule (see the root AGENTS.md and src/AGENTS.md): identifier-emitting paths route through SurqlIdent.Escape, but inline mode still trades the parameter boundary for convenience, and geometry literals cannot be represented inline at all (EmitInline throws SurrealDbSerializationException if the tree contains one).

Both entry points throw ArgumentNullException when node is null.

Parsing

SurqlParser (Palm.SurrealDB.Net.SurrealQL) is the inverse of SurqlEmitter — it turns SurrealQL text back into the AST:

public static SurqlExpression ParseExpression(string text);
public static SurqlStatement ParseStatement(string text);

ParseExpression currently supports the full expression subset: literals, parameters, field idioms (including index/filter/graph/destructure/optional parts), function calls, arrays, objects, casts, ranges, and the full unary/binary operator set. ParseStatement currently supports SELECT; other statement kinds are added incrementally as the AST/emitter grow to cover them. Both throw SurqlParseException (carrying 1-based Line/Column) when the text is not valid or not fully consumed, and ArgumentNullException when text is null.

The parser exists primarily to round-trip verify the emitter (every emitter-producible AST node must appear in the parser's round-trip test cases — see the project's LESSONS-LEARNED.md "oracle blind spot" note) rather than as a general-purpose SurrealQL frontend; the lexer (SurqlLexer), token model (SurqlToken), and expression/statement parser internals are all internal.

Formatting

SurqlFormatter (Palm.SurrealDB.Net.SurrealQL) formats an AST node with minimal parentheses, driven by the same operator precedence table the parser enforces (loose→tight: nullish < or < and < equality < relation < add/sub < mul/div < power < prefix < range):

public static string Format(SurqlNode node);

This differs from SurqlEmitter.EmitInline, which fully parenthesizes every non-atomic operand for version-robust correctness regardless of precedence. SurqlFormatter.Format is for producing readable output (logs, debugging, docs) and assumes the SurrealDB 3.1.4 precedence the parser verifies; like EmitInline, its output is not injection-safe — prefer SurqlEmitter.Emit for anything that reaches the server with untrusted values. Throws ArgumentNullException when node is null.

  • Palm.SurrealDB.Net.Abstractions — this project's only dependency: SurrealValue, RecordId, SurrealDuration, and the other core value types used throughout the AST.
  • Palm.SurrealDB.Net.Serialization — CBOR codec and the ISurrealTypeMapper name-mapping source of truth.
  • Palm.SurrealDB.Net.Protocol — WebSocket/HTTP RPC transport.
  • Palm.SurrealDB.Net — the client (SurrealDbClient, QueryAsync, CRUD, live queries); consumes SurqlText produced here.
  • Palm.SurrealDB.Net.Linq — the IQueryable<T> LINQ provider; translates LINQ expression trees into this project's AST (SurqlExpression/SurqlStatement) and emits them via the same SurqlEmitter.Emit path documented above.
  • Palm.SurrealDB.FSharp — an idiomatic F# DSL that wraps this project's AST with value/operator modules and statement computation expressions.
  • Repository root README — ecosystem overview, quick start, and build/test instructions.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (7)

Showing the top 5 NuGet packages that depend on Palm.SurrealDB.Net.SurrealQL:

Package Downloads
Palm.SurrealDB.EntityFrameworkCore.Diagnostics

Diagnostics, event definitions and command interception for the Palm.SurrealDB EF Core provider.

Palm.SurrealDB.Net.Linq

LINQ provider for SurrealDB — translates expression trees to SurrealQL via the Palm.SurrealDB.Net.SurrealQL AST.

Palm.SurrealDB.EntityFrameworkCore

Entity Framework Core provider for SurrealDB, built on the Palm.SurrealDB.Net client and SurrealQL AST.

Palm.SurrealDB.EntityFrameworkCore.Migrations

Convergent schema management (EnsureSurrealSchema / GetSurrealSchemaDiff / GenerateSurrealSchemaScript) for the SurrealDB EF Core provider. Not EF migrations — SurrealDB has no relational migration model (D1).

Palm.SurrealDB.FSharp

Idiomatic F# DSL over the SurrealQL AST for the Palm.SurrealDB ecosystem.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 170 9/5/2026
0.2.0 173 9/3/2026