Palm.SurrealDB.FSharp.TypeProviders 0.2.1

dotnet add package Palm.SurrealDB.FSharp.TypeProviders --version 0.2.1
                    
NuGet\Install-Package Palm.SurrealDB.FSharp.TypeProviders -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.FSharp.TypeProviders" 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.FSharp.TypeProviders" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Palm.SurrealDB.FSharp.TypeProviders" />
                    
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.FSharp.TypeProviders --version 0.2.1
                    
#r "nuget: Palm.SurrealDB.FSharp.TypeProviders, 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.FSharp.TypeProviders@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.FSharp.TypeProviders&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Palm.SurrealDB.FSharp.TypeProviders&version=0.2.1
                    
Install as a Cake Tool

Palm.SurrealDB.FSharp.TypeProviders

An erasing F# type provider over a SurrealDB schema, for the Palm.SurrealDB .NET ecosystem. It reads your database's schema at compile time — from a committed JSON snapshot, or live over HTTP — and provides types whose table and field names the compiler checks:

open Palm.SurrealDB.FSharp.TypeProviders

type Db = SurrealProvider<Snapshot="schema.json">

Db.Tables.person.TableName      // "person"
Db.Tables.person.Fields.age     // "age" — feeds the F# DSL, which takes field names as strings

let rows = SurrealRows.materialise schema 0 response
let ada = Db.Tables.person.Row rows.[0]

printfn "%s is %d" ada.name ada.age   // string and int64, from the schema

Rename age in the database, refresh the snapshot, rebuild, and the compiler says so:

error FS0039: The type 'Row' does not define a field, constructor, or member named 'age'.
Maybe you want one of the following:   years

No code generation step, no generated files to check in, and no obj in sight.

Why this project exists

The F# DSL takes field names as strings (Surql.Field "age"), because that is what SurrealQL is. Strings are not checked by anything: a renamed column is a runtime surprise, and usually a quiet one — a query that returns no rows rather than one that fails. This provider closes that gap without asking you to hand-maintain a parallel set of record types that can drift from the database just as easily.

It is erasing, not generative: a provided row is the IReadOnlyDictionary<string, obj> the runtime bridge produced. No wrapper type exists at run time, so there is nothing to allocate, nothing to keep in sync, and no generated assembly.

Installation

<ItemGroup>
  <PackageReference Include="Palm.SurrealDB.FSharp.TypeProviders" Version="..." />
  
  <CustomAdditionalCompileInputs Include="schema.json" />
</ItemGroup>

Targets net10.0. See the root README for the ecosystem-wide build, test and quality-gate commands.

The schema snapshot

The snapshot is a small JSON document of this project's own, not a SurrealDB response verbatim. Each field's kind is the SurrealQL type expression exactly as INFO FOR TABLE … STRUCTURE reports it:

{
  "tables": [
    { "name": "person", "schemafull": true, "fields": [
      { "name": "name", "kind": "string", "readonly": false },
      { "name": "age", "kind": "int", "readonly": false },
      { "name": "joined", "kind": "datetime", "readonly": true },
      { "name": "nickname", "kind": "none | string", "readonly": false },
      { "name": "tags", "kind": "array<string>", "readonly": false }
    ] }
  ]
}

Note that SurrealDB normalises option<T> to the union none | T, so that is how an optional field appears.

There is no scaffolding command yet, and writing the snapshot is currently a manual step. The design-time component can fetch a live schema and render exactly this document (SchemaModel.renderSnapshot), but that component is deliberately not referenceable by consumers — it is netstandard2.0, dependency-free, and loaded into the compiler's process — so the rendering path is not reachable from your project. Until a dotnet tool exists, write the file by hand, or point the provider at the live database and keep the snapshot for offline builds.

A committed snapshot is the default path on purpose: a build must not require a running database. Requiring one would make every fresh clone, every CI leg and every offline build fail at compile time, which is a far worse property than a snapshot that has gone stale.

For a live read, pass Url, Namespace and Database instead of Snapshot. Credentials are named by environment variable, never passed as literals:

type Db =
    SurrealProvider<Url="http://127.0.0.1:8000", Namespace="test", Database="test",
                    UserVariable="SURREALDB_USER", PasswordVariable="SURREALDB_PASS">

UserVariable and PasswordVariable default to SURREALDB_USER and SURREALDB_PASS. They are the names of variables to read, so a credential cannot end up committed in a source file.

Snapshot changes and incremental builds

A snapshot edit alone does not trigger a rebuild. MSBuild decides a project is up to date from CoreCompile's declared inputs, and a JSON file the provider happens to read is not one of them — so the compiler never re-runs, the previously provided types survive, and the build stays green against a schema that no longer matches.

Declaring the snapshot as a compile input fixes it exactly:

<CustomAdditionalCompileInputs Include="schema.json" />

Both halves are measured in PackagingConsumptionTests: with the declaration, editing only the snapshot fails the next incremental build; without it, the same edit builds clean.

