Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

```text
/
├── src/Cosmos.Sql/ – Cosmos DB SQL model (FSharp.Azure.Cosmos.Sql)
│ ├── EquatableArray.fs – immutable array with structural equality
│ └── ImmutableArrayPatterns.fs – active patterns that match an `ImmutableArray` by its length
├── src/Cosmos/ – main library (FSharp.Azure.Cosmos)
│ ├── SubStatusCodes.fs – documented Cosmos DB sub-status codes
│ ├── Cosmos.fs – core types and container extensions
Expand All @@ -28,6 +31,7 @@
├── src/Shared/ – source files that every F# project under src and tests compiles
│ └── ValueCollections.fs – `Seq`, `List` and `Array` functions that return `voption` and struct tuples
├── tests/Cosmos.Tests/ – MSTest integration test project
├── tests/Cosmos.Sql.Tests/ – MSTest unit tests of FSharp.Azure.Cosmos.Sql, no emulator needed
├── tests/Cosmos.Tests.Infrastructure/ – shared test fixtures, emulator settings and assertion helpers
├── build/ – FAKE build scripts
└── docsSrc/ – FSharp.Formatting documentation source
Expand Down
2 changes: 2 additions & 0 deletions FSharp.Azure.Cosmos.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"solution": {
"path": "FSharp.Azure.Cosmos.slnx",
"projects": [
"src\\Cosmos.Sql\\FSharp.Azure.Cosmos.Sql.fsproj",
"src\\Cosmos\\FSharp.Azure.Cosmos.fsproj",
"tests\\Cosmos.Sql.Tests\\FSharp.Azure.Cosmos.Sql.Tests.fsproj",
"tests\\Cosmos.Tests.Infrastructure\\FSharp.Azure.Cosmos.Tests.Infrastructure.fsproj",
"tests\\Cosmos.Tests\\FSharp.Azure.Cosmos.Tests.fsproj"
]
Expand Down
2 changes: 2 additions & 0 deletions FSharp.Azure.Cosmos.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<File Path="README.md" />
</Folder>
<Project Path="build/build.fsproj" />
<Project Path="src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj" />
<Project Path="src/Cosmos/FSharp.Azure.Cosmos.fsproj" />
<Project Path="tests/Cosmos.Sql.Tests/FSharp.Azure.Cosmos.Sql.Tests.fsproj" />
<Project Path="tests/Cosmos.Tests.Infrastructure/FSharp.Azure.Cosmos.Tests.Infrastructure.fsproj" />
<Project Path="tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj" />
</Solution>
23 changes: 23 additions & 0 deletions src/Cosmos.Sql/AssemblyInfo.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Auto-Generated by FAKE; do not edit
namespace System
open System.Reflection

[<assembly: AssemblyTitleAttribute("FSharp.Azure.Cosmos.Sql")>]
[<assembly: AssemblyProductAttribute("FSharp.Azure.Cosmos")>]
[<assembly: AssemblyVersionAttribute("1.2.0")>]
[<assembly: AssemblyMetadataAttribute("ReleaseDate","2026-10-01T00:00:00.0000000+02:00")>]
[<assembly: AssemblyFileVersionAttribute("1.2.0")>]
[<assembly: AssemblyInformationalVersionAttribute("1.2.0")>]
[<assembly: AssemblyMetadataAttribute("ReleaseChannel","release")>]
[<assembly: AssemblyMetadataAttribute("GitHash","210cf041a0ce44fcc5a5df369550a77a1f75d436")>]
do ()

module internal AssemblyVersionInformation =
let [<Literal>] AssemblyTitle = "FSharp.Azure.Cosmos.Sql"
let [<Literal>] AssemblyProduct = "FSharp.Azure.Cosmos"
let [<Literal>] AssemblyVersion = "1.2.0"
let [<Literal>] AssemblyMetadata_ReleaseDate = "2026-10-01T00:00:00.0000000+02:00"
let [<Literal>] AssemblyFileVersion = "1.2.0"
let [<Literal>] AssemblyInformationalVersion = "1.2.0"
let [<Literal>] AssemblyMetadata_ReleaseChannel = "release"
let [<Literal>] AssemblyMetadata_GitHash = "210cf041a0ce44fcc5a5df369550a77a1f75d436"
199 changes: 199 additions & 0 deletions src/Cosmos.Sql/EquatableArray.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
namespace FSharp.Azure.Cosmos.Sql

open System
open System.Collections
open System.Collections.Generic
open System.Collections.Immutable
open System.Runtime.InteropServices

/// <summary>
/// An immutable array with structural equality: two instances are equal when they hold equal elements in the same
/// order, and the hash code combines the hash codes of the elements.
/// <para>
/// The own equality of <see cref="T:System.Collections.Immutable.ImmutableArray`1"/>, its
/// <see cref="M:System.Collections.Immutable.ImmutableArray`1.Equals(System.Collections.Immutable.ImmutableArray{`0})"/>
/// that <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/> calls, compares the reference of the array
/// it wraps. Only comparers that go through <see cref="T:System.Collections.IStructuralEquatable"/>, such as the equality
/// the F# compiler generates for records and unions, compare its elements;
/// <see cref="M:System.ValueTuple`2.Equals(System.ValueTuple{`0,`1})"/>, a
/// <see cref="T:System.Collections.Generic.HashSet`1"/> of arrays or a C# caller compares references, and a default
/// array never equals an empty one. The nodes of the syntax tree hold their child sequences in this wrapper instead
/// (the pattern Roslyn incremental generators use), so their equality is structural under every comparer, for
/// round-trip tests, cache keys and the idempotence of passes.
/// </para>
/// <para>
/// The default value holds no array; it behaves as an empty array and is equal to one.
/// </para>
/// </summary>
/// <param name="array">The elements, in order.</param>
[<Struct; CustomEquality; NoComparison>]
type EquatableArray<'T> (array : ImmutableArray<'T>) =

