Repository navigation
docs: add ADR 0001 for the FSharp.Azure.Cosmos.Quotations architecture - #34
Open
xperiandri wants to merge 42 commits into
Open
xperiandri wants to merge 42 commits into
xperiandri wants to merge 42 commits into
Conversation
Contributor
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request. Check if the Files changed in this pull request are included in default exclusions.
This was referenced Oct 2, 2026
Merged
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
from
October 3, 2026 01:38
e1f35eb to
8d9ffe6
Compare
This was referenced Oct 5, 2026
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
from
October 10, 2026 00:06
ff19ea3 to
9767e66
Compare
This was referenced Oct 10, 2026
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
2 times, most recently
from
October 10, 2026 08:26
7557194 to
6820cc6
Compare
4 of 6 tasks
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
5 times, most recently
from
October 11, 2026 09:03
7b47902 to
3182e04
Compare
This was referenced Oct 11, 2026
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
from
October 11, 2026 11:03
3182e04 to
176dffd
Compare
This was referenced Oct 11, 2026
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
3 times, most recently
from
October 11, 2026 17:52
2b5ba34 to
8965bd9
Compare
The maintainer decided on 2026-10-10 that neither FSharp.Azure.Cosmos nor the Quotations package declares Newtonsoft.Json: the build check of the SDK already asks every consumer to reference it and leaves the version to the consumer. The package table and the package list lose the dependency, and open question 16 is answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hedgehog MSTest adapter of #43 is proposed upstream as hedgehogqa/fsharp-hedgehog#487. The record notes how the proposal differs from the in-repository copy (net8.0 and F# 8, upstream's documentation in the shared files, VSTest), that MSTest 4.2 is the adapter's lowest version, and that one test of #43 stops compiling on MSTest 4.5.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer's reviews of 2026-10-10 changed several things the record describes, and it now says so where each belongs. The Hedgehog MSTest adapter (#43): the in-repository copy follows the coding guidelines of this repository, while the upstream pull request keeps upstream's style in a copy of its own, so a change of behaviour goes into both; an invocation that MSTest could not run stops the run instead of being shrunk; the cancellation token is checked after every invocation, whatever its outcome, which the automatic review of #50 asked for; Odd and Even stay inside their range; DateOnly and TimeOnly get generators, upstream through hedgehogqa/fsharp-hedgehog#488. The open question of the adapter's code style is answered, formatting and nullness checking included: the two projects are formatted with Fantomas and build with nullness checking on, and only the verbatim port keeps upstream's settings. Collections (section 4): the shared collection functions of #47 that return voption and struct tuples, tested with properties in #50, and the rule of one module per pipeline of #49. Build (section 5): why FAKE's DotNet.test cannot run the test applications, and fsprojects/FAKE#2903, which adds DotNet.testMTP. The spec model (sections 6 and 16): EquatableArray and the ImmutableArray patterns are #48 and the model itself #42; the validator returns an ImmutableArray of struct errors. Golden baselines (section 13): a literal type provider was evaluated on the question asked in #46 and rejected, with the reasons. Tests (section 22): test category attributes sorted by name, in #51 with an instruction scoped to those files, Pascal-case assertion functions and ImmutableArray for shared test data. Two more answers of the same day: the Windows emulator lane of #45 runs weekly instead of nightly (section 2), and the shared collection functions of #47 are in the namespace FSharp.Azure.Cosmos instead of the global one (section 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Section 4 said that an F# record or union holding an ImmutableArray loses structural equality. It does not: the equality the compiler generates goes through IStructuralEquatable, which the array implements, and compares the elements. What is true is narrower. The array's own Equals, which EqualityComparer.Default calls, compares references, so a struct tuple, a HashSet with the default comparer and a C# caller see two equal arrays as different. The structural path boxes every element, hashes only the last eight, throws on compare for different lengths, and a default array never equals an empty one. The section now gives these as the reasons for EquatableArray, as the documentation comment of the type in #48 already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The benchmarks of #56 give the cost model of section 11 its first numbers. A quotation literal costs 24 to 75 µs per evaluation inside FSharp.Core, which unpickles the tree every time; the shape key, the plan lookup and the QueryDefinition together cost about 1 µs; a value costs a fraction of a microsecond by reflection and hundreds of times more compiled; a request to the emulator takes 1.4 to 2.1 ms. The record states what follows for the design: the plan cache does not remove the cost of an inline quotation, compiled queries do, and the precompilation tool of Phase 5 stays undecided until the translation itself can be measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#57 makes the SDK's parser the judge of the printer. The record states how it departs from the plan and why: the SDK's generated sources are copied instead of generated again, so no Java is needed; the conversion is F# and sits in a project that never references the SDK, because the SDK assembly has internal types of the same names as the generated parser; and the runtime package stays below 4.10. It also keeps what the parser taught: a sign directly before a digit belongs to the number, and three distinctions of a tree are not in its text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#58 is the first stage of the translator: it brings a quotation into its normal form. The record states where the implementation departs from the design and why: one walk instead of rounds, with recursion found exactly and two limits; substitution with care for a variable that is bound at two nested places, which Expr.Substitute gets wrong; the mutable copy of a struct that the compiler binds; what stays although it has a reflected definition; and the passes that wait for the registry. It also records what the stage costs and where the time goes in FSharp.Core, with the proposal that follows for the plan cache, and how the tests are built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#59 computes the parts of a quotation that read no variable. The record states what the step found: 37 operators of FSharp.Core that reflection cannot call and the functions that stand in for them, the checked operators whose witnesses are unchecked, how enumeration values are computed, why the precomputed forms of the FSharp.Reflection functions are used, and how an exception of the caller's code is reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result of the third step of phase 1 (#60): which values of a quotation are parameters, what its shape key holds, and where the implementation departs from section 11. A literal is a part of the key, a value that the quotation builds is not computed, a branch that is not taken is not computed, and the key is the key of the normal form. Also what the review of #59 changed in the two steps before: the normalizer does not read the branch that a constant condition does not choose, and the evaluator computes the members of a nullable, raises NullReferenceException for a function that is null and computes a coercion as the cast it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cost model says that a quotation literal is unpickled on every evaluation. That is reported to dotnet/fsharp as issue 20764 now, so the ADR names the issue where it states the cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fourth step of phase 1 (#61) has the first two members of IJsonLayout and one implementation, JsonLayout.ofSerializerOptions. The record says where it departs from section 10: the serializer is asked for its contract instead of having its configuration read again, so one function describes System.Text.Json with and without FSharp.SystemTextJson; plain System.Text.Json options are described, not refused; and what the two serializers were found to write for a member without a value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the reviews of #58 to #60 on the evening of 2026-10-11 changed: - the normalizer inlines an inline member of a type, whose definition the compiler keeps for the member itself and not for its form with witnesses, and applies a definition only to arguments it takes; - the evaluator computes a node once, for one part and for a caller that keeps what was computed; - the parameterizer tells a function by a class made of the type of functions too, and Query.constant takes only a value that a literal can be. The counts of tests and of planted defects follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ADR 0001 Hedgehog 2.0.4 cannot generate the two types, and hedgehogqa/fsharp-hedgehog#488 adds the generators upstream. Until a release has them, the adapter of this repository registers the same two for the parameters of a property (#43). The record of the adapter says so, and when the stand-in goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The normalizer reports the outermost construct that a query cannot express also when the rewriting fails inside it, and an inner binding of a variable hides an assignment from the outer one (#58). The layout reads the obsolete setting that ignores null, which both serializers still follow (#61). The counts of tests and planted defects follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every call of a function with a reflected definition gets nodes of its own (#58). The type that a value is declared with decides whether it is a literal, so a nullable and an option are none (#60). In the layout an override has no condition of the declaration it overrides, a property of a record that is not public is not written, and a value whose converter writes null itself is not described (#61). The counts of tests and planted defects follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A receiver that is null is reported after the arguments of the call are computed (#59). A record whose converter an attribute of the application makes is not described, and a read-only member with a rule that code has set is not taken to be left out (#61). One node at two places stays one value and one slot: the record says why, and what would change it. Found while the next step was built, and fixed in #61: a read-only member is a collection, which the serializer writes all the same, by the converter that writes it, and that can be one the member names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call of a function that calls itself stays a call and is an error only when it is left in the normal form and reads the document, so code that never runs may hold one however the constant gets to its condition (#58). The layout is asked about a member together with the type it is read from, because a quotation names an inherited property as its declaring type has it, and a union with a case that is null is left out where a null is left out (#61). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default value of a struct is its zeroed value, for which no constructor runs, also when a struct of another language declares a constructor without parameters (#59). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result of the step that describes how enumerations, tuples, collections and maps are written (#62), and where it departs from the sketch of section 10: every question takes the member, one case says that the layout does not know, an enumeration is written to see how it is written, and a struct tuple that is written as an empty object is an object whose items have no names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record of the layout step gains what the seventh review of it found: the extension data has no absence that the layout describes, a member of a record that is not written has its declared name and is null or left out, and a rule that code sets to null cannot be told from no rule, which stays a known limit. The eighth review, of the values, found three things, two of them in the layout step: only the factory of FSharp.SystemTextJson itself shows the options of a record, every question is answered once also when several threads ask at a time, and an enumeration is asked for every value that has a name, so that a converter which writes them in two ways is not described. A planted defect that no test failed for any more led to two more rules about a resolver of the application, which can make a contract from any converter: a record is described only when the serializer made its contract, and the contract of a member with a converter of its own is asked by the rules of the serializer, which writes such a member with its converter whatever the resolver makes for the type. The counts of tests and planted defects of both steps follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result of the sixth step of phase 1 (#63): IJsonLayout answers how a case of a union is written, as a mark that tells the case and a place for each of its fields, both made of steps from the value of the union. The record says where this departs from the sketch of sections 10 and 20.9, what FSharp.SystemTextJson really does, what is not described, and how the step is tested. The ninth review found two things. A node that a quotation comes with at several places, a quotation that is spliced twice, was one node in the normal form only as long as nothing was rewritten in it or around it; the normalizer keeps it one node now and walks the quotation once for such nodes, which is measured. And a reference or a nullable of a member that names a converter is described as any other member: the serializer leaves its null out, or writes it, before it asks the converter. The counts of tests and planted defects follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the review of the open pull requests of phase 1 changed, with the pull request each point belongs to: - the normalizer (#58): a function that is applied to the document is a captured compiled one whenever it reads no variable of the quotation, however the quotation gets to it, and a quotation that is nested too deeply stays the error inside a construct whose own error would print it; - the parameterizer (#60): the null and the default value of a function type are refused as a function is; - the layout (#61): the layout asks the resolver that the serializer asks, and why its modifiers are not copied; - the values of the layout (#62): the encoding of an enumeration is a kind of JSON value, and what stands for one value in a query is a requirement on the translator step; - the unions of the layout (#63): why the converter of the unions without fields never writes a case that is null. The counts of tests and planted defects follow the branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the review of the open pull requests of phase 1 changed, with the pull request each point belongs to: - the parameterizer (#60): the null of a union type is a case of the union, which the compiler writes as a case, so as a value it is data and no null literal; - the layout (#61): what is written for a null is asked of the serializer, because three converters of FSharp.SystemTextJson ask for the null and fail on it; a null in a field of a record is left out by the rule of the library, which a union that is written as the value it wraps follows too; and a type without a contract is not described; - the values of the layout (#62): a converter of an enumeration that refuses a value with either exception leaves it not described; - the unions of the layout (#63): a record that a case unwraps is described only when the converter of the union makes the converter of the record. The counts of tests and planted defects follow the branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the review of the open pull requests of phase 1 changed, with the pull request each point belongs to: - the normalizer (#58, raised on #60): the default value of a boolean, which the compiler writes for bool (), is the constant false, so the branch that it does not choose is gone from the normal form and the parameterizer computes nothing of it; - the values of the layout (#62): a converter of tuples is the one of FSharp.SystemTextJson only when its class is the library's, because an application can derive a converter from the public base class; - the unions of the layout (#63): no case of a union is described when one of its cases is written with the name of the tag twice, for the fields or for one field next to an internal tag. The counts of tests and planted defects follow the branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer decided to build the JSON layout narrow first: it supports the set-ups that it has been compared with the serializer for and refuses the others. The layout of every setting of FSharp.SystemTextJson (#61, #62, #63) is shelved, and what those steps verified stays in the record under a heading of its own. - the decision table (section 2) and the key decisions get the scope of the layout; - section 10 says that the first layout is narrowed, and section 16 has the result of the narrow one (#65): the set-ups that it supports and refuses, the answers that it gives, and where it departs from the design; - the record of the application's own converter of records, which the layout does not know, and the two ways to describe its records; - the paragraph on dotnet test through FAKE says that FAKE 6.2.0 has DotNet.testMTP and that #64 replaces the hand-written call. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What the review of the open pull requests of phase 1 changed, with the pull request each point belongs to: - the parameterizer (#60): a number that is not finite is no literal, because the text of a query has no NaN and no infinity, so such a number that code builds into a quotation is a slot and Query.constant of one is reported; - the layout (#65): a record that writes two of its members under one name is not described, nor is a union whose cases share a tag, and next to an internal tag a case that writes the members of a record that the layout does not describe is taken to write the name of the tag twice. The counts of tests and planted defects follow the branches. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What the second review of the open pull requests of phase 1 changed, with the pull request each point belongs to: - the parameterizer (#60): the default value of a type is a literal only where the type has one, as the zero of a number has; the default value of a date, a guid, a nullable, a union or a struct is data, so it is a slot and the translator is never asked to write it into the text of a query. The type tells that, and no value is made to ask it, because a struct can have a constructor that a default value never runs; - the layout (#65): the values of an enumeration that have no name are asked of the serializer too, the smallest number that no constant has and a combination of two constants that is no constant, so a converter that writes them in a way of its own makes the enumeration not described; options with a policy for the keys of dictionaries are refused, because the policy spells the property that a key is written as, which the layout does not say; and a case with two fields that a naming policy spells alike is not described, as the document holds one property for both; options that write the numbers that are not finite as texts are refused too, and the reasons for a reference handler name both of its kinds. That the layout does not tell a scalar that a converter of the application writes is recorded as a requirement on the translator step; and a collection or a dictionary with derived types is not described, because a value written as one of them is an object with the discriminator. The counts of tests and planted defects follow the branches. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
hedgehogqa/fsharp-hedgehog#487 is merged and Hedgehog.MSTest 1.0.0 is on NuGet, so the repository does not carry a copy of the adapter: the pull request that ported it (#43) is closed, the two adapter projects are not added, and the test projects reference the package. The record says which commits of #43 live on in #50 and #54, what the property tests of the stack run on now, and that Hedgehog 2.0.4 still cannot generate DateOnly and TimeOnly. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Section 23 is new. It designs normalized members: a query compares a member through a computed property, and the translator reads the formula of that property from the container. The section keeps the decisions of the maintainer (the container holds the formula, all operations by default), the container schema, the translation table, the scenarios, how to change a definition and the emulator results of 2026-10-17. The details of the application are left out. The other sections change where the new facts contradict them: - The container members have no `Query` prefix (§12, §15). - Property tests run through the released Hedgehog.MSTest package, and the text says that no property writes to the database (§2, §14, §17, §18, §19, §21.7, §22). - SDK 10.0.402 does not contain the compiler fix for dotnet/fsharp#20466 (§2, §17, §21.7). - Section 21.5 records the two C# projects of the application and the FSharp.Core fixes that make them unnecessary. - The tuple gaps link their upstream issues, among them dotnet/fsharp#20800 and dotnet/fsharp#20801, and the build tool links #68. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
docs/quotations-architecture-adr
branch
from
October 18, 2026 23:13
0f7a831 to
077ceb5
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed Changes
Adds the agreed architecture of the planned
FSharp.Azure.Cosmos.Quotationspackage family as an Architecture Decision Record:docsSrc/ADRs/0001-Quotations-Architecture.md, published by fsdocs under a new "Architecture Decision Records" category.The record opens with the usual ADR header (status, context, decision, consequences), followed by the full design: translating native F# quotations into Cosmos DB NoSQL SQL over the public
QueryDefinitionpipeline, an own SQL AST with validator and printer, the null/undefined and optional-value semantics, the pluggable translator model, System.Text.Json + FSharp.SystemTextJson only, the test architecture and the phased roadmap. Implementation follows in one pull request per Phase 0 step; each later pull request updates this record together with the decision it changes.Types of changes
Documentation only.
Checklist
build.cmd BuildDocswith fsdocs--strictpasses)Further comments
Examples taken from a private application were replaced by neutral names and descriptions.
🤖 Generated with Claude Code