This is also the limit of what compile-time checking can do. A snapshot that is out of date with the database is undetectable at compile time by construction — refresh it in the same change as the migration, and let SurrealRows.materialise catch what gets past that.

What each field is provided as

Schema kind Provided type Notes
bool / string / float / decimal bool / string / float / decimal
int int64 SurrealValue.GetInt64 returns long; int32 would truncate.
datetime DateTimeOffset Not DateTime — GetDateTime() returns DateTimeOffset, and mapping to DateTime silently drops the offset.
uuid Guid
bytes byte[]
duration string Canonical spelling; read it back with SurrealDuration.Parse.
record<t> string Canonical table:key; read it back with RecordId.Parse.
object IReadOnlyDictionary<string, obj>
array<T> / set<T> T[] A set is provided as an array — distinctness is a database constraint, not a shape the read side can enforce.
bare array / set obj[] No element type was declared, so the elements carry a widening note.
number obj Not float. number is SurrealDB's numeric SUPERTYPE: one column holds int, float and decimal values unchanged. Declare int, float or decimal to get a checked field. Carries a widening note.
none \| T T option An omitted key and an explicit NONE both read as None. null folds into none, so int \| null is int64 option.
any, or a union of unrelated types obj Carries a widening note in the property's XML doc: the field is not schema-checked.

duration and record map to strings because SurrealDuration and RecordId live in a net10.0 assembly the design-time component must never bind. The substitutes are lossless, not convenient: TimeSpan is the tempting choice for duration and is measurably wrong, since SurrealDuration carries nanoseconds and TimeSpan ticks are 100ns.

A field the mapping cannot represent at all is refused, naming the table and field, rather than quietly provided as obj.

Reading rows

The provider gives you types; SurrealRows turns a query response into values of them.

open Palm.SurrealDB.FSharp.TypeProviders

let schema =
    { Table = Db.Tables.person.TableName
      Required = List.ofArray Db.Tables.person.RequiredFields
      Optional = List.ofArray Db.Tables.person.OptionalFields }

let rows = SurrealRows.materialise schema 0 response
let people = rows |> Seq.map Db.Tables.person.Row

materialise checks each row against the schema the types came from, so a stale snapshot fails there — where the table name and the expected field list are both in hand — rather than later at a getter, as a KeyNotFoundException naming a string key. A failed statement raises rather than yielding an empty result set, because "the server rejected the query" and "no records matched" are indistinguishable to a caller reading a count.

Fields the snapshot does not know about are carried through, not rejected: a snapshot older than the database is the expected condition.

How it is built

Three assemblies, and the split is not optional:

Assembly Target Role
Palm.SurrealDB.FSharp.TypeProviders net10.0 The runtime bridge (SurrealRows), and the TypeProviderAssembly attribute that points the compiler at the component below. This is the only one you reference.
Palm.SurrealDB.FSharp.TypeProviders.DesignTime netstandard2.0 Runs inside the F# compiler's process: schema reader, type mapping, provided-type tree, getter quotations. Shipped at tools/fsharp41/netstandard2.0/ in the package, never in lib/.
…TypeProviders.Tests net10.0 xUnit v3, including a packaged-consumer gate.

Two rules govern the design-time component, and neither is enforced by any compiler:

  • It has no dependencies at all — not System.Text.Json, nothing. It is loaded into the compiler's process and everything it needs must resolve there. The JSON reader is hand-rolled for this reason, and FSharp.TypeProviders.SDK is vendored as source rather than referenced (a PackageReference fails every consumer build with error FS3049).
  • Getter quotations name only BCL and FSharp.Core types. A quotation is not compiled in the component; it is spliced into your program and resolved against your references. Naming anything else builds green everywhere and throws MethodAccessException at your line, at run time.

RowExprTests walks the built expressions and asserts both, because rules this invisible cannot be maintained by reading.

Limitations

  • IDE behaviour is unverified. The gates here run dotnet build. Visual Studio and Rider IntelliSense are not tested — this is a macOS development environment. One concrete mechanism to expect: the schema is cached per provider instance and never invalidated, so a long-lived IDE language service will pin the first-read schema until it restarts. Command-line builds are unaffected — each one is a fresh compiler process.
  • Widening notes reach editors only. A field provided as obj carries its note as XML doc, which is a tooltip. A command-line-only consumer never sees it.
  • No provided query methods. The provider gives you names and row types; you still write the query with the F# DSL. Executing against the client from a provided member is out of scope for v1.
  • Read side only. Record.objectOf in Palm.SurrealDB.FSharp already covers writes.
  • geometry and range have no provided type. SurrealTypeSyntax refuses them, so a table using one cannot be provided. The bridge still carries such values through untouched.
Project Purpose
Palm.SurrealDB.FSharp The F# DSL these field literals feed.
Palm.SurrealDB.Net.Abstractions SurrealValue, RecordId, SurrealDuration — what the bridge converts from.

Further reading

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 103 9/5/2026
0.2.0 113 9/3/2026