/// <summary>
/// The elements; never a default <see cref="T:System.Collections.Immutable.ImmutableArray`1"/>, so it can be
/// enumerated and indexed without a check.
/// </summary>
member _.Items = if array.IsDefault then ImmutableArray<'T>.Empty else array

/// The number of elements.
member _.Length = if array.IsDefault then 0 else array.Length

/// Whether the array has no elements.
member this.IsEmpty = this.Length = 0

/// <summary>
/// The element at <paramref name="index"/>.
/// </summary>
/// <param name="index">The zero-based position of the element.</param>
member this.Item
with get (index : int) = this.Items[index]

/// <summary>
/// Returns an allocation-free enumerator over the elements, which F# <see langword="for"/> loops use.
/// </summary>
member this.GetEnumerator () = this.Items.GetEnumerator ()

/// <summary>
/// Whether <paramref name="other"/> holds equal elements in the same order; elements are compared with
/// <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </summary>
/// <param name="other">The array to compare with.</param>
member this.Equals (other : EquatableArray<'T>) =
let left = this.Items
let right = other.Items

if left.Length <> right.Length then
false
else
let comparer = EqualityComparer<'T>.Default
let mutable equal = true
let mutable index = 0

while equal && index < left.Length do
equal <- comparer.Equals (left[index], right[index])
index <- index + 1

equal

/// <inheritdoc />
override this.Equals (other : objnull) =
match other with
| :? EquatableArray<'T> as other -> this.Equals other
| _ -> false

/// Combines the hash codes of the elements, in order, so equal arrays have equal hash codes.
override this.GetHashCode () =
let mutable hash = HashCode ()

for item in this.Items do
hash.Add item

hash.ToHashCode ()

/// Shows the elements in square brackets, separated by semicolons, the way F# shows an array.
override this.ToString () =
let items = this.Items |> Seq.map (fun item -> $"%A{item}")
$"""[| {String.Join ("; ", items)} |]"""

/// <summary>
/// Wraps <paramref name="items"/> without copying it, so that an
/// <see cref="T:System.Collections.Immutable.ImmutableArray`1"/> can be given where an
/// <see cref="T:FSharp.Azure.Cosmos.Sql.EquatableArray`1"/> is expected.
/// </summary>
/// <param name="items">The elements.</param>
static member op_Implicit (items : ImmutableArray<'T>) : EquatableArray<'T> = EquatableArray items

/// <summary>
/// The elements of <paramref name="array"/> as an <see cref="T:System.Collections.Immutable.ImmutableArray`1"/>,
/// without copying them and never as a default array, so that an
/// <see cref="T:FSharp.Azure.Cosmos.Sql.EquatableArray`1"/> can be given where an
/// <see cref="T:System.Collections.Immutable.ImmutableArray`1"/> is expected.
/// </summary>
/// <param name="array">The array.</param>
static member op_Implicit (array : EquatableArray<'T>) : ImmutableArray<'T> = array.Items

interface IEquatable<EquatableArray<'T>> with
/// <inheritdoc />
member this.Equals other = this.Equals other

interface IReadOnlyList<'T> with
/// <inheritdoc />
member this.Count = this.Length

/// <inheritdoc />
member this.Item
with get index = this.Items[index]

/// <inheritdoc />
member this.GetEnumerator () : IEnumerator<'T> = (this.Items :> IEnumerable<'T>).GetEnumerator()

/// <inheritdoc />
member this.GetEnumerator () : IEnumerator = (this.Items :> IEnumerable).GetEnumerator()

/// <summary>
/// Functions that create and transform <see cref="T:FSharp.Azure.Cosmos.Sql.EquatableArray`1"/> values.
/// </summary>
[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module EquatableArray =

/// The array without elements.
[<GeneralizableValue>]
let empty<'T> : EquatableArray<'T> = EquatableArray ImmutableArray<'T>.Empty

/// <summary>
/// Wraps <paramref name="items"/> without copying it.
/// </summary>
/// <param name="items">The elements.</param>
let ofImmutableArray (items : ImmutableArray<'T>) = EquatableArray items

/// <summary>
/// Copies <paramref name="items"/> into a new array.
/// </summary>
/// <param name="items">The elements.</param>
let ofArray (items : 'T array) = EquatableArray (ImmutableArray.Create<'T> items)
Comment thread
xperiandri marked this conversation as resolved.

/// <summary>
/// Wraps <paramref name="items"/> without copying it, as
/// <see cref="M:System.Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray``1(``0[])"/> does.
/// <para>
/// The caller gives the array away: the result is immutable only as long as nothing writes to
/// <paramref name="items"/> any more. A later write changes the elements, the equality and the hash code of the
/// result, also where it already is a key of a dictionary or a member of a set.
/// </para>
/// </summary>
/// <param name="items">The elements, in an array that is never written to again.</param>
let unsafeOfArray (items : 'T array) = EquatableArray (ImmutableCollectionsMarshal.AsImmutableArray<'T> items)

/// <summary>
/// Copies <paramref name="items"/> into a new array.
/// </summary>
/// <param name="items">The elements.</param>
let ofSeq (items : 'T seq) = EquatableArray (ImmutableArray.CreateRange<'T> items)

/// <summary>
/// An array that holds <paramref name="item"/> only.
/// </summary>
/// <param name="item">The element.</param>
let singleton (item : 'T) = EquatableArray (ImmutableArray.Create<'T> item)

/// <summary>
/// The elements of <paramref name="array"/> as an <see cref="T:System.Collections.Immutable.ImmutableArray`1"/>,
/// without copying them.
/// </summary>
/// <param name="array">The array.</param>
let toImmutableArray (array : EquatableArray<'T>) = array.Items

/// <summary>
/// Applies <paramref name="mapping"/> to every element of <paramref name="array"/>, in order.
/// </summary>
/// <param name="mapping">The function to apply.</param>
/// <param name="array">The array.</param>
let map (mapping : 'T -> 'U) (array : EquatableArray<'T>) =
let items = array.Items
let builder = ImmutableArray.CreateBuilder<'U> items.Length

for item in items do
builder.Add (mapping item)

EquatableArray (builder.MoveToImmutable ())
30 changes: 30 additions & 0 deletions src/Cosmos.Sql/FSharp.Azure.Cosmos.Sql.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>$(AssemblyBaseName).Sql</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Deterministic>true</Deterministic>
<!-- Not packed until the syntax tree, the printer and the validator are in: the collection types alone are no package -->
<IsPackable>false</IsPackable>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Optimize>true</Optimize>
<Tailcalls>true</Tailcalls>
</PropertyGroup>

<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="EquatableArray.fs" />
<Compile Include="ImmutableArrayPatterns.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions src/Cosmos.Sql/ImmutableArrayPatterns.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// <summary>
/// Struct partial active patterns that match an <see cref="T:System.Collections.Immutable.ImmutableArray`1"/> by its
/// length, so code over the children of a syntax node reads like a list pattern without converting to an F# list.
/// <para>
/// Each pattern returns a value option made a struct by <see cref="T:Microsoft.FSharp.Core.StructAttribute"/> on its
/// return value, or a <see cref="T:System.Boolean"/>, and its payload is a struct tuple, so a match allocates nothing.
/// A multi-case active pattern over the lengths would return a heap
/// <see cref="T:Microsoft.FSharp.Core.FSharpChoice`3"/> on every match, which is why every length is its own partial
/// pattern and lengths a pattern does not cover fall through to the next case.
/// </para>
/// </summary>
/// <example>
/// <code lang="fsharp">
/// match arguments with
/// | Arr0 -> "no argument"
/// | Arr1 single -> "one argument"
/// | Arr2 (first, second) -> "two arguments"
/// | Arr3 (first, second, third) -> "three arguments"
/// | ArrN 4 -> "four arguments"
/// | _ -> "more arguments"
/// </code>
/// </example>
[<AutoOpen>]
module FSharp.Azure.Cosmos.Sql.ImmutableArrayPatterns

open System.Collections.Immutable

/// <summary>
/// Matches an array without elements. A default (uninitialized) array counts as empty.
/// </summary>
/// <param name="items">The array to match.</param>
let (|Arr0|_|) (items : ImmutableArray<'T>) = items.IsDefaultOrEmpty

/// <summary>
/// Matches an array of exactly one element and returns that element.
/// </summary>
/// <param name="items">The array to match.</param>
[<return : Struct>]
let (|Arr1|_|) (items : ImmutableArray<'T>) =
if not items.IsDefault && items.Length = 1 then
ValueSome items[0]
else
ValueNone

/// <summary>
/// Matches an array of exactly two elements and returns them as a struct tuple, which the pattern binds with or
/// without the <see langword="struct"/> keyword.
/// </summary>
/// <param name="items">The array to match.</param>
[<return : Struct>]
let (|Arr2|_|) (items : ImmutableArray<'T>) =
if not items.IsDefault && items.Length = 2 then
ValueSome (struct (items[0], items[1]))
else
ValueNone

/// <summary>
/// Matches an array of exactly three elements and returns them as a struct tuple.
/// </summary>
/// <param name="items">The array to match.</param>
[<return : Struct>]
let (|Arr3|_|) (items : ImmutableArray<'T>) =
if not items.IsDefault && items.Length = 3 then
ValueSome (struct (items[0], items[1], items[2]))
else
ValueNone

/// <summary>
/// Matches an array of exactly <paramref name="length"/> elements, for the lengths that have no pattern of their own;
/// index the array to read its elements.
/// </summary>
/// <param name="length">The number of elements to match, written as the argument of the pattern.</param>
/// <param name="items">The array to match.</param>
let (|ArrN|_|) (length : int) (items : ImmutableArray<'T>) = (if items.IsDefault then 0 else items.Length) = length
Loading
Loading