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
<PackageReference Include="Palm.SurrealDB.FSharp.TypeProviders" Version="0.2.1" />
<PackageVersion Include="Palm.SurrealDB.FSharp.TypeProviders" Version="0.2.1" />
<PackageReference Include="Palm.SurrealDB.FSharp.TypeProviders" />
paket add Palm.SurrealDB.FSharp.TypeProviders --version 0.2.1
#r "nuget: Palm.SurrealDB.FSharp.TypeProviders, 0.2.1"
#:package Palm.SurrealDB.FSharp.TypeProviders@0.2.1
#addin nuget:?package=Palm.SurrealDB.FSharp.TypeProviders&version=0.2.1
#tool nuget:?package=Palm.SurrealDB.FSharp.TypeProviders&version=0.2.1
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, andFSharp.TypeProviders.SDKis vendored as source rather than referenced (aPackageReferencefails every consumer build witherror FS3049). - Getter quotations name only BCL and
FSharp.Coretypes. 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 throwsMethodAccessExceptionat 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
objcarries 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.objectOfinPalm.SurrealDB.FSharpalready covers writes. geometryandrangehave no provided type.SurrealTypeSyntaxrefuses them, so a table using one cannot be provided. The bridge still carries such values through untouched.
Related projects
| 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
- F# DSL article — the DSL, and the provider's place in it
docs/superpowers/plans/planx2-typeproviders.md— the plan, with every design decision and the measurement behind it
| Product | Versions 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. |
-
net10.0
- FSharp.Core (>= 10.1.301)
- Palm.SurrealDB.Net.Abstractions (>= 0.2.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.