├── icon.png ├── SoloDB ├── IdGenerator.fs ├── NativeArray.fs ├── CustomTypeId.fs ├── SoloDB.fsproj ├── Attributes.fs ├── Types.fs ├── Extensions.fs ├── JsonFunctions.fs ├── Connections.fs ├── Utils.fs ├── SoloDBInterfaces.fs └── MongoEmulation.fs ├── .github └── workflows │ └── dotnet.yml ├── SoloDB.sln ├── .gitattributes ├── .gitignore ├── LICENSE.txt └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unconcurrent/SoloDB/HEAD/icon.png -------------------------------------------------------------------------------- /SoloDB/IdGenerator.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase.Attributes 2 | 3 | open System 4 | open SoloDatabase 5 | open System.Reflection 6 | open System.Linq.Expressions 7 | 8 | #nowarn "3535" 9 | 10 | [] 11 | type IIdGenerator = 12 | /// 13 | /// object collection -> object document -> id 14 | /// 15 | abstract GenerateId: obj -> obj -> obj 16 | abstract IsEmpty: obj -> bool 17 | 18 | type IIdGenerator<'T> = 19 | /// 20 | /// object collection -> object document -> id 21 | /// 22 | abstract GenerateId: ISoloDBCollection<'T> -> 'T -> obj 23 | /// old id -> bool 24 | abstract IsEmpty: obj -> bool -------------------------------------------------------------------------------- /SoloDB/NativeArray.fs: -------------------------------------------------------------------------------- 1 | module internal NativeArray 2 | 3 | open System 4 | open System.Runtime.InteropServices 5 | open Microsoft.FSharp.NativeInterop 6 | 7 | #nowarn "9" 8 | 9 | [] 10 | type NativeArray private (ptr: nativeptr, len: int32) = 11 | member this.Length = len 12 | member this.Span = Span(NativePtr.toVoidPtr ptr, int len) 13 | 14 | [] 15 | val mutable private Disposed: bool 16 | 17 | static member val Empty = 18 | let mutable empty = new NativeArray(NativePtr.nullPtr, 0) 19 | empty.Disposed <- true 20 | empty 21 | 22 | static member internal Alloc(len: int32) = 23 | let mutable ptr = Marshal.AllocHGlobal (nativeint len) |> NativePtr.ofNativeInt 24 | 25 | new NativeArray(ptr, len) 26 | 27 | member this.Dispose() = 28 | if not this.Disposed then 29 | this.Disposed <- true 30 | Marshal.FreeHGlobal (ptr |> NativePtr.toNativeInt) 31 | 32 | interface IDisposable with 33 | member this.Dispose() = 34 | this.Dispose() -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '**.md' 8 | - '.gitignore' 9 | pull_request: 10 | branches: [ master ] 11 | paths-ignore: 12 | - '**.md' 13 | - '.gitignore' 14 | 15 | jobs: 16 | build: 17 | if: github.actor == 'Unconcurrent' || github.actor == 'sologub' 18 | runs-on: ubuntu-latest # or self-hosted 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: '9.0.x' 26 | 27 | - name: Cache NuGet packages 28 | uses: actions/cache@v4 29 | with: 30 | path: ~/.nuget/packages 31 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.fsproj') }} 32 | restore-keys: | 33 | ${{ runner.os }}-nuget- 34 | 35 | - name: Build and Restore 36 | run: dotnet build SoloDB -c Release 37 | 38 | - name: Upload Artifacts 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: build-artifacts-${{ github.sha }} 42 | path: ./SoloDB/bin/Release/* 43 | -------------------------------------------------------------------------------- /SoloDB/CustomTypeId.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System 4 | open SoloDatabase 5 | open SoloDatabase.Attributes 6 | open System.Reflection 7 | open System.Linq.Expressions 8 | 9 | 10 | [] 11 | type internal CustomTypeId<'t> = 12 | static member val internal Value = 13 | typeof<'t>.GetProperties(BindingFlags.Public ||| BindingFlags.Instance) 14 | |> Seq.choose( 15 | fun p -> 16 | match p.GetCustomAttribute(true) with 17 | | a when Utils.isNull a -> None 18 | | a -> 19 | if not p.CanWrite then failwithf "Cannot create a generator for a non writtable parameter '%s' for type %s" p.Name typeof<'t>.FullName 20 | match a.IdGenerator with 21 | | null -> failwithf "Generator type for Id property (%s) is null." p.Name 22 | | generator -> Some (p, generator) 23 | ) 24 | |> Seq.tryHead 25 | |> Option.bind( 26 | fun (p, gt) -> 27 | if gt.GetInterfaces() |> Seq.exists(fun i -> i.FullName.StartsWith "SoloDatabase.Attributes.IIdGenerator" && not i.IsArray) |> not then 28 | failwithf "Generator type for Id property (%s) does not implement the IIdGenerator or IIdGenerator<'T> interface." p.Name 29 | 30 | let instance = (Activator.CreateInstance gt) 31 | Some {| 32 | Generator = instance 33 | SetId = fun id o -> p.SetValue(o, id) 34 | GetId = fun o -> p.GetValue(o) 35 | IdType = p.PropertyType 36 | Property = p 37 | |} 38 | ) -------------------------------------------------------------------------------- /SoloDB.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34916.146 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SoloDB", "SoloDB\SoloDB.fsproj", "{AD8262F4-F835-4F10-ACF8-A7A78488FBB2}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Tests", "..\SoloDBTests\Tests\Tests.fsproj", "{6345B0D6-909E-4C54-9630-0188C07BEDF4}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "..\SoloDBTests\CSharpTests\CSharpTests.csproj", "{1846A323-5030-4682-8C3D-B49D3543D1DE}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchMaster", "..\SoloDBTests\BenchMaster\BenchMaster.csproj", "{5665ED34-0635-4DD9-98CC-E1B8C4873E17}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {AD8262F4-F835-4F10-ACF8-A7A78488FBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {AD8262F4-F835-4F10-ACF8-A7A78488FBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {AD8262F4-F835-4F10-ACF8-A7A78488FBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {AD8262F4-F835-4F10-ACF8-A7A78488FBB2}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {6345B0D6-909E-4C54-9630-0188C07BEDF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {6345B0D6-909E-4C54-9630-0188C07BEDF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {6345B0D6-909E-4C54-9630-0188C07BEDF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {6345B0D6-909E-4C54-9630-0188C07BEDF4}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {1846A323-5030-4682-8C3D-B49D3543D1DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {1846A323-5030-4682-8C3D-B49D3543D1DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {1846A323-5030-4682-8C3D-B49D3543D1DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {1846A323-5030-4682-8C3D-B49D3543D1DE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {5665ED34-0635-4DD9-98CC-E1B8C4873E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {5665ED34-0635-4DD9-98CC-E1B8C4873E17}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {5665ED34-0635-4DD9-98CC-E1B8C4873E17}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {5665ED34-0635-4DD9-98CC-E1B8C4873E17}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {0737FF8E-CB5D-4744-B047-707B070426BD} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /SoloDB/SoloDB.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.0;netstandard2.1 6 | False 7 | SoloDB 8 | SoloDB is a document database built on top of SQLite using the JSONB data type. It leverages the robustness and simplicity of SQLite to provide an efficient and lightweight database solution for handling JSON documents. 9 | Radu Sologub 10 | Radu Sologub 11 | SoloDatabase 12 | https://github.com/Unconcurrent/SoloDB 13 | README.md 14 | https://github.com/Unconcurrent/SoloDB 15 | git 16 | database 17 | True 18 | icon.png 19 | LICENSE.txt 20 | embedded 21 | $(OutputPath) 22 | 0.6.0 23 | 9 24 | true 25 | True 26 | 3370 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | True 52 | \ 53 | 54 | 55 | True 56 | \ 57 | 58 | 59 | True 60 | \ 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /SoloDB/Attributes.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase.Attributes 2 | 3 | open System 4 | 5 | /// 6 | /// Marks a property for indexing within the database collection. 7 | /// 8 | /// An index significantly improves query performance for the decorated property. 9 | /// The index will be created automatically under two conditions: 10 | /// 11 | /// 12 | /// 13 | /// When the collection for this document type is accessed for the very first time. 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// When the EnsureAddedAttributeIndexes() method is explicitly called on the collection. 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | [] 25 | type IndexedAttribute(unique: bool) = 26 | inherit Attribute() 27 | 28 | /// 29 | /// Initializes a new instance of the class, specifying whether the index should enforce uniqueness. 30 | /// 31 | /// 32 | /// A boolean value indicating whether the indexed values must be unique across all documents in the collection. 33 | /// If true, the database will reject insertions or updates that would result in duplicate values for this property. 34 | /// 35 | new() = IndexedAttribute(false) 36 | 37 | /// 38 | /// Gets a value indicating whether the index enforces a uniqueness constraint. 39 | /// 40 | member val Unique = unique 41 | 42 | /// 43 | /// Instructs the serializer to include type information when storing a document. 44 | /// 45 | /// This is essential for collections that store documents from an inheritance hierarchy (polymorphic collections). 46 | /// By adding this attribute to a base class, the serializer will embed a type discriminator field (e.g., "_type") 47 | /// in the stored document. This ensures that when the document is retrieved, it can be correctly deserialized 48 | /// back into its original, specific derived type, preserving the class hierarchy. 49 | /// 50 | /// 51 | [] 52 | type PolimorphicAttribute() = 53 | inherit Attribute() 54 | 55 | /// 56 | /// Designates a property as the document's primary identifier (ID). 57 | /// 58 | /// This attribute inherently creates a unique index on the property. Each document in a collection must have a unique ID. 59 | /// It inherits from with the `unique` flag set to true. 60 | /// 61 | /// 62 | /// 63 | /// Specifies a that implements a custom ID generation strategy. 64 | /// This allows for user-defined logic for creating new document IDs (e.g., using a custom algorithm, a specific format, or an external service). 65 | /// 66 | [] 67 | [] 68 | type SoloId(idGenerator: Type) = 69 | inherit IndexedAttribute(true) 70 | 71 | /// 72 | /// Gets the of the custom ID generator to be used for this primary key. 73 | /// 74 | member val IdGenerator = idGenerator -------------------------------------------------------------------------------- /SoloDB/Types.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase.Types 2 | 3 | open System 4 | open System.Threading 5 | open System.Collections.Generic 6 | 7 | type internal DisposableMutex = 8 | val mutex: Mutex 9 | 10 | internal new(name: string) = 11 | let mutex = new Mutex(false, name) 12 | mutex.WaitOne() |> ignore 13 | 14 | { mutex = mutex } 15 | 16 | 17 | interface IDisposable with 18 | member this.Dispose() = 19 | this.mutex.ReleaseMutex() 20 | this.mutex.Dispose() 21 | 22 | /// 23 | /// This is for internal use only, don't touch. 24 | /// 25 | [] 26 | type DbObjectRow = { 27 | /// If the Id is NULL then the ValueJSON is a error message encoded in a JSON string. 28 | Id: Nullable 29 | ValueJSON: string 30 | } 31 | 32 | [] 33 | type Metadata = { 34 | Key: string 35 | Value: string 36 | } 37 | 38 | [] 39 | type SoloDBFileHeader = { 40 | Id: int64 41 | Name: string 42 | FullPath: string 43 | DirectoryId: int64 44 | Length: int64 45 | Created: DateTimeOffset 46 | Modified: DateTimeOffset 47 | Metadata: IReadOnlyDictionary 48 | } 49 | 50 | [] 51 | type SoloDBDirectoryHeader = { 52 | Id: int64 53 | Name: string 54 | FullPath: string 55 | ParentId: Nullable 56 | Created: DateTimeOffset 57 | Modified: DateTimeOffset 58 | Metadata: IReadOnlyDictionary 59 | } 60 | 61 | [] 62 | type SoloDBEntryHeader = 63 | | File of file: SoloDBFileHeader 64 | | Directory of directory: SoloDBDirectoryHeader 65 | 66 | member this.Name = 67 | match this with 68 | | File f -> f.Name 69 | | Directory d -> d.Name 70 | 71 | member this.FullPath = 72 | match this with 73 | | File f -> f.FullPath 74 | | Directory d -> d.FullPath 75 | 76 | member this.DirectoryId = 77 | match this with 78 | | File f -> f.DirectoryId |> Nullable 79 | | Directory d -> d.ParentId 80 | 81 | member this.Created = 82 | match this with 83 | | File f -> f.Created 84 | | Directory d -> d.Created 85 | 86 | member this.Modified = 87 | match this with 88 | | File f -> f.Modified 89 | | Directory d -> d.Modified 90 | 91 | member this.Metadata = 92 | match this with 93 | | File f -> f.Metadata 94 | | Directory d -> d.Metadata 95 | 96 | [] 97 | type internal SoloDBFileChunk = { 98 | Id: int64 99 | FileId: int64 100 | Number: int64 101 | Data: byte array 102 | } 103 | 104 | type SoloDBConfiguration = internal { 105 | /// The general switch to enable or disable caching. 106 | /// By disabling it, any cached data will be automatically cleared. 107 | mutable CachingEnabled: bool 108 | } 109 | 110 | /// 111 | /// Specifies the field to sort by when listing files or directories. 112 | /// 113 | type SortField = 114 | /// Sort by name (case-insensitive). 115 | | Name = 0 116 | /// Sort by file size (only applicable to files; directories treated as 0). 117 | | Size = 1 118 | /// Sort by creation date. 119 | | Created = 2 120 | /// Sort by modification date. 121 | | Modified = 3 122 | 123 | /// 124 | /// Specifies the sort direction. 125 | /// 126 | type SortDirection = 127 | /// Sort in ascending order (A-Z, smallest first, oldest first). 128 | | Ascending = 0 129 | /// Sort in descending order (Z-A, largest first, newest first). 130 | | Descending = 1 -------------------------------------------------------------------------------- /SoloDB/Extensions.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Linq 6 | open System.Text.RegularExpressions 7 | open System.Runtime.CompilerServices 8 | open System.Linq.Expressions 9 | open System.Reflection 10 | 11 | /// These extensions are supported by the Linq Expression to SQLite translator. 12 | /// Linq Expressions do not support C# dynamic, therefore all the Dyn method are 13 | /// made to simulate that. This class also contains SQL native methods: 14 | /// Like and Any that propagate into (LIKE) and (json_each() with a WHERE filter). 15 | [] 16 | type Extensions = 17 | /// 18 | /// Returns all elements contained in the specified IGrouping<'Key, 'T> as an array. 19 | /// Useful for materializing grouped query results into a concrete collection. 20 | /// 21 | [] 22 | static member Items<'Key, 'T>(g: IGrouping<'Key, 'T>) = 23 | g |> Seq.toArray 24 | 25 | /// The SQL LIKE operator, to be used in queries. 26 | [] 27 | static member Like(this: string, pattern: string) = 28 | let regexPattern = 29 | "^" + Regex.Escape(pattern).Replace("\\%", ".*").Replace("\\_", ".") + "$" 30 | Regex.IsMatch(this, regexPattern, RegexOptions.IgnoreCase) 31 | 32 | /// 33 | /// This method will be translated directly into an EXISTS subquery with the `json_each` table-valued function. 34 | /// To be used inside queries with items that contain arrays or other supported collections. 35 | /// 36 | /// 37 | /// Example: collection.Where(x => x.Numbers.Any(x => x > 10)).LongCount(); 38 | /// 39 | [] 40 | static member Any<'T>(this: ICollection<'T>, condition: Expression>) = 41 | let f = condition.Compile true 42 | this |> Seq.exists f.Invoke 43 | 44 | /// 45 | /// Dynamically retrieves a property value from an object and casts it to the specified generic type, to be used in queries. 46 | /// 47 | /// The source object. 48 | /// The name of the property to retrieve. 49 | /// The value of the property cast to type 'T. 50 | [] 51 | static member Dyn<'T>(this: obj, propertyName: string) : 'T = 52 | let prop = this.GetType().GetProperty(propertyName, BindingFlags.Public ||| BindingFlags.Instance) 53 | if prop <> null && prop.CanRead then 54 | prop.GetValue(this) :?> 'T 55 | else 56 | let msg = sprintf "Property '%s' not found on type '%s' or is not readable." propertyName (this.GetType().FullName) 57 | raise (ArgumentException(msg)) 58 | 59 | /// 60 | /// Retrieves a property value from an object using a PropertyInfo object and casts it to the specified generic type, to be used in queries. 61 | /// 62 | /// The source object. 63 | /// The PropertyInfo object representing the property to retrieve. 64 | /// The value of the property cast to type 'T. 65 | [] 66 | static member Dyn<'T>(this: obj, property: PropertyInfo) : 'T = 67 | try 68 | property.GetValue(this) :?> 'T 69 | with 70 | | :? InvalidCastException as ex -> 71 | let msg = sprintf "Cannot cast property '%s' value to type '%s'." property.Name (typeof<'T>.FullName) 72 | raise (InvalidCastException(msg, ex)) 73 | 74 | /// 75 | /// Dynamically retrieves a property value from an object, to be used in queries. 76 | /// 77 | /// The source object. 78 | /// The name of the property to retrieve. 79 | /// The value of the property as an obj. 80 | [] 81 | static member Dyn(this: obj, propertyName: string) : obj = 82 | let prop = this.GetType().GetProperty(propertyName, BindingFlags.Public ||| BindingFlags.Instance) 83 | if prop <> null && prop.CanRead then 84 | prop.GetValue(this) 85 | else 86 | let msg = sprintf "Property '%s' not found on type '%s' or is not readable." propertyName (this.GetType().FullName) 87 | raise (ArgumentException(msg)) 88 | 89 | /// To be used in queries. 90 | [] 91 | static member CastTo<'T>(this: obj) : 'T = 92 | this :?> 'T 93 | 94 | /// This method sets a new value to a property inside the UpdateMany method's transform expression. This should not be called inside real code. 95 | [] 96 | static member Set<'T>(this: 'T, value: obj) : unit = 97 | (raise << NotImplementedException) "This is a function for the SQL update builder." 98 | 99 | /// This method appends a new value to a array-like property inside the UpdateMany method's transform expression. This should not be called inside real code. 100 | [] 101 | static member Append(this: IEnumerable<'T>, value: obj) : unit = // For arrays 102 | (raise << NotImplementedException) "This is a function for the SQL update builder." 103 | 104 | /// This method sets a new value at an index to a array-like property inside the UpdateMany method's transform expression. This should not be called inside real code. 105 | [] 106 | static member SetAt(this: ICollection<'T>, index: int, value: obj) : unit = 107 | (raise << NotImplementedException) "This is a function for the SQL update builder." 108 | 109 | /// This method removes value at an index to a array-like property inside the UpdateMany method's transform expression. This should not be called inside real code. 110 | [] 111 | static member RemoveAt(this: ICollection<'T>, index: int) : unit = 112 | (raise << NotImplementedException) "This is a function for the SQL update builder." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Remove the tests from the public, just like the SQLite team. 7 | CSharpTests/ 8 | Tests/ 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Mono auto generated files 21 | mono_crash.* 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | [Ww][Ii][Nn]32/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Oo]ut/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # ASP.NET Scaffolding 71 | ScaffoldingReadMe.txt 72 | 73 | # StyleCop 74 | StyleCopReport.xml 75 | 76 | # Files built by Visual Studio 77 | *_i.c 78 | *_p.c 79 | *_h.h 80 | *.ilk 81 | *.meta 82 | *.obj 83 | *.iobj 84 | *.pch 85 | *.pdb 86 | *.ipdb 87 | *.pgc 88 | *.pgd 89 | *.rsp 90 | *.sbr 91 | *.tlb 92 | *.tli 93 | *.tlh 94 | *.tmp 95 | *.tmp_proj 96 | *_wpftmp.csproj 97 | *.log 98 | *.vspscc 99 | *.vssscc 100 | .builds 101 | *.pidb 102 | *.svclog 103 | *.scc 104 | 105 | # Chutzpah Test files 106 | _Chutzpah* 107 | 108 | # Visual C++ cache files 109 | ipch/ 110 | *.aps 111 | *.ncb 112 | *.opendb 113 | *.opensdf 114 | *.sdf 115 | *.cachefile 116 | *.VC.db 117 | *.VC.VC.opendb 118 | 119 | # Visual Studio profiler 120 | *.psess 121 | *.vsp 122 | *.vspx 123 | *.sap 124 | 125 | # Visual Studio Trace Files 126 | *.e2e 127 | 128 | # TFS 2012 Local Workspace 129 | $tf/ 130 | 131 | # Guidance Automation Toolkit 132 | *.gpState 133 | 134 | # ReSharper is a .NET coding add-in 135 | _ReSharper*/ 136 | *.[Rr]e[Ss]harper 137 | *.DotSettings.user 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Coverlet is a free, cross platform Code Coverage Tool 150 | coverage*.json 151 | coverage*.xml 152 | coverage*.info 153 | 154 | # Visual Studio code coverage results 155 | *.coverage 156 | *.coveragexml 157 | 158 | # NCrunch 159 | _NCrunch_* 160 | .*crunch*.local.xml 161 | nCrunchTemp_* 162 | 163 | # MightyMoose 164 | *.mm.* 165 | AutoTest.Net/ 166 | 167 | # Web workbench (sass) 168 | .sass-cache/ 169 | 170 | # Installshield output folder 171 | [Ee]xpress/ 172 | 173 | # DocProject is a documentation generator add-in 174 | DocProject/buildhelp/ 175 | DocProject/Help/*.HxT 176 | DocProject/Help/*.HxC 177 | DocProject/Help/*.hhc 178 | DocProject/Help/*.hhk 179 | DocProject/Help/*.hhp 180 | DocProject/Help/Html2 181 | DocProject/Help/html 182 | 183 | # Click-Once directory 184 | publish/ 185 | 186 | # Publish Web Output 187 | *.[Pp]ublish.xml 188 | *.azurePubxml 189 | # Note: Comment the next line if you want to checkin your web deploy settings, 190 | # but database connection strings (with potential passwords) will be unencrypted 191 | *.pubxml 192 | *.publishproj 193 | 194 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 195 | # checkin your Azure Web App publish settings, but sensitive information contained 196 | # in these scripts will be unencrypted 197 | PublishScripts/ 198 | 199 | # NuGet Packages 200 | *.nupkg 201 | # NuGet Symbol Packages 202 | *.snupkg 203 | # The packages folder can be ignored because of Package Restore 204 | **/[Pp]ackages/* 205 | # except build/, which is used as an MSBuild target. 206 | !**/[Pp]ackages/build/ 207 | # Uncomment if necessary however generally it will be regenerated when needed 208 | #!**/[Pp]ackages/repositories.config 209 | # NuGet v3's project.json files produces more ignorable files 210 | *.nuget.props 211 | *.nuget.targets 212 | 213 | # Microsoft Azure Build Output 214 | csx/ 215 | *.build.csdef 216 | 217 | # Microsoft Azure Emulator 218 | ecf/ 219 | rcf/ 220 | 221 | # Windows Store app package directories and files 222 | AppPackages/ 223 | BundleArtifacts/ 224 | Package.StoreAssociation.xml 225 | _pkginfo.txt 226 | *.appx 227 | *.appxbundle 228 | *.appxupload 229 | 230 | # Visual Studio cache files 231 | # files ending in .cache can be ignored 232 | *.[Cc]ache 233 | # but keep track of directories ending in .cache 234 | !?*.[Cc]ache/ 235 | 236 | # Others 237 | ClientBin/ 238 | ~$* 239 | *~ 240 | *.dbmdl 241 | *.dbproj.schemaview 242 | *.jfm 243 | *.pfx 244 | *.publishsettings 245 | orleans.codegen.cs 246 | 247 | # Including strong name files can present a security risk 248 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 249 | #*.snk 250 | 251 | # Since there are multiple workflows, uncomment next line to ignore bower_components 252 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 253 | #bower_components/ 254 | 255 | # RIA/Silverlight projects 256 | Generated_Code/ 257 | 258 | # Backup & report files from converting an old project file 259 | # to a newer Visual Studio version. Backup files are not needed, 260 | # because we have git ;-) 261 | _UpgradeReport_Files/ 262 | Backup*/ 263 | UpgradeLog*.XML 264 | UpgradeLog*.htm 265 | ServiceFabricBackup/ 266 | *.rptproj.bak 267 | 268 | # SQL Server files 269 | *.mdf 270 | *.ldf 271 | *.ndf 272 | 273 | # Business Intelligence projects 274 | *.rdl.data 275 | *.bim.layout 276 | *.bim_*.settings 277 | *.rptproj.rsuser 278 | *- [Bb]ackup.rdl 279 | *- [Bb]ackup ([0-9]).rdl 280 | *- [Bb]ackup ([0-9][0-9]).rdl 281 | 282 | # Microsoft Fakes 283 | FakesAssemblies/ 284 | 285 | # GhostDoc plugin setting file 286 | *.GhostDoc.xml 287 | 288 | # Node.js Tools for Visual Studio 289 | .ntvs_analysis.dat 290 | node_modules/ 291 | 292 | # Visual Studio 6 build log 293 | *.plg 294 | 295 | # Visual Studio 6 workspace options file 296 | *.opt 297 | 298 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 299 | *.vbw 300 | 301 | # Visual Studio LightSwitch build output 302 | **/*.HTMLClient/GeneratedArtifacts 303 | **/*.DesktopClient/GeneratedArtifacts 304 | **/*.DesktopClient/ModelManifest.xml 305 | **/*.Server/GeneratedArtifacts 306 | **/*.Server/ModelManifest.xml 307 | _Pvt_Extensions 308 | 309 | # Paket dependency manager 310 | .paket/paket.exe 311 | paket-files/ 312 | 313 | # FAKE - F# Make 314 | .fake/ 315 | 316 | # CodeRush personal settings 317 | .cr/personal 318 | 319 | # Python Tools for Visual Studio (PTVS) 320 | __pycache__/ 321 | *.pyc 322 | 323 | # Cake - Uncomment if you are using it 324 | # tools/** 325 | # !tools/packages.config 326 | 327 | # Tabs Studio 328 | *.tss 329 | 330 | # Telerik's JustMock configuration file 331 | *.jmconfig 332 | 333 | # BizTalk build output 334 | *.btp.cs 335 | *.btm.cs 336 | *.odx.cs 337 | *.xsd.cs 338 | 339 | # OpenCover UI analysis results 340 | OpenCover/ 341 | 342 | # Azure Stream Analytics local run output 343 | ASALocalRun/ 344 | 345 | # MSBuild Binary and Structured Log 346 | *.binlog 347 | 348 | # NVidia Nsight GPU debugger configuration file 349 | *.nvuser 350 | 351 | # MFractors (Xamarin productivity tool) working folder 352 | .mfractor/ 353 | 354 | # Local History for Visual Studio 355 | .localhistory/ 356 | 357 | # BeatPulse healthcheck temp database 358 | healthchecksdb 359 | 360 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 361 | MigrationBackup/ 362 | 363 | # Ionide (cross platform F# VS Code tools) working folder 364 | .ionide/ 365 | 366 | # Fody - auto-generated XML schema 367 | FodyWeavers.xsd -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /SoloDB/JsonFunctions.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open SoloDatabase.Attributes 4 | open System.Reflection 5 | open System.Linq.Expressions 6 | open System 7 | open SoloDatabase 8 | open Utils 9 | open SoloDatabase.Types 10 | open SoloDatabase.JsonSerializator 11 | open System.Runtime.CompilerServices 12 | 13 | 14 | [] 15 | type internal HasTypeId<'t> = 16 | // Instead of concurrent dictionaries, we can use a static class 17 | // if the parameters can be represented as generic types. 18 | static member val private Properties: PropertyInfo array = typeof<'t>.GetProperties() 19 | static member private IdPropertyFilter (p: PropertyInfo) = p.Name = "Id" && p.PropertyType = typeof && p.CanWrite && p.CanRead 20 | static member val internal Value = 21 | HasTypeId<'t>.Properties 22 | |> Array.exists HasTypeId<'t>.IdPropertyFilter 23 | 24 | static member val internal Read = 25 | match HasTypeId<'t>.Properties 26 | |> Array.tryFind HasTypeId<'t>.IdPropertyFilter 27 | with 28 | | Some p -> 29 | let x = ParameterExpression.Parameter(typeof<'t>, "x") 30 | let l = LambdaExpression.Lambda>( 31 | MethodCallExpression.Call(x, p.GetMethod), 32 | [x] 33 | ) 34 | let fn = l.Compile(false) 35 | fn.Invoke 36 | | None -> fun (_x: 't) -> failwithf "Cannot read nonexistant Id from Type %A" typeof<'t>.FullName 37 | 38 | static member val internal Write = 39 | match HasTypeId<'t>.Properties 40 | |> Array.tryFind HasTypeId<'t>.IdPropertyFilter 41 | with 42 | | Some p -> 43 | let x = ParameterExpression.Parameter(typeof<'t>, "x") 44 | let y = ParameterExpression.Parameter(typeof, "y") 45 | let l = LambdaExpression.Lambda>( 46 | MethodCallExpression.Call(x, p.SetMethod, y), 47 | [|x; y|] 48 | ) 49 | let fn = l.Compile(false) 50 | fun x y -> fn.Invoke(x, y) 51 | | None -> fun (_x: 't) (_y: int64) -> failwithf "Cannot write nonexistant Id from Type %A" typeof<'t>.FullName 52 | 53 | 54 | module JsonFunctions = 55 | let inline internal mustIncludeTypeInformationInSerializationFn (t: Type) = 56 | t.IsAbstract || not (isNull (t.GetCustomAttribute())) 57 | 58 | let internal mustIncludeTypeInformationInSerialization<'T> = 59 | mustIncludeTypeInformationInSerializationFn typeof<'T> 60 | 61 | /// Used internally, do not touch! 62 | let toJson<'T> o = 63 | let element = JsonValue.Serialize<'T> o 64 | // Remove the int64 Id property from the json format, as it is stored as a separate column. 65 | if HasTypeId<'T>.Value then 66 | match element with 67 | | Object o -> ignore (o.Remove "Id") 68 | | _ -> () 69 | element.ToJsonString() 70 | 71 | let internal toTypedJson<'T> o = 72 | let element = JsonValue.SerializeWithType<'T> o 73 | // Remove the int64 Id property from the json format, as it is stored as a separate column. 74 | if HasTypeId<'T>.Value then 75 | match element with 76 | | Object o -> ignore (o.Remove "Id") 77 | | _ -> () 78 | element.ToJsonString() 79 | 80 | /// Used internally, do not touch! 81 | let toSQLJson (item: obj) = 82 | match box item with 83 | | :? string as s -> s :> obj, false 84 | | :? char as c -> string c :> obj, false 85 | 86 | | :? Type as t -> t.FullName :> obj, false 87 | 88 | | :? int8 as x -> x :> obj, false 89 | | :? int16 as x -> x :> obj, false 90 | | :? int32 as x -> x :> obj, false 91 | | :? int64 as x -> x :> obj, false 92 | | :? nativeint as x -> x :> obj, false 93 | 94 | | :? uint8 as x -> x :> obj, false 95 | | :? uint16 as x -> x :> obj, false 96 | | :? uint32 as x -> x :> obj, false 97 | | :? uint64 as x -> x :> obj, false 98 | | :? unativeint as x -> x :> obj, false 99 | 100 | | :? float32 as x -> x :> obj, false 101 | | :? float as x -> x :> obj, false 102 | 103 | | _other -> 104 | 105 | let element = JsonValue.Serialize item 106 | match element with 107 | | Boolean b -> b :> obj, false 108 | | Null -> null, false 109 | | Number _ 110 | | String _ 111 | -> element.ToObject(), false 112 | | other -> 113 | // Cannot remove the Id value from here, maybe it is needed. 114 | other.ToJsonString(), true 115 | 116 | 117 | let internal fromJson<'T> (json: JsonValue) = 118 | match json with 119 | | Null when typeof = typeof<'T> -> 120 | nan :> obj :?> 'T 121 | | Null when typeof = typeof<'T> -> 122 | nanf :> obj :?> 'T 123 | | Null when typeof<'T>.IsValueType -> 124 | raise (InvalidOperationException "Invalid operation on a value type.") 125 | | json -> json.ToObject<'T>() 126 | 127 | /// Used internally, do not touch! 128 | let fromIdJson<'T> (element: JsonValue) = 129 | let id = element.["Id"].ToObject() 130 | let value = element.GetProperty("Value") |> fromJson<'T> 131 | 132 | id, value 133 | 134 | let internal fromSQLite<'R when 'R :> obj> (row: DbObjectRow) : 'R = 135 | let inline genericReinterpret (a: 'a when 'a : unmanaged) = 136 | // No allocations. 137 | Unsafe.As<'a, 'R>(&Unsafe.AsRef(&a)) 138 | 139 | if row :> obj = null then 140 | Unchecked.defaultof<'R> 141 | else 142 | 143 | // If the Id is NULL then the ValueJSON is a error message encoded in a JSON string. 144 | if not row.Id.HasValue then 145 | let exc = toJson row.ValueJSON 146 | raise (exn exc) 147 | 148 | // Checking if the SQLite returned a raw string. 149 | if typeof<'R> = typeof then 150 | row.ValueJSON :> obj :?> 'R 151 | else 152 | 153 | match typeof<'R> with 154 | | OfType float when row.ValueJSON <> null -> (genericReinterpret << float) row.ValueJSON 155 | | OfType float32 when row.ValueJSON <> null -> (genericReinterpret << float32) row.ValueJSON 156 | | OfType decimal -> (genericReinterpret << decimal) row.ValueJSON 157 | 158 | | OfType int8 -> (genericReinterpret << int8) row.ValueJSON 159 | | OfType uint8 -> (genericReinterpret << uint8) row.ValueJSON 160 | | OfType int16 -> (genericReinterpret << int16) row.ValueJSON 161 | | OfType uint16 -> (genericReinterpret << uint16) row.ValueJSON 162 | | OfType int32 -> (genericReinterpret << int32) row.ValueJSON 163 | | OfType uint32 -> (genericReinterpret << uint32) row.ValueJSON 164 | | OfType int64 -> (genericReinterpret << int64) row.ValueJSON 165 | | OfType uint64 -> (genericReinterpret << uint64) row.ValueJSON 166 | | OfType nativeint -> (genericReinterpret << nativeint) row.ValueJSON 167 | | OfType unativeint -> (genericReinterpret << unativeint) row.ValueJSON 168 | 169 | | _ -> 170 | 171 | 172 | match JsonValue.Parse row.ValueJSON with 173 | | Null when typeof = typeof<'R> -> 174 | Unchecked.defaultof<'R> 175 | | Null when typeof<'R>.IsValueType && typeof <> typeof<'R> && typeof <> typeof<'R> -> 176 | Unchecked.defaultof<'R> 177 | | json when typeof = typeof<'R> -> 178 | let id = row.Id.Value 179 | if json.JsonType = JsonValueType.Object && not (json.Contains "Id") && id >= 0 then 180 | json.["Id"] <- id 181 | 182 | json :> obj :?> 'R 183 | | json -> 184 | 185 | let mutable obj = fromJson<'R> json 186 | 187 | // An Id of -1 mean that it is an inserted object inside the IQueryable. 188 | if row.Id.Value <> -1 && HasTypeId<'R>.Value then 189 | HasTypeId<'R>.Write obj row.Id.Value 190 | obj -------------------------------------------------------------------------------- /SoloDB/Connections.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System.Data 4 | 5 | /// 6 | /// Contains types related to database connection management, pooling, and transactions. 7 | /// 8 | module Connections = 9 | open Microsoft.Data.Sqlite 10 | open SQLiteTools 11 | open System 12 | open System.Collections.Concurrent 13 | open System.Threading.Tasks 14 | open Utils 15 | 16 | /// 17 | /// Represents a specialized whose Dispose method is a no-op. 18 | /// This is used to pass a connection to a user-defined transaction block without it being closed prematurely. 19 | /// The actual disposal is handled by the . 20 | /// 21 | /// The connection string for the database. 22 | type TransactionalConnection internal (connectionStr: string) = 23 | inherit SqliteConnection(connectionStr) 24 | 25 | /// 26 | /// Performs the actual disposal of the base . 27 | /// This should only be called by the owning . 28 | /// 29 | /// If true, disposes managed resources. 30 | member internal this.DisposeReal(disposing) = 31 | base.Dispose disposing 32 | 33 | /// 34 | /// A static, reusable IDisposable object that performs no action upon disposal. 35 | /// 36 | static member private NoopDispose = { new IDisposable with override _.Dispose() = () } 37 | 38 | /// 39 | /// Implementation of the IDisableDispose interface. 40 | /// 41 | interface IDisableDispose with 42 | /// 43 | /// Returns a dummy IDisposable that does nothing. 44 | /// 45 | /// An IDisposable that can be safely "disposed" without affecting the connection. 46 | member this.DisableDispose(): IDisposable = 47 | TransactionalConnection.NoopDispose 48 | 49 | /// 50 | /// Overrides the default Dispose behavior to do nothing. This prevents the connection 51 | /// from being closed inside a 'use' binding within a transaction. 52 | /// 53 | /// Disposal flag. 54 | override this.Dispose(disposing) = 55 | // This is intentionally a no-op. 56 | () 57 | 58 | /// 59 | /// Manages a pool of reusable objects to reduce the overhead 60 | /// of opening and closing database connections. It also provides transaction management. 61 | /// 62 | /// The database connection string. 63 | /// An action to perform initial setup on a newly created connection. 64 | /// The database configuration settings. 65 | and ConnectionManager internal (connectionStr: string, setup: SqliteConnection -> unit, config: Types.SoloDBConfiguration) = 66 | /// A collection of all connections ever created by this manager, for disposal purposes. 67 | let all = ConcurrentStack() 68 | /// The pool of available, ready-to-use connections. 69 | let pool = ConcurrentStack() 70 | /// A flag indicating whether the manager has been disposed. 71 | let mutable disposed = false 72 | 73 | /// 74 | /// Checks if the manager has been disposed and throws an exception if it has. 75 | /// 76 | let checkDisposed () = 77 | if disposed then raise (ObjectDisposedException(nameof(ConnectionManager))) 78 | 79 | /// 80 | /// Returns a used connection to the pool. Before returning, it verifies that any 81 | /// explicit transactions on the connection have been completed. 82 | /// 83 | /// The connection to return to the pool. 84 | /// Thrown if the connection is still inside a transaction. 85 | member internal this.TakeBack(pooledConn: CachingDbConnection) = 86 | // SQLite does not support nested transactions, therefore we can use this to check if the user forgot to 87 | // end the transaction before returning it to the pool. 88 | try pooledConn.Execute("BEGIN; ROLLBACK;") |> ignore 89 | with 90 | | :? SqliteException as se when se.SqliteErrorCode = 1 && se.SqliteExtendedErrorCode = 1 -> 91 | ("The transaction must be finished before you return the connection to the pool.", se) |> InvalidOperationException |> raise 92 | 93 | pool.Push pooledConn 94 | 95 | /// 96 | /// Borrows a connection from the pool. If the pool is empty, a new connection is created. 97 | /// 98 | /// A ready-to-use . 99 | member this.Borrow() = 100 | checkDisposed() 101 | match pool.TryPop() with 102 | | true, c -> 103 | if c.Inner.State <> ConnectionState.Open then 104 | c.Inner.Open() 105 | c 106 | | false, _ -> 107 | let c = new CachingDbConnection(connectionStr, this.TakeBack, config) 108 | c.Inner.Open() 109 | setup c.Inner 110 | all.Push c 111 | c 112 | 113 | /// 114 | /// Gets a collection of all connections (both in-pool and in-use) created by this manager. 115 | /// Used for final disposal. 116 | /// 117 | member internal this.All = all 118 | 119 | /// 120 | /// Creates a new that will not be closed prematurely. 121 | /// 122 | /// A new, open . 123 | member internal this.CreateForTransaction() = 124 | checkDisposed() 125 | let c = new TransactionalConnection(connectionStr) 126 | c.Open() 127 | setup c 128 | c 129 | 130 | /// 131 | /// The core implementation for executing a synchronous function within a database transaction. 132 | /// It handles beginning the transaction and committing or rolling back based on the outcome. 133 | /// 134 | /// The function to execute within the transaction. 135 | /// The result of the function . 136 | member private this.WithTransactionBorrowed(f: CachingDbConnection -> 'T) = 137 | use connectionForTransaction = this.Borrow() 138 | connectionForTransaction.Execute("BEGIN IMMEDIATE;") |> ignore 139 | try 140 | connectionForTransaction.InsideTransaction <- true 141 | try 142 | let ret = f connectionForTransaction 143 | connectionForTransaction.Execute "COMMIT;" |> ignore 144 | ret 145 | with ex -> 146 | connectionForTransaction.Execute "ROLLBACK;" |> ignore 147 | reraise() 148 | finally 149 | connectionForTransaction.InsideTransaction <- false 150 | 151 | /// 152 | /// The core implementation for executing an asynchronous function within a database transaction. 153 | /// 154 | /// The asynchronous function to execute within the transaction. 155 | /// A task that represents the asynchronous operation, containing the result of the function . 156 | member private this.WithTransactionBorrowedAsync(f: CachingDbConnection -> Task<'T>) = task { 157 | use connectionForTransaction = this.Borrow() 158 | connectionForTransaction.Execute("BEGIN IMMEDIATE;") |> ignore 159 | try 160 | connectionForTransaction.InsideTransaction <- true 161 | try 162 | let! ret = f connectionForTransaction 163 | connectionForTransaction.Execute "COMMIT;" |> ignore 164 | return ret 165 | with ex -> 166 | connectionForTransaction.Execute "ROLLBACK;" |> ignore 167 | return reraiseAnywhere ex 168 | finally 169 | connectionForTransaction.InsideTransaction <- false 170 | } 171 | 172 | /// 173 | /// Executes a synchronous function within a database transaction using a pooled connection. 174 | /// 175 | /// The function to execute. 176 | /// The result of the function. 177 | member internal this.WithTransaction(f: CachingDbConnection -> 'T) = 178 | this.WithTransactionBorrowed f 179 | 180 | /// 181 | /// Executes an asynchronous function within a database transaction using a pooled connection. 182 | /// 183 | /// The asynchronous function to execute. 184 | /// A task representing the asynchronous transactional operation. 185 | member internal this.WithAsyncTransaction(f: CachingDbConnection -> Task<'T>) = task { 186 | return! this.WithTransactionBorrowedAsync f 187 | } 188 | 189 | /// 190 | /// Disposes the connection manager, which closes and disposes all connections it has created. 191 | /// 192 | interface IDisposable with 193 | override this.Dispose() = 194 | disposed <- true 195 | for c in all do 196 | c.DisposeReal() 197 | all.Clear() 198 | pool.Clear() 199 | () 200 | 201 | 202 | /// 203 | /// A discriminated union that represents the different types of database connections available within the system. 204 | /// This allows for abstracting over whether a connection is from a pool or part of an explicit transaction. 205 | /// 206 | and [] Connection = 207 | /// A connection sourced from a connection pool. 208 | | Pooled of pool: ConnectionManager 209 | /// A dedicated, non-disposing connection for an ongoing transaction. 210 | | Transactional of conn: TransactionalConnection 211 | /// A raw, pass-through connection, typically for internal operations. 212 | | Transitive of tc: SqliteConnection 213 | 214 | /// 215 | /// Gets an active based on the connection type. 216 | /// If Pooled, it borrows a connection. Otherwise, it returns the existing connection. 217 | /// 218 | /// An active . 219 | member this.Get() : SqliteConnection = 220 | match this with 221 | | Pooled pool -> pool.Borrow() 222 | | Transactional conn -> conn 223 | | Transitive c -> c 224 | 225 | /// 226 | /// Executes a synchronous function within a transaction. The behavior depends on the connection type. 227 | /// 228 | /// The function to execute within the transaction. 229 | /// The result of the function. 230 | /// Thrown if attempting to start a transaction on a simple Transitive connection. 231 | member this.WithTransaction(f: SqliteConnection -> 'T) = 232 | match this with 233 | | Pooled pool -> pool.WithTransaction f 234 | | Transactional conn -> 235 | f conn 236 | | Transitive conn when conn.IsWithinTransaction() -> 237 | f conn 238 | | Transitive _conn -> 239 | raise (InvalidOperationException "A simple Transitive Connection should never be used with a transation.") 240 | 241 | /// 242 | /// Executes an asynchronous function within a transaction. The behavior depends on the connection type. 243 | /// 244 | /// The asynchronous function to execute. 245 | /// A task representing the asynchronous operation. 246 | /// Thrown if attempting to start a transaction on a Transitive connection. 247 | member this.WithAsyncTransaction(f: SqliteConnection -> Task<'T>) = 248 | match this with 249 | | Pooled pool -> 250 | pool.WithAsyncTransaction f 251 | | Transactional conn -> 252 | f conn 253 | | Transitive _conn -> 254 | raise (InvalidOperationException "A Transitive Connection should never be used with a transation.") 255 | 256 | /// 257 | /// Extends the class with helper methods. 258 | /// 259 | and SqliteConnection with 260 | /// 261 | /// Checks if the connection is currently operating within a transaction managed by this system. 262 | /// 263 | /// true if the connection is within a transaction, otherwise false. 264 | member this.IsWithinTransaction() = 265 | match this with 266 | | :? TransactionalConnection -> true 267 | | :? CachingDbConnection as cc -> cc.InsideTransaction 268 | | _other -> false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoloDB - A Document Database With Full LINQ Support 2 | 3 | [SoloDB](https://solodb.org/) is a high-performance, lightweight, and robust embedded .NET database that elegantly combines the power of a NoSQL document store with the reliability of SQL. Built directly on top of SQLite and its native [JSONB](https://sqlite.org/jsonb.html) support, SoloDB offers a serverless, feature-rich experience, combining a simple [MongoDB](https://www.mongodb.com/)-like API with full LINQ support for expressive, strongly-typed queries. 4 | 5 | It is designed for developers who need a fast, reliable, and easy-to-use database solution without the overhead of a separate server. It's perfect for desktop applications, mobile apps (via .NET MAUI), and small to medium-sized web applications. 6 | 7 | I wrote a detailed comparison with a popular alternative, [LiteDB](https://github.com/litedb-org/LiteDB) — including benchmarks, API differences, and developer experience. [Read the article here](https://unconcurrent.com/articles/SoloDBvsLiteDB.html). 8 | 9 | ## Table of Contents 10 | 11 | - [Core Features](#core-features) 12 | - [Why SoloDB?](#why-solodb) 13 | - [Installation](#installation) 14 | - [Getting Started: A 60-Second Guide](#getting-started-a-60-second-guide) 15 | - [Usage and Examples](#usage-and-examples) 16 | - [Initializing the Database](#initializing-the-database) 17 | - [Working with Collections](#working-with-collections) 18 | - [Indexing for Performance](#indexing-for-performance) 19 | - [Atomic Transactions](#atomic-transactions) 20 | - [Storing Polymorphic Data](#storing-polymorphic-data) 21 | - [Custom ID Generation](#custom-id-generation) 22 | - [Integrated File Storage](#integrated-file-storage) 23 | - [Direct SQL Access](#direct-sql-access) 24 | - [F# Example](#f-example) 25 | - [Database Management](#database-management) 26 | - [Backups](#backups) 27 | - [Optimization](#optimization) 28 | - [License](#license) 29 | - [FAQ](#faq) 30 | 31 | ## Core Features 32 | 33 | SoloDB is packed with features that provide a seamless and powerful developer experience. 34 | 35 | - **SQLite Core**: Leverages the world's most deployed database engine, ensuring rock-solid stability, performance, and reliability. 36 | - **Serverless Architecture**: As a .NET library, it runs in-process with your application. No separate server or configuration is required. 37 | - **Hybrid NoSQL & SQL**: Store and query schemaless JSON documents with a familiar object-oriented API, or drop down to raw SQL for complex queries. 38 | - **Full LINQ Support**: Use the full power of LINQ and IQueryable to build expressive, strongly-typed queries against your data. 39 | - **ACID Transactions**: Guarantees atomicity, consistency, isolation, and durability for all operations, thanks to SQLite's transactional nature. 40 | - **Expressive Indexing**: Create unique or non-unique indexes on document properties for lightning-fast queries, using simple attributes. 41 | - **Integrated File Storage**: A robust, hierarchical file system API (similar to System.IO) for storing and managing large files and binary data directly within the database. 42 | - **Polymorphic Collections**: Store objects of different derived types within a single collection and query them by their base or concrete type. 43 | - **Thread-Safe**: A built-in connection pool ensures safe, concurrent access from multiple threads. 44 | - **Customizable ID Generation**: Use the default long primary key, or implement your own custom ID generation strategy (e.g., string, Guid). 45 | - **.NET Standard 2.0 & 2.1**: Broad compatibility with .NET Framework, .NET Core, and modern .NET runtimes. 46 | - **Open Source**: Licensed under the permissive LGPL-3.0. 47 | - **Documentation**: See the [official documentation](https://solodb.org/docs.html) for detailed guides. 48 | 49 | ## Why SoloDB? 50 | 51 | In a world of countless database solutions, SoloDB was created to fill a specific niche: to provide a simple, modern, and powerful alternative to document databases like MongoDB, but with the unmatched reliability and zero-configuration nature of SQLite. It's for developers who love the flexibility of NoSQL but don't want to sacrifice the transactional integrity and robustness of a traditional SQL database. 52 | 53 | ## Installation 54 | 55 | Install SoloDB directly from the NuGet Package Manager. 56 | 57 | ```bash 58 | dotnet add package SoloDB 59 | ``` 60 | 61 | ## Getting Started: A 60-Second Guide 62 | 63 | Here is a complete example to get you up and running instantly. 64 | 65 | ```csharp 66 | using SoloDatabase; 67 | using SoloDatabase.Attributes; 68 | 69 | // 1. Initialize the database (on-disk or in-memory) 70 | using var db = new SoloDB("my_app_data.db"); 71 | 72 | // 2. Get a strongly-typed collection 73 | var users = db.GetCollection(); 74 | 75 | // 3. Insert some data 76 | var user = new User 77 | { 78 | Name = "John Doe", 79 | Email = "john.doe@example.com", 80 | CreatedAt = DateTime.UtcNow 81 | }; 82 | users.Insert(user); 83 | Console.WriteLine($"Inserted user with auto-generated ID: {user.Id}"); 84 | 85 | // 4. Query your data with LINQ 86 | var foundUser = users.FirstOrDefault(u => u.Email == "john.doe@example.com"); 87 | if (foundUser != null) 88 | { 89 | Console.WriteLine($"Found user: {foundUser.Name}"); 90 | 91 | // 6. Update a document 92 | foundUser.Name = "Johnathan Doe"; 93 | users.Update(foundUser); 94 | Console.WriteLine("User has been updated."); 95 | } 96 | 97 | // 5. Delete a document 98 | users.Delete(user.Id); 99 | Console.WriteLine($"User deleted. Final count: {users.Count()}"); 100 | 101 | // Define your data model 102 | public class User 103 | { 104 | // A 'long Id' property is automatically used as the primary key. 105 | public long Id { get; set; } 106 | 107 | [Indexed] // Create an index on the 'Email' property for fast lookups. 108 | public string Email { get; set; } 109 | public string Name { get; set; } 110 | public DateTime CreatedAt { get; set; } 111 | } 112 | ``` 113 | 114 | ## Usage and Examples 115 | 116 | ### Initializing the Database 117 | 118 | You can create a database on disk for persistence or in-memory for temporary data and testing. 119 | 120 | ```csharp 121 | using SoloDatabase; 122 | 123 | // Create or open a database file on disk 124 | using var onDiskDB = new SoloDB("path/to/database.db"); 125 | 126 | // Create a named, shareable in-memory database 127 | using var sharedMemoryDB = new SoloDB("memory:my-shared-db"); 128 | ``` 129 | 130 | ### Working with Collections 131 | 132 | A collection is a container for your documents, analogous to a table in SQL. 133 | 134 | ```csharp 135 | // Get a strongly-typed collection. This is the recommended approach. 136 | var products = db.GetCollection(); 137 | 138 | // Get an untyped collection for dynamic scenarios. 139 | var untypedProducts = db.GetUntypedCollection("Product"); 140 | 141 | public class Product { /* ... */ } 142 | ``` 143 | 144 | ### Indexing for Performance 145 | 146 | Create indexes on properties to dramatically speed up query performance. Simply add the [Indexed] attribute to your model. 147 | 148 | ```csharp 149 | using SoloDatabase.Attributes; 150 | 151 | public class IndexedProduct 152 | { 153 | public long Id { get; set; } // The primary key is always indexed. 154 | 155 | [Indexed(unique: true)] // Create a unique index to enforce SKU uniqueness. 156 | public string SKU { get; set; } 157 | 158 | [Indexed] // Create a non-unique index for fast category lookups. 159 | public string Category { get; set; } 160 | public decimal Price { get; set; } 161 | } 162 | 163 | var products = db.GetCollection(); 164 | 165 | // This query will be very fast, using the index on 'Category'. 166 | var books = products.Where(p => p.Category == "Books").ToList(); 167 | ``` 168 | 169 | ### Atomic Transactions 170 | 171 | For operations that must either fully complete or not at all, use WithTransaction. If an exception is thrown inside the delegate, all database changes are automatically rolled back. 172 | 173 | ```csharp 174 | try 175 | { 176 | db.WithTransaction(tx => { 177 | var accounts = tx.GetCollection(); 178 | var fromAccount = accounts.GetById(1); 179 | var toAccount = accounts.GetById(2); 180 | 181 | fromAccount.Balance -= 100; 182 | toAccount.Balance += 100; 183 | 184 | accounts.Update(fromAccount); 185 | accounts.Update(toAccount); 186 | 187 | // If something fails here, both updates will be reverted. 188 | // throw new InvalidOperationException("Simulating a failure!"); 189 | }); 190 | } 191 | catch (Exception ex) 192 | { 193 | Console.WriteLine($"Transaction failed and was rolled back: {ex.Message}"); 194 | } 195 | ``` 196 | 197 | ### Storing Polymorphic Data 198 | 199 | Store different but related object types in the same collection. SoloDB automatically handles serialization and deserialization. 200 | 201 | ```csharp 202 | public abstract class Shape 203 | { 204 | public long Id { get; set; } 205 | public string Color { get; set; } 206 | } 207 | public class Circle : Shape { public double Radius { get; set; } } 208 | public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } } 209 | 210 | // Store all shapes in one collection 211 | var shapes = db.GetCollection(); 212 | shapes.Insert(new Circle { Color = "Red", Radius = 5.0 }); 213 | shapes.Insert(new Rectangle { Color = "Blue", Width = 4.0, Height = 6.0 }); 214 | 215 | // You can query for specific derived types using OfType() 216 | var circles = shapes.OfType().ToList(); 217 | Console.WriteLine($"Found {circles.Count} circle(s)."); 218 | ``` 219 | 220 | ### Custom ID Generation 221 | 222 | While the default long auto-incrementing ID is sufficient for most cases, you can define your own ID types and generation logic. 223 | 224 | ```csharp 225 | using SoloDatabase.Attributes; 226 | 227 | // 1. Define a custom ID generator 228 | public class GuidIdGenerator : IIdGenerator 229 | { 230 | public object GenerateId(ISoloDBCollection collection, MyObject item) 231 | { 232 | return Guid.NewGuid().ToString("N"); 233 | } 234 | 235 | public bool IsEmpty(object id) => string.IsNullOrEmpty(id as string); 236 | } 237 | 238 | // 2. Define the model with the custom ID 239 | public class MyObject 240 | { 241 | [SoloId(typeof(GuidIdGenerator))] 242 | public string Id { get; set; } 243 | public string Data { get; set; } 244 | } 245 | 246 | // 3. Use it 247 | var collection = db.GetCollection(); 248 | var newItem = new MyObject { Data = "Custom ID Test" }; 249 | collection.Insert(newItem); // newItem.Id is now populated with a GUID string. 250 | Console.WriteLine($"Generated ID: {newItem.Id}"); 251 | ``` 252 | 253 | ### Integrated File Storage 254 | 255 | SoloDB includes a powerful file storage system for managing binary data, large documents, or any kind of file. 256 | 257 | ```csharp 258 | using System.Text; 259 | 260 | var fs = db.FileSystem; 261 | var content = "This is the content of my file."; 262 | var contentBytes = Encoding.UTF8.GetBytes(content); 263 | 264 | // Upload data from a stream 265 | using (var stream = new MemoryStream(contentBytes)) 266 | { 267 | fs.Upload("/reports/report-2024.txt", stream); 268 | } 269 | 270 | // Set custom metadata 271 | fs.SetMetadata("/reports/report-2024.txt", "Author", "Ruslan"); 272 | 273 | // Download the file 274 | using (var targetStream = new MemoryStream()) 275 | { 276 | fs.Download("/reports/report-2024.txt", targetStream); 277 | targetStream.Position = 0; 278 | string downloadedContent = new StreamReader(targetStream).ReadToEnd(); 279 | Console.WriteLine($"Downloaded content: {downloadedContent}"); 280 | } 281 | 282 | // Check if a file exists 283 | bool exists = fs.Exists("/reports/report-2024.txt"); // true 284 | 285 | // Delete a file 286 | fs.DeleteFileAt("/reports/report-2024.txt"); 287 | ``` 288 | 289 | ### Direct SQL Access 290 | 291 | For complex scenarios not easily covered by LINQ, you can execute raw SQL commands directly. 292 | 293 | ```csharp 294 | using var pooledConnection = db.Connection.Borrow(); 295 | 296 | // Execute a command that doesn't return data 297 | pooledConnection.Execute("UPDATE Product SET Price = Price * 1.1 WHERE Category = 'Electronics'"); 298 | 299 | // Execute a query and map the first result to a value 300 | var highestPrice = pooledConnection.QueryFirst("SELECT MAX(Price) FROM Product"); 301 | 302 | // Execute a query and map results to objects 303 | var cheapProducts = pooledConnection.Query("SELECT * FROM Product WHERE Price < @MaxPrice", new { MaxPrice = 10.0 }); 304 | ``` 305 | 306 | ### F# Example 307 | 308 | SoloDB works seamlessly with F#. 309 | 310 | ```fsharp 311 | open SoloDatabase 312 | open System.Linq 313 | 314 | [] 315 | type MyFSharpType = { Id: int64; Name: string; Data: string } 316 | 317 | use db = new SoloDB("fsharp_demo.db") 318 | let collection = db.GetCollection() 319 | 320 | // Insert a document 321 | let data = { Id = 0L; Name = "F# Document"; Data = "Some data" } 322 | collection.Insert(data) |> ignore 323 | printfn "Inserted document with ID: %d" data.Id 324 | 325 | // Query all documents into an F# list 326 | let documents = collection.ToList() 327 | printfn "Found %d documents" documents.Count 328 | 329 | // Update a document 330 | let updatedData = { data with Data = "Updated F# data" } 331 | collection.Update(updatedData) 332 | 333 | // Delete a document 334 | let deleteCount = collection.Delete(data.Id) 335 | printfn "Deleted %d document(s)" deleteCount 336 | ``` 337 | 338 | ## Database Management 339 | 340 | ### Backups 341 | 342 | Create live backups of your database without interrupting application operations. 343 | 344 | ```csharp 345 | // Back up to another SoloDB instance 346 | using var backupDb = new SoloDB("path/to/backup.db"); 347 | db.BackupTo(backupDb); 348 | 349 | // Or vacuum the database into a new, clean file 350 | db.VacuumTo("path/to/optimized_backup.db"); 351 | ``` 352 | 353 | ### Optimization 354 | 355 | You can ask SQLite to analyze the database and potentially improve query plans. This runs automatically at startup but can also be triggered manually. 356 | 357 | ```csharp 358 | db.Optimize(); 359 | ``` 360 | 361 | ## License 362 | 363 | This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0). 364 | 365 | In addition, special permission is granted to distribute applications that incorporate an unmodified DLL of this library in Single-file deployments, Native AOT builds, and other bundling technologies that embed the library directly into the executable file. This ensures you can use modern .NET deployment strategies without violating the license. 366 | 367 | Full license details are available [here](https://solodb.org/legal.html). 368 | 369 | ## FAQ 370 | 371 | ### Why create this project? 372 | 373 | For fun, for profit, and to create a simpler, more integrated alternative to document databases like MongoDB, while retaining the unparalleled reliability and simplicity of SQLite. 374 | 375 | API is subject to change. 376 | -------------------------------------------------------------------------------- /SoloDB/Utils.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Collections.Concurrent 6 | open System.Security.Cryptography 7 | open System.IO 8 | open System.Text 9 | open System.Runtime.ExceptionServices 10 | open System.Runtime.InteropServices 11 | open System.Buffers 12 | open System.Reflection 13 | open System.Threading 14 | open System.Runtime.CompilerServices 15 | open System.Linq.Expressions 16 | 17 | 18 | // FormatterServices.GetSafeUninitializedObject for 19 | // types without a parameterless constructor. 20 | #nowarn "0044" 21 | 22 | module Utils = 23 | let isNull x = Object.ReferenceEquals(x, null) 24 | 25 | /// Returns a deterministic variable name based on a int. 26 | let internal getVarName (l: int) = 27 | $"V{l}" 28 | 29 | // For the use in F# Builders, like task { .. }. 30 | // https://stackoverflow.com/a/72132958/9550932 31 | let inline reraiseAnywhere<'a> (e: exn) : 'a = 32 | ExceptionDispatchInfo.Capture(e).Throw() 33 | Unchecked.defaultof<'a> 34 | 35 | type internal QueryPlan = 36 | static member val ExplainQueryPlanReference = sprintf "This is a string reference that will be used to determine in which special mode Aggregate() method will be used...%i" 435 37 | static member val GetGeneratedSQLReference = sprintf "This is a string reference that will be used to determine in which special mode Aggregate() method will be used...%i" 436 38 | 39 | let internal isTuple (t: Type) = 40 | not t.IsArray && 41 | (typeof.IsAssignableFrom t || typeof.IsAssignableFrom t || t.Name.StartsWith "Tuple`" || t.Name.StartsWith "ValueTuple`") 42 | 43 | type System.Char with 44 | static member IsAsciiLetterOrDigit this = 45 | (this >= '0' && this <= '9') || 46 | (this >= 'A' && this <= 'Z') || 47 | (this >= 'a' && this <= 'z') 48 | 49 | static member IsAsciiLetter this = 50 | (this >= 'A' && this <= 'Z') || 51 | (this >= 'a' && this <= 'z') 52 | 53 | type System.Decimal with 54 | static member IsInteger (this: Decimal) = 55 | this = (Decimal.Floor this) 56 | 57 | [] 58 | let internal (|OfType|_|) (_typ: 'x -> 'a) (objType: Type) = 59 | if typeof<'a>.Equals objType || typeof<'a>.IsAssignableFrom(objType) then ValueSome () else ValueNone 60 | 61 | let internal isNumber (value: obj) = 62 | match value with 63 | | :? int8 64 | | :? uint8 65 | | :? int16 66 | | :? uint16 67 | | :? int32 68 | | :? uint32 69 | | :? int64 70 | | :? uint64 71 | | :? nativeint 72 | 73 | | :? float32 74 | | :? float 75 | | :? decimal -> true 76 | | _ -> false 77 | 78 | let internal isIntegerBasedType (t: Type) = 79 | match t with 80 | | OfType int8 81 | | OfType uint8 82 | | OfType int16 83 | | OfType uint16 84 | | OfType int32 85 | | OfType uint32 86 | | OfType int64 87 | | OfType uint64 88 | | OfType nativeint 89 | -> true 90 | | _ -> false 91 | 92 | let inline internal isFloatBasedType (t: Type) = 93 | match t with 94 | | OfType float32 95 | | OfType float 96 | | OfType decimal 97 | -> true 98 | | _ -> false 99 | 100 | let internal isIntegerBased (value: obj) = 101 | match value with 102 | | :? int8 103 | | :? uint8 104 | | :? int16 105 | | :? uint16 106 | | :? int32 107 | | :? uint32 108 | | :? int64 109 | | :? uint64 110 | | :? nativeint 111 | -> true 112 | | _other -> false 113 | 114 | let internal typeToName (t: Type) = 115 | let fullname = t.FullName 116 | if fullname.Length > 0 && Char.IsAsciiLetter fullname.[0] // To not insert auto generated classes. 117 | then Some fullname 118 | else None 119 | 120 | let private nameToTypeCache = ConcurrentDictionary() 121 | 122 | let internal nameToType (typeName: string) = 123 | nameToTypeCache.GetOrAdd(typeName, fun typeName -> 124 | match typeName with 125 | | "Double" | "double" -> typeof 126 | | "Single" | "float" -> typeof 127 | | "Byte" | "byte" -> typeof 128 | | "SByte" | "sbyte" -> typeof 129 | | "Int16" | "short" -> typeof 130 | | "UInt16" | "ushort" -> typeof 131 | | "Int32" | "int" -> typeof 132 | | "UInt32" | "uint" -> typeof 133 | | "Int64" | "long" -> typeof 134 | | "UInt64" | "ulong" -> typeof 135 | | "Char" | "char" -> typeof 136 | | "Boolean" | "bool" -> typeof 137 | | "Object" | "object" -> typeof 138 | | "String" | "string" -> typeof 139 | | "Decimal" | "decimal" -> typeof 140 | | "DateTime" -> typeof 141 | | "Guid" -> typeof 142 | | "TimeSpan" -> typeof 143 | | "IntPtr" -> typeof 144 | | "UIntPtr" -> typeof 145 | | "Array" -> typeof 146 | | "Delegate" -> typeof 147 | | "MulticastDelegate" -> typeof 148 | | "IDisposable" -> typeof 149 | | "Stream" -> typeof 150 | | "Exception" -> typeof 151 | | "Thread" -> typeof 152 | | typeName -> 153 | 154 | match Type.GetType(typeName) with 155 | | null -> 156 | match Type.GetType("System." + typeName) with 157 | | null -> 158 | AppDomain.CurrentDomain.GetAssemblies() 159 | |> Seq.collect(fun a -> a.GetTypes()) 160 | |> Seq.find(fun t -> t.FullName = typeName) 161 | | fastType -> fastType 162 | | fastType -> fastType 163 | ) 164 | 165 | let private shaHashBytes (bytes: byte array) = 166 | use sha = SHA1.Create() 167 | sha.ComputeHash(bytes) 168 | 169 | let internal shaHash (o: obj) = 170 | match o with 171 | | :? (byte array) as bytes -> 172 | shaHashBytes(bytes) 173 | | :? string as str -> 174 | shaHashBytes(str |> Encoding.UTF8.GetBytes) 175 | | other -> raise (InvalidDataException(sprintf "Cannot hash object of type: %A" (other.GetType()))) 176 | 177 | let internal bytesToHex (hash: byte array) = 178 | let sb = new StringBuilder() 179 | for b in hash do 180 | sb.Append (b.ToString("x2")) |> ignore 181 | 182 | sb.ToString() 183 | 184 | 185 | // State machine states 186 | type internal State = 187 | | Valid // Currently valid base64 sequence 188 | | Invalid // Invalid sequence 189 | | PaddingOne // Seen one padding character 190 | | PaddingTwo // Seen two padding characters (max allowed) 191 | 192 | /// 193 | /// Finds the last index of valid base64 content in a string using a state machine approach 194 | /// 195 | /// String to check for base64 content 196 | /// Index of the last valid base64 character, or -1 if no valid base64 found 197 | let internal findLastValidBase64Index (input: string) = 198 | if System.String.IsNullOrEmpty(input) then 199 | -1 200 | else 201 | // Base64 alphabet check - optimized with lookup array 202 | let isBase64Char c = 203 | let code = c 204 | (code >= 'A' && code <= 'Z') || 205 | (code >= 'a' && code <= 'z') || 206 | (code >= '0' && code <= '9') || 207 | c = '+' || c = '/' || c = '=' 208 | 209 | 210 | // Track the last valid position 211 | let mutable lastValidPos = -1 212 | // Current position in the quadruplet (base64 works in groups of 4) 213 | let mutable quadPos = 0 214 | // Current state 215 | let mutable state = State.Valid 216 | 217 | for i = 0 to input.Length - 1 do 218 | let c = input.[i] 219 | 220 | match state with 221 | | State.Valid -> 222 | if not (isBase64Char c) then 223 | // Non-base64 character found 224 | state <- State.Invalid 225 | elif c = '=' then 226 | // Padding can only appear at positions 2 or 3 in a quadruplet 227 | if quadPos = 2 then 228 | state <- State.PaddingOne 229 | lastValidPos <- i 230 | elif quadPos = 3 then 231 | state <- State.PaddingTwo 232 | lastValidPos <- i 233 | else 234 | state <- State.Invalid 235 | else 236 | lastValidPos <- i 237 | quadPos <- (quadPos + 1) % 4 238 | 239 | | State.PaddingOne -> 240 | if c = '=' && quadPos = 3 then 241 | // Second padding character is only valid at position 3 242 | state <- State.PaddingTwo 243 | lastValidPos <- i 244 | quadPos <- 0 // Reset for next quadruplet 245 | else 246 | state <- State.Invalid 247 | 248 | | State.PaddingTwo -> 249 | // After two padding characters, we should start a new quadruplet 250 | if isBase64Char c && c <> '=' then 251 | state <- State.Valid 252 | quadPos <- 1 // Position 0 is this character 253 | lastValidPos <- i 254 | else 255 | state <- State.Invalid 256 | 257 | | State.Invalid -> 258 | // Once invalid, check if we can start a new valid sequence 259 | if isBase64Char c && c <> '=' then 260 | state <- State.Valid 261 | quadPos <- 1 // Position 0 is this character 262 | lastValidPos <- i 263 | 264 | // Final validation: for a complete valid base64 string, we need quadPos = 0 265 | // or a valid padding situation at the end 266 | match state with 267 | | State.Valid when quadPos = 0 -> lastValidPos 268 | | State.PaddingOne | State.PaddingTwo -> lastValidPos 269 | | _ -> 270 | // If we don't end with complete quadruplet, find the last complete one 271 | if lastValidPos >= 0 then 272 | let remainingChars = (lastValidPos + 1) % 4 273 | if remainingChars = 0 then 274 | lastValidPos 275 | else 276 | lastValidPos - remainingChars + 1 277 | else 278 | -1 279 | 280 | let internal trimToValidBase64 (input: string) = 281 | if System.String.IsNullOrEmpty(input) then 282 | input 283 | else 284 | let lastValidIndex = findLastValidBase64Index input 285 | 286 | if lastValidIndex >= 0 then 287 | if lastValidIndex = input.Length - 1 then 288 | // Already valid, no need to create a new string 289 | input 290 | else 291 | // Extract only the valid part 292 | input.Substring(0, lastValidIndex + 1) 293 | else 294 | // No valid base64 found 295 | "" 296 | 297 | let internal sqlBase64 (data: obj) = 298 | match data with 299 | | null -> null 300 | | :? (byte array) as bytes -> 301 | // Requirement #2: If BLOB, encode to base64 TEXT 302 | System.Convert.ToBase64String(bytes) :> obj 303 | | :? string as str -> 304 | // Requirement #6: Ignore leading and trailing whitespace 305 | let trimmedStr = str.Trim() 306 | let trimmedStr = trimToValidBase64 trimmedStr 307 | 308 | match trimmedStr.Length with 309 | | 0 -> Array.empty :> obj 310 | | _ -> 311 | 312 | System.Convert.FromBase64String trimmedStr :> obj 313 | | _ -> 314 | // Requirement #5: Raise an error for types other than TEXT, BLOB, or NULL 315 | failwith "The base64() function requires a TEXT, BLOB, or NULL argument" 316 | 317 | [] 318 | type internal GenericMethodArgCache = 319 | static member val private cache = ConcurrentDictionary({ 320 | new IEqualityComparer with 321 | override this.Equals (x: MethodInfo, y: MethodInfo): bool = 322 | x.MethodHandle.Value = y.MethodHandle.Value 323 | override this.GetHashCode (obj: MethodInfo): int = 324 | obj.MethodHandle.Value |> int 325 | }) 326 | 327 | static member Get(method: MethodInfo) = 328 | let args = GenericMethodArgCache.cache.GetOrAdd(method, (fun m -> m.GetGenericArguments())) 329 | args 330 | 331 | [] 332 | type internal GenericTypeArgCache = 333 | static member val private cache = ConcurrentDictionary({ 334 | new IEqualityComparer with 335 | override this.Equals (x: Type, y: Type): bool = 336 | x.TypeHandle.Value = y.TypeHandle.Value 337 | override this.GetHashCode (obj: Type): int = 338 | obj.TypeHandle.Value |> int 339 | }) 340 | 341 | static member Get(t: Type) = 342 | let args = GenericTypeArgCache.cache.GetOrAdd(t, (fun m -> m.GetGenericArguments())) 343 | args 344 | 345 | /// For the F# compiler to allow the implicit use of 346 | /// the .NET Expression we need to use it in a C# style class. 347 | [][] // make it static 348 | type internal ExpressionHelper = 349 | static member inline internal get<'a, 'b>(expression: Expression>) = expression 350 | 351 | static member inline internal id (x: Type) = 352 | let parameter = Expression.Parameter x 353 | Expression.Lambda(parameter, [|parameter|]) 354 | 355 | static member inline internal eq (x: Type) (b: obj) = 356 | let parameter = Expression.Parameter x 357 | Expression.Lambda(Expression.Equal(parameter, Expression.Constant(b, x)), [|parameter|]) 358 | 359 | static member inline internal min (e1: Expression) (e2: Expression) = 360 | // Produces an expression tree representing min(e1, e2) using LessThan 361 | Expression.Condition( 362 | Expression.LessThan(e1, e2), 363 | e1, 364 | e2 365 | ) 366 | 367 | static member inline internal constant<'T> (x: 'T) = 368 | // Returns a strongly-typed constant expression for value x 369 | Expression.Constant(x, typeof<'T>) 370 | 371 | module internal SeqExt = 372 | let internal sequentialGroupBy keySelector (sequence: seq<'T>) = 373 | seq { 374 | use enumerator = sequence.GetEnumerator() 375 | if enumerator.MoveNext() then 376 | let mutable currentKey = keySelector enumerator.Current 377 | let mutable currentList = System.Collections.Generic.List<'T>() 378 | let mutable looping = true 379 | 380 | while looping do 381 | let current = enumerator.Current 382 | let key = keySelector current 383 | 384 | if key = currentKey then 385 | currentList.Add current 386 | else 387 | yield currentList :> IList<'T> 388 | currentList.Clear() 389 | currentList.Add current 390 | currentKey <- key 391 | 392 | if not (enumerator.MoveNext()) then 393 | yield currentList :> IList<'T> 394 | looping <- false 395 | } -------------------------------------------------------------------------------- /SoloDB/SoloDBInterfaces.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System.Linq.Expressions 4 | open System.Runtime.CompilerServices 5 | open System.Linq 6 | open System.Data 7 | 8 | /// 9 | /// Represents a typed collection within a SoloDB database instance that supports LINQ querying and CRUD operations. 10 | /// 11 | /// The type of entities stored in this collection. Must be serializable. 12 | /// 13 | /// This interface extends to provide full LINQ support while adding 14 | /// database-specific operations like Insert, Update, Delete, and indexing capabilities. 15 | /// Supports polymorphic storage when is an abstract class or interface. 16 | /// 17 | /// 18 | type ISoloDBCollection<'T> = 19 | inherit IOrderedQueryable<'T> 20 | 21 | /// 22 | /// Gets the name of the collection within the database. 23 | /// 24 | /// 25 | /// A string representing the collection name, typically derived from the type name . 26 | /// 27 | /// 28 | /// 29 | /// SoloDB db = new SoloDB(...); 30 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 31 | /// Console.WriteLine($"Collection name: {collection.Name}"); // Outputs: "User" 32 | /// 33 | /// 34 | abstract member Name: string with get 35 | 36 | /// 37 | /// Gets a value indicating whether this collection is currently participating in a database transaction. 38 | /// 39 | /// 40 | /// true if the collection is within a transaction context; otherwise, false. 41 | /// 42 | /// 43 | /// When true, all operations are deferred until the transaction is committed or rolled back. 44 | /// 45 | /// 46 | abstract member InTransaction: bool with get 47 | 48 | /// 49 | /// Gets a value indicating whether type information is included in stored documents. 50 | /// 51 | /// 52 | /// true if type information is stored for polymorphic support; otherwise, false. 53 | /// 54 | /// 55 | /// Essential for abstract types and interfaces to enable proper deserialization of derived types. 56 | /// Can be forced using . 57 | /// 58 | /// 59 | /// 60 | /// SoloDB db = new SoloDB(...); 61 | /// 62 | /// // For abstract base class 63 | /// ISoloDBCollection<Animal> animals = db.GetCollection<Animal>(); 64 | /// Console.WriteLine($"Includes type info: {animals.IncludeType}"); // true 65 | /// 66 | /// // For concrete class 67 | /// ISoloDBCollection<User> users = db.GetCollection<User>(); 68 | /// Console.WriteLine($"Includes type info: {users.IncludeType}"); // false 69 | /// 70 | /// 71 | abstract member IncludeType: bool with get 72 | 73 | /// 74 | /// Inserts a new entity into the collection and returns the auto-generated unique identifier. 75 | /// 76 | /// The entity to insert. Any Id fields will be set. 77 | /// 78 | /// The auto-generated unique identifier assigned to the inserted entity. 79 | /// 80 | /// Thrown when item has an invalid Id value or generator. 81 | /// Thrown when the item would violate constraints. 82 | /// 83 | /// Uses custom ID generators specified by SoloIdAttribute or defaults to int64 auto-increment. 84 | /// Automatically creates indexes for properties marked with indexing attributes. 85 | /// 86 | /// 87 | /// 88 | /// SoloDB db = new SoloDB(...); 89 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 90 | /// 91 | /// User user = new User { Id = 0L, Name = "Alice", Email = "alice@example.com" }; 92 | /// long id = collection.Insert(user); 93 | /// Console.WriteLine($"Inserted with ID: {id}"); // Outputs auto-generated ID 94 | /// Console.WriteLine($"User ID updated: {user.Id}"); // Same as returned ID 95 | /// 96 | /// 97 | /// 98 | /// 99 | abstract member Insert: item: 'T -> int64 100 | 101 | /// 102 | /// Inserts a new entity or replaces an existing one based on unique indexes, returning the assigned identifier. 103 | /// 104 | /// The entity to insert or replace. 105 | /// 106 | /// The unique identifier of the inserted or replaced entity. 107 | /// 108 | /// Thrown when the item would violate constraints. 109 | /// 110 | /// Replacement occurs when the entity matches an existing record on any unique index. 111 | /// If no match is found, performs a standard insert operation. 112 | /// Useful for upsert scenarios where you want to avoid duplicate constraint violations. 113 | /// 114 | /// 115 | /// 116 | /// SoloDB db = new SoloDB(...); 117 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 118 | /// 119 | /// // First insert 120 | /// User user = new User { Id = 0L, Email = "alice@example.com", Name = "Alice" }; 121 | /// long id1 = collection.InsertOrReplace(user); 122 | /// Console.WriteLine($"First insert/replace ID: {id1}"); 123 | /// 124 | /// // Later upsert with same email (assuming unique index on Email) 125 | /// User updatedUser = new User { Id = 0L, Email = "alice@example.com", Name = "Alice Smith" }; 126 | /// long id2 = collection.InsertOrReplace(updatedUser); // id2 == id1 127 | /// Console.WriteLine($"Second insert/replace ID: {id2}"); 128 | /// 129 | /// 130 | /// 131 | /// 132 | abstract member InsertOrReplace: item: 'T -> int64 133 | 134 | /// 135 | /// Inserts multiple entities in a single atomic transaction, returning their assigned identifiers. 136 | /// 137 | /// The sequence of entities to insert. 138 | /// 139 | /// A list containing the unique identifiers assigned to each inserted entity, in the same order as the input sequence. 140 | /// 141 | /// Thrown when is null. 142 | /// Thrown when item has an invalid Id value or generator. 143 | /// Thrown when any item would violate constraints. 144 | /// 145 | /// All insertions occur within a single transaction - if any insertion fails, all are rolled back. 146 | /// Significantly more efficient than multiple individual calls. 147 | /// Empty sequences are handled gracefully and return an empty list. 148 | /// 149 | /// 150 | /// 151 | /// SoloDB db = new SoloDB(...); 152 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 153 | /// 154 | /// List<User> users = new List<User> 155 | /// { 156 | /// new User { Id = 0L, Name = "Alice", Email = "alice@example.com" }, 157 | /// new User { Id = 0L, Name = "Bob", Email = "bob@example.com" }, 158 | /// new User { Id = 0L, Name = "Charlie", Email = "charlie@example.com" } 159 | /// }; 160 | /// IList<long> ids = collection.InsertBatch(users); 161 | /// Console.WriteLine($"Inserted {users.Count} users with IDs: {string.Join(", ", ids)}"); 162 | /// 163 | /// 164 | /// 165 | /// 166 | abstract member InsertBatch: items: 'T seq -> System.Collections.Generic.IList 167 | 168 | /// 169 | /// Inserts or replaces multiple entities based on unique indexes in a single atomic transaction. 170 | /// 171 | /// The sequence of entities to insert or replace. 172 | /// 173 | /// A list containing the unique identifiers assigned to each entity, in the same order as the input sequence. 174 | /// 175 | /// Thrown when is null. 176 | /// Thrown when item has an invalid Id value or generator. 177 | /// Thrown when any item would violate constraints. 178 | /// 179 | /// Combines the efficiency of batch operations with upsert semantics. 180 | /// All operations occur within a single transaction for consistency. 181 | /// Throws if any entity has a pre-assigned non-zero ID to prevent confusion about intended behavior. 182 | /// 183 | /// 184 | /// 185 | /// SoloDB db = new SoloDB(...); 186 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 187 | /// 188 | /// List<User> users = new List<User> 189 | /// { 190 | /// new User { Id = 0L, Email = "alice@example.com", Name = "Alice" }, // Insert 191 | /// new User { Id = 0L, Email = "existing@example.com", Name = "Updated" } // Replace existing 192 | /// }; 193 | /// IList<long> ids = collection.InsertOrReplaceBatch(users); 194 | /// Console.WriteLine($"Inserted/Replaced {users.Count} users with IDs: {string.Join(", ", ids)}"); 195 | /// 196 | /// 197 | /// 198 | /// 199 | abstract member InsertOrReplaceBatch: items: 'T seq -> System.Collections.Generic.IList 200 | 201 | /// 202 | /// Attempts to retrieve an entity by its unique identifier, returning None if not found. 203 | /// 204 | /// The unique identifier of the entity to retrieve. 205 | /// 206 | /// Some(entity) if found; otherwise, None. 207 | /// 208 | /// 209 | /// Utilizes the primary key index for optimal O(log n) performance. 210 | /// 211 | /// 212 | abstract member TryGetById: id: int64 -> 'T option 213 | 214 | /// 215 | /// Retrieves an entity by its unique identifier, throwing an exception if not found. 216 | /// 217 | /// The unique identifier of the entity to retrieve. 218 | /// 219 | /// The entity with the specified identifier. 220 | /// 221 | /// Thrown when no entity with the specified ID exists. 222 | /// 223 | /// Utilizes the primary key index for optimal O(log n) performance. 224 | /// 225 | /// 226 | /// 227 | /// SoloDB db = new SoloDB(...); 228 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 229 | /// 230 | /// try 231 | /// { 232 | /// User user = collection.GetById(42L); 233 | /// Console.WriteLine($"Found user: {user.Name}"); 234 | /// } 235 | /// catch (KeyNotFoundException) 236 | /// { 237 | /// Console.WriteLine("User not found"); 238 | /// } 239 | /// 240 | /// 241 | /// 242 | abstract member GetById: id: int64 -> 'T 243 | 244 | /// 245 | /// Attempts to retrieve an entity using a custom identifier type, returning None if not found. 246 | /// 247 | /// The custom identifier of the entity to retrieve. 248 | /// The type of the custom identifier, such as string, Guid, or composite key types. 249 | /// 250 | /// Some(entity) if found; otherwise, None. 251 | /// 252 | /// 253 | /// The must match the type specified in the entity's SoloIdAttribute. 254 | /// 255 | /// 256 | /// 257 | abstract member TryGetById<'IdType when 'IdType : equality>: id: 'IdType -> 'T option 258 | 259 | /// 260 | /// Retrieves an entity using a custom identifier type, throwing an exception if not found. 261 | /// 262 | /// The custom identifier of the entity to retrieve. 263 | /// The type of the custom identifier. 264 | /// 265 | /// The entity with the specified custom identifier. 266 | /// 267 | /// Thrown when no entity with the specified ID exists. 268 | /// 269 | /// For entities using custom ID generators. 270 | /// The type parameter must exactly match the ID type defined in the entity. 271 | /// Utilizes a sqlite index for optimal O(log n) performance. 272 | /// 273 | /// 274 | /// 275 | /// SoloDB db = new SoloDB(...); 276 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 277 | /// 278 | /// try 279 | /// { 280 | /// // Retrieve by string ID 281 | /// User user = collection.GetById<string>("user_abc123"); 282 | /// Console.WriteLine($"Found user by string ID: {user.Name}"); 283 | /// 284 | /// // Or retrieve by GUID 285 | /// Guid guid = Guid.Parse(...); 286 | /// User userByGuid = collection.GetById<Guid>(guid); 287 | /// Console.WriteLine($"Found user by GUID: {userByGuid.Name}"); 288 | /// } 289 | /// catch (KeyNotFoundException) 290 | /// { 291 | /// Console.WriteLine("User not found with the specified custom ID."); 292 | /// } 293 | /// 294 | /// 295 | /// 296 | abstract member GetById<'IdType when 'IdType : equality>: id: 'IdType -> 'T 297 | 298 | /// 299 | /// Creates a non-unique index on the specified property to optimize query performance. 300 | /// 301 | /// A lambda expression identifying the property to index. 302 | /// The type of the property being indexed. 303 | /// 304 | /// The result of the Execute command on SQLite. 305 | /// 306 | /// Thrown when is null. 307 | /// Thrown when the expression doesn't reference a valid property. 308 | /// 309 | /// Indexes dramatically improve query performance for filtered and sorted operations. 310 | /// Non-unique indexes allow multiple documents to have the same indexed value. 311 | /// Index creation is idempotent - calling multiple times has no adverse effects. 312 | /// Indexes are persistent and maintained automatically during Insert/Update/Delete operations. 313 | /// 314 | /// 315 | /// 316 | /// SoloDB db = new SoloDB(...); 317 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 318 | /// 319 | /// // Create index on Name property for faster searches 320 | /// int indexEntries = collection.EnsureIndex(u => u.Name); 321 | /// Console.WriteLine($"Index on Name created with {indexEntries} entries."); 322 | /// 323 | /// // Create composite index (if supported) 324 | /// collection.EnsureIndex(u => new { u.City, u.Country }); 325 | /// 326 | /// // Now queries like this will be much faster: 327 | /// List<User> users = collection.Where(u => u.Name == "Alice").ToList(); 328 | /// 329 | /// 330 | /// 331 | /// 332 | abstract member EnsureIndex<'R>: expression: Expression> -> int 333 | 334 | /// 335 | /// Creates a unique constraint and index on the specified property, preventing duplicate values. 336 | /// 337 | /// A lambda expression identifying the property that must have unique values. 338 | /// The type of the property being indexed. 339 | /// 340 | /// The number of index entries created. 341 | /// 342 | /// Thrown when is null. 343 | /// Thrown when the expression references the already indexed Id property, the expression contains variables, or the expression is invalid in any other cases. 344 | /// Thrown when expression is not contant in relation to the parameter only. 345 | /// 346 | /// Combines the performance benefits of indexing with data integrity enforcement. 347 | /// Prevents insertion or updates that would create duplicate values. 348 | /// Essential for implementing business rules like unique email addresses or usernames. 349 | /// Can be used with for upsert operations. 350 | /// 351 | /// 352 | /// 353 | /// SoloDB db = new SoloDB(...); 354 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 355 | /// 356 | /// // Ensure email addresses are unique 357 | /// collection.EnsureUniqueAndIndex(u => u.Email); 358 | /// Console.WriteLine("Unique index on Email ensured."); 359 | /// 360 | /// // This will succeed 361 | /// collection.Insert(new User { Id = 0L, Email = "alice@example.com", Name = "Alice" }); 362 | /// 363 | /// // This will throw InvalidOperationException due to duplicate email 364 | /// try 365 | /// { 366 | /// collection.Insert(new User { Id = 0L, Email = "alice@example.com", Name = "Another Alice" }); 367 | /// } 368 | /// catch (InvalidOperationException) 369 | /// { 370 | /// Console.WriteLine("Duplicate email address detected as expected."); 371 | /// } 372 | /// 373 | /// 374 | /// 375 | /// 376 | abstract member EnsureUniqueAndIndex<'R>: expression: Expression> -> int 377 | 378 | /// 379 | /// Removes an existing index on the specified property if it exists. 380 | /// 381 | /// A lambda expression identifying the property whose index should be removed. 382 | /// The type of the property whose index is being removed. 383 | /// 384 | /// The number of index entries removed, or 0 if the index didn't exist. 385 | /// 386 | /// Thrown when is null. 387 | /// 388 | /// Safe operation that doesn't fail if the index doesn't exist. 389 | /// Useful for schema migrations or performance tuning scenarios. 390 | /// Removing an index will slow down queries that rely on it but frees up storage space. 391 | /// Cannot remove unique constraints that would result in data integrity violations. 392 | /// 393 | /// 394 | /// 395 | /// SoloDB db = new SoloDB(...); 396 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 397 | /// 398 | /// // Remove index on Name property 399 | /// int removedEntries = collection.DropIndexIfExists(u => u.Name); 400 | /// if (removedEntries > 0) 401 | /// { 402 | /// Console.WriteLine($"Removed index with {removedEntries} entries"); 403 | /// } 404 | /// else 405 | /// { 406 | /// Console.WriteLine("Index did not exist"); 407 | /// } 408 | /// 409 | /// 410 | /// 411 | /// 412 | abstract member DropIndexIfExists<'R>: expression: Expression> -> int 413 | 414 | /// 415 | /// Ensures that all indexes defined by attributes are created, including those added after collection creation. 416 | /// 417 | /// 418 | /// Automatically processes all properties marked with indexing attributes like SoloIndexAttribute. 419 | /// Useful for handling schema evolution where new indexes are added to existing types. 420 | /// Should be called after adding new indexing attributes to ensure they take effect. 421 | /// 422 | /// 423 | /// 424 | /// SoloDB db = new SoloDB(...); 425 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 426 | /// 427 | /// // After adding [SoloIndex] attribute to a property 428 | /// collection.EnsureAddedAttributeIndexes(); 429 | /// Console.WriteLine("All attribute-defined indexes are now guaranteed to exist."); 430 | /// 431 | /// 432 | /// 433 | /// 434 | abstract member EnsureAddedAttributeIndexes: unit -> unit 435 | 436 | /// 437 | /// Returns the internal SQLite connection object for advanced operations. 438 | /// 439 | /// 440 | /// The underlying instance. 441 | /// 442 | /// 443 | /// WARNING: This method is not intended for public usage and should be avoided. 444 | /// 445 | [] 446 | abstract member GetInternalConnection: unit -> Microsoft.Data.Sqlite.SqliteConnection 447 | 448 | /// 449 | /// Updates an existing entity in the database based on its identifier. 450 | /// 451 | /// The entity to update, which must have a valid non-zero identifier. 452 | /// Thrown when the entity has a bad identifier. 453 | /// Thrown when no entity with the specified ID exists. 454 | /// Thrown when the item would violate constraints. 455 | /// 456 | /// Replaces the entire entity in the database with the provided instance. 457 | /// The entity's identifier must be populated and match an existing record. 458 | /// Maintains referential integrity and enforces unique constraints. 459 | /// 460 | /// 461 | /// 462 | /// SoloDB db = new SoloDB(...); 463 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 464 | /// 465 | /// // Retrieve, modify, and update 466 | /// User userToUpdate = collection.GetById(42L); // Assume user with ID 42 exists 467 | /// if (userToUpdate != null) 468 | /// { 469 | /// userToUpdate.Name = "Updated Name"; 470 | /// userToUpdate.Email = "newemail@example.com"; 471 | /// collection.Update(userToUpdate); // Entire record is replaced 472 | /// Console.WriteLine($"User with ID 42 updated to Name: {userToUpdate.Name}, Email: {userToUpdate.Email}"); 473 | /// } 474 | /// 475 | /// // For mutable records (C# classes are mutable by default) 476 | /// User userData = collection.GetById(42L); // Assume user with ID 42 exists 477 | /// if (userData != null) 478 | /// { 479 | /// userData.Data = "New data"; 480 | /// collection.Update(userData); 481 | /// Console.WriteLine($"User with ID 42 data updated: {userData.Data}"); 482 | /// } 483 | /// 484 | /// 485 | /// 486 | /// 487 | abstract member Update: item: 'T -> unit 488 | 489 | /// 490 | /// Deletes an entity by its unique identifier. 491 | /// 492 | /// The unique identifier of the entity to delete. 493 | /// 494 | /// The number of entities deleted (0 if not found, 1 if successfully deleted). 495 | /// 496 | /// 497 | /// Safe operation that doesn't throw exceptions when the entity doesn't exist. 498 | /// Returns 0 if no entity with the specified ID exists. 499 | /// Returns 1 if the entity was successfully deleted. 500 | /// Automatically maintains index consistency. 501 | /// 502 | /// 503 | /// 504 | /// SoloDB db = new SoloDB(...); 505 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 506 | /// 507 | /// int deleteCount = collection.Delete(42L); 508 | /// switch (deleteCount) 509 | /// { 510 | /// case 0: 511 | /// Console.WriteLine("Entity with ID 42 was not found"); 512 | /// break; 513 | /// case 1: 514 | /// Console.WriteLine("Entity with ID 42 was successfully deleted"); 515 | /// break; 516 | /// default: 517 | /// Console.WriteLine("Unexpected result"); // Should never happen 518 | /// break; 519 | /// } 520 | /// 521 | /// 522 | /// 523 | /// 524 | abstract member Delete: id: int64 -> int 525 | 526 | /// 527 | /// Deletes an entity using a custom identifier type. 528 | /// 529 | /// The custom identifier of the entity to delete. 530 | /// The type of the custom identifier. 531 | /// 532 | /// The number of entities deleted (0 if not found, 1 if successfully deleted). 533 | /// 534 | /// 535 | /// For entities using custom ID types like string, Guid, or composite keys. 536 | /// The must match the entity's configured ID type. 537 | /// 538 | /// 539 | /// 540 | /// SoloDB db = new SoloDB(...); 541 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 542 | /// 543 | /// // Delete by string ID 544 | /// int count1 = collection.Delete<string>("user_abc123"); 545 | /// Console.WriteLine($"Deleted {count1} entity by string ID."); 546 | /// 547 | /// // Delete by GUID 548 | /// Guid guidToDelete = Guid.Parse("550e8400-e29b-41d4-a716-446655440000"); // Replace with an actual GUID 549 | /// int count2 = collection.Delete<Guid>(guidToDelete); 550 | /// Console.WriteLine($"Deleted {count2} entity by GUID."); 551 | /// 552 | /// 553 | /// 554 | abstract member Delete<'IdType when 'IdType : equality>: id: 'IdType -> int 555 | 556 | /// 557 | /// Deletes all entities that match the specified filter condition. 558 | /// 559 | /// A lambda expression defining the deletion criteria. 560 | /// 561 | /// The number of entities that were deleted. 562 | /// 563 | /// Thrown when is null. 564 | /// 565 | /// Performs a bulk delete operation in a single database command. 566 | /// More efficient than multiple individual delete operations. 567 | /// Returns 0 if no entities match the filter criteria. 568 | /// 569 | /// 570 | /// 571 | /// SoloDB db = new SoloDB(...); 572 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 573 | /// 574 | /// // Delete all inactive users 575 | /// int deletedCount = collection.DeleteMany(u => u.IsActive == false); 576 | /// Console.WriteLine($"Deleted {deletedCount} inactive users"); 577 | /// 578 | /// // Delete all users from a specific city 579 | /// int cityDeletions = collection.DeleteMany(u => u.City == "Old City"); 580 | /// Console.WriteLine($"Deleted {cityDeletions} users from Old City"); 581 | /// 582 | /// 583 | /// 584 | /// 585 | abstract member DeleteMany: filter: Expression> -> int 586 | 587 | /// 588 | /// Deletes the first entity that matches the specified filter condition. 589 | /// 590 | /// A lambda expression defining the deletion criteria. 591 | /// 592 | /// The number of entities deleted (0 if no match found, 1 if successfully deleted). 593 | /// 594 | /// Thrown when is null. 595 | /// 596 | /// Stops after deleting the first matching entity, even if multiple matches exist. 597 | /// The order of "first" depends on the underlying storage order, not insertion order. 598 | /// 599 | /// 600 | /// 601 | /// SoloDB db = new SoloDB(...); 602 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 603 | /// 604 | /// // Delete one user with a specific email 605 | /// int deleteCount = collection.DeleteOne(u => u.Email == "old@example.com"); 606 | /// if (deleteCount == 1) 607 | /// { 608 | /// Console.WriteLine("User deleted successfully"); 609 | /// } 610 | /// else 611 | /// { 612 | /// Console.WriteLine("No user found with that email"); 613 | /// } 614 | /// 615 | /// 616 | /// 617 | /// 618 | abstract member DeleteOne: filter: Expression> -> int 619 | 620 | /// 621 | /// Replaces all entities matching the filter condition with the provided entity. 622 | /// 623 | /// A lambda expression defining which entities to replace. 624 | /// The replacement entity data. 625 | /// 626 | /// The number of entities that were replaced. 627 | /// 628 | /// Thrown when the replacement would violate constraints. 629 | /// 630 | /// Each matching entity is completely replaced with the provided item data. 631 | /// The replacement item's ID is ignored - each replaced entity retains its original ID. 632 | /// More efficient than manual update loops for bulk replacement scenarios. 633 | /// 634 | /// 635 | /// 636 | /// SoloDB db = new SoloDB(...); 637 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 638 | /// 639 | /// User template = new User { Id = 0L, Status = "Updated", LastModified = DateTime.UtcNow }; 640 | /// 641 | /// // Replace all entities with old status 642 | /// int replaceCount = collection.ReplaceMany( 643 | /// u => u.Status == "Pending", 644 | /// template 645 | /// ); 646 | /// Console.WriteLine($"Updated {replaceCount} entities to new status"); 647 | /// 648 | /// 649 | /// 650 | /// 651 | abstract member ReplaceMany: filter: Expression> * item: 'T -> int 652 | 653 | /// 654 | /// Replaces the first entity matching the filter condition with the provided entity. 655 | /// 656 | /// A lambda expression defining which entity to replace. 657 | /// The replacement entity data. 658 | /// 659 | /// The number of entities replaced (0 if no match found, 1 if successfully replaced). 660 | /// 661 | /// Thrown when the replacement would violate constraints. 662 | /// 663 | /// Replaces only the first matching entity, preserving its original ID. 664 | /// The replacement item's ID field is ignored during the operation. 665 | /// 666 | /// 667 | /// 668 | /// SoloDB db = new SoloDB(...); 669 | /// ISoloDBCollection<User> collection = db.GetCollection<User>(); 670 | /// 671 | /// User newData = new User { Id = 0L, Name = "Updated Name", Email = "new@example.com" }; 672 | /// 673 | /// // Replace specific user 674 | /// int replaceCount = collection.ReplaceOne( 675 | /// u => u.Email == "old@example.com", 676 | /// newData 677 | /// ); 678 | /// if (replaceCount == 1) 679 | /// { 680 | /// Console.WriteLine("User successfully replaced"); 681 | /// } 682 | /// else 683 | /// { 684 | /// Console.WriteLine("No user found with that email to replace"); 685 | /// } 686 | /// 687 | /// 688 | /// 689 | /// 690 | abstract member ReplaceOne: filter: Expression> * item: 'T -> int 691 | 692 | /// 693 | /// Performs a partial update on all entities that match the specified filter condition. 694 | /// 695 | /// An array of lambda expressions defining the modifications to apply (e.g., item.Set(value) method calls). 696 | /// A lambda expression defining the criteria for which entities to update. 697 | /// 698 | /// The number of entities that were updated. 699 | /// 700 | /// Thrown when or is null. 701 | /// Thrown when the update would violate a database constraint (e.g., a unique index). 702 | /// 703 | /// It is required to perform only one update per expression. 704 | /// This method provides a efficient way to perform bulk partial updates. 705 | /// The actions should be item.Set(value) or col.Append(value) or col.SetAt(index,value) or col.RemoveAt(index) 706 | /// 707 | /// 708 | /// 709 | abstract member UpdateMany: filter: Expression> * [] transform: Expression> array -> int 710 | 711 | 712 | 713 | [] 714 | type UntypedCollectionExt = 715 | [] 716 | static member InsertBatchObj(collection: ISoloDBCollection, s: obj seq) = 717 | if isNull s then raise (System.ArgumentNullException(nameof s)) 718 | s |> Seq.map JsonSerializator.JsonValue.SerializeWithType |> collection.InsertBatch 719 | 720 | [] 721 | static member InsertObj(collection: ISoloDBCollection, o: obj) = 722 | o |> JsonSerializator.JsonValue.SerializeWithType |> collection.Insert -------------------------------------------------------------------------------- /SoloDB/MongoEmulation.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase.MongoDB 2 | 3 | open System.Linq.Expressions 4 | open System 5 | open System.Collections.Generic 6 | open System.Collections 7 | open System.IO 8 | open SoloDatabase 9 | open SoloDatabase.JsonSerializator 10 | open System.Runtime.CompilerServices 11 | open System.Runtime.InteropServices 12 | open System.Dynamic 13 | open System.Globalization 14 | open System.Reflection 15 | open System.Linq 16 | 17 | /// 18 | /// Represents a BSON document, which is a wrapper around a JsonValue to provide 19 | /// a dynamic and versatile way to interact with JSON/BSON data structures. 20 | /// It implements IDynamicMetaObjectProvider to allow for dynamic property access. 21 | /// 22 | /// The underlying JsonValue that this BsonDocument wraps. 23 | type BsonDocument (json: JsonValue) = 24 | /// 25 | /// Initializes a new, empty instance of the class, representing an empty JSON object. 26 | /// 27 | new () = BsonDocument (JsonValue.New()) 28 | 29 | /// 30 | /// Initializes a new instance of the class by serializing a given object. 31 | /// 32 | /// The object to serialize into the BsonDocument. 33 | new (objToSerialize: obj) = BsonDocument (JsonValue.Serialize objToSerialize) 34 | 35 | /// 36 | /// Gets the underlying of the BsonDocument. 37 | /// 38 | member this.Json = json 39 | 40 | /// 41 | /// Adds or updates a key-value pair in the BsonDocument. The value is serialized to a JsonValue. 42 | /// 43 | /// The key of the element to add or update. 44 | /// The value of the element to add or update. 45 | member this.Add (key: string, value: obj) = 46 | json.[key] <- JsonValue.Serialize value 47 | 48 | /// 49 | /// Retrieves a associated with the specified key. 50 | /// 51 | /// The key of the value to retrieve. 52 | /// The for the given key, or if the key is not found. 53 | member this.GetValue (key: string) : JsonValue = 54 | match json.TryGetProperty(key) with 55 | | true, v -> v 56 | | false, _ -> JsonValue.Null 57 | 58 | /// 59 | /// Removes a key-value pair from the BsonDocument. 60 | /// If the underlying structure is a list, the key is parsed to an int32 and used as index. 61 | /// 62 | /// The key or index of the element to remove. 63 | /// true if the element is successfully removed; otherwise, false. 64 | /// Thrown if the underlying data structure is not a JSON object or list. 65 | member this.Remove (key: string) : bool = 66 | match json with 67 | | JsonValue.Object(map) -> map.Remove(key) 68 | | JsonValue.List(l) -> 69 | let index = Int32.Parse(key, CultureInfo.InvariantCulture) 70 | if index < l.Count then 71 | l.RemoveAt(index) 72 | true 73 | else false 74 | | _ -> failwith "Invalid operation: the internal data structure is not an object." 75 | 76 | /// 77 | /// Determines whether the BsonDocument contains a specific item. 78 | /// For an object, it checks for key containment. For a list, it checks for value containment. 79 | /// 80 | /// The item to locate in the BsonDocument. 81 | /// true if the item is found; otherwise, false. 82 | /// Thrown if the operation is not applicable to the underlying JSON type. 83 | member this.Contains(item: obj) = 84 | let itemJson = JsonValue.Serialize item 85 | match json with 86 | | List l -> l.Contains itemJson 87 | | Object o -> o.Keys.Contains (itemJson.ToObject()) 88 | | other -> raise (InvalidOperationException (sprintf "Cannot call Contains(%A) on %A" itemJson other)) 89 | 90 | /// 91 | /// Deserializes the BsonDocument to an instance of the specified type. 92 | /// 93 | /// The type to deserialize the document into. 94 | /// An instance of type . 95 | member this.ToObject<'T>() = json.ToObject<'T>() 96 | 97 | /// 98 | /// Serializes this BsonDocument to its JSON string representation. 99 | /// 100 | /// A JSON string that represents the current BsonDocument. 101 | member this.ToJsonString () = json.ToJsonString () 102 | 103 | /// 104 | /// Gets the value of the BsonDocument as a . 105 | /// 106 | /// Thrown if the underlying value is not a boolean. 107 | member this.AsBoolean = match json with JsonValue.Boolean b -> b | _ -> raise (InvalidCastException("Not a boolean")) 108 | 109 | /// 110 | /// Gets the value of the BsonDocument as a BSON array (another BsonDocument wrapping a list). 111 | /// 112 | /// Thrown if the underlying value is not a JSON list. 113 | member this.AsBsonArray = match json with JsonValue.List _l -> BsonDocument(json) | _ -> raise (InvalidCastException("Not a BSON array")) 114 | 115 | /// 116 | /// Gets the value of the BsonDocument as a BSON document (another BsonDocument wrapping an object). 117 | /// 118 | /// Thrown if the underlying value is not a JSON object. 119 | member this.AsBsonDocument = match json with JsonValue.Object _o -> BsonDocument(json) | _ -> raise (InvalidCastException("Not a BSON document")) 120 | 121 | /// 122 | /// Gets the value of the BsonDocument as a . 123 | /// 124 | /// Thrown if the underlying value is not a string. 125 | member this.AsString = match json with JsonValue.String s -> s | _ -> raise (InvalidCastException("Not a string")) 126 | 127 | /// 128 | /// Gets the value of the BsonDocument as a . 129 | /// 130 | /// Thrown if the underlying value is not a number or is out of the Int32 range. 131 | member this.AsInt32 = match json with JsonValue.Number n when n <= decimal Int32.MaxValue && n >= decimal Int32.MinValue -> int n | _ -> raise (InvalidCastException("Not an int32")) 132 | 133 | /// 134 | /// Gets the value of the BsonDocument as a . 135 | /// 136 | /// Thrown if the underlying value is not a number or is out of the Int64 range. 137 | member this.AsInt64 = match json with JsonValue.Number n when n <= decimal Int64.MaxValue && n >= decimal Int64.MinValue -> int64 n | _ -> raise (InvalidCastException("Not an int64")) 138 | 139 | /// 140 | /// Gets the value of the BsonDocument as a . 141 | /// 142 | /// Thrown if the underlying value is not a number. 143 | member this.AsDouble = match json with JsonValue.Number n -> double n | _ -> raise (InvalidCastException("Not a double")) 144 | 145 | /// 146 | /// Gets the value of the BsonDocument as a . 147 | /// 148 | /// Thrown if the underlying value is not a number. 149 | member this.AsDecimal = match json with JsonValue.Number n -> n | _ -> raise (InvalidCastException("Not a decimal")) 150 | 151 | /// 152 | /// Gets a value indicating whether the BsonDocument represents a boolean. 153 | /// 154 | member this.IsBoolean = match json with JsonValue.Boolean _ -> true | _ -> false 155 | 156 | /// 157 | /// Gets a value indicating whether the BsonDocument represents a BSON array (JSON list). 158 | /// 159 | member this.IsBsonArray = match json with JsonValue.List _ -> true | _ -> false 160 | 161 | /// 162 | /// Gets a value indicating whether the BsonDocument represents a BSON document (JSON object). 163 | /// 164 | member this.IsBsonDocument = match json with JsonValue.Object _ -> true | _ -> false 165 | 166 | /// 167 | /// Gets a value indicating whether the BsonDocument represents a string. 168 | /// 169 | member this.IsString = match json with JsonValue.String _ -> true | _ -> false 170 | 171 | /// 172 | /// Gets a value indicating whether the BsonDocument represents a number that fits within an . 173 | /// 174 | member this.IsInt32 = match json with JsonValue.Number n when n <= decimal Int32.MaxValue && n >= decimal Int32.MinValue -> true | _ -> false 175 | 176 | /// 177 | /// Gets a value indicating whether the BsonDocument represents a number that fits within an . 178 | /// 179 | member this.IsInt64 = match json with JsonValue.Number n when n <= decimal Int64.MaxValue && n >= decimal Int64.MinValue -> true | _ -> false 180 | 181 | /// 182 | /// Gets a value indicating whether the BsonDocument represents a number. 183 | /// 184 | member this.IsDouble = match json with JsonValue.Number _ -> true | _ -> false 185 | 186 | /// 187 | /// Converts the BsonDocument's value to a using lenient conversion rules. 188 | /// Non-empty strings, non-zero numbers, lists, and objects convert to true. Null, empty strings and zero convert to false. 189 | /// 190 | /// The boolean representation of the value. 191 | member this.ToBoolean() = 192 | match json with 193 | | JsonValue.Boolean b -> b 194 | | JsonValue.String "" -> false 195 | | JsonValue.String _s -> true 196 | | JsonValue.Number n when n = decimal 0 -> false 197 | | JsonValue.Number _n -> true 198 | | JsonValue.List _l -> true 199 | | JsonValue.Object _o -> true 200 | | JsonValue.Null -> false 201 | 202 | /// 203 | /// Converts the BsonDocument's value to an . 204 | /// It can convert from numbers or numeric strings. 205 | /// 206 | /// The Int32 representation of the value. 207 | /// Thrown if the value cannot be converted to an Int32. 208 | member this.ToInt32() = 209 | match json with 210 | | JsonValue.Number n when n <= decimal Int32.MaxValue && n >= decimal Int32.MinValue -> int n 211 | | JsonValue.String s -> 212 | match Int32.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 213 | | true, i -> i 214 | | false, _ -> raise (InvalidCastException("Cannot convert to int32")) 215 | | _ -> raise (InvalidCastException("Cannot convert to int32")) 216 | 217 | /// 218 | /// Converts the BsonDocument's value to an . 219 | /// It can convert from numbers or numeric strings. 220 | /// 221 | /// The Int64 representation of the value. 222 | /// Thrown if the value cannot be converted to an Int64. 223 | member this.ToInt64() = 224 | match json with 225 | | JsonValue.Number n when n <= decimal Int64.MaxValue && n >= decimal Int64.MinValue -> int64 n 226 | | JsonValue.String s -> 227 | match Int64.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 228 | | true, i -> i 229 | | false, _ -> raise (InvalidCastException("Cannot convert to int64")) 230 | | _ -> raise (InvalidCastException("Cannot convert to int64")) 231 | 232 | /// 233 | /// Converts the BsonDocument's value to a . 234 | /// It can convert from numbers or numeric strings. 235 | /// 236 | /// The double-precision floating-point representation of the value. 237 | /// Thrown if the value cannot be converted to a double. 238 | member this.ToDouble() = 239 | match json with 240 | | JsonValue.Number n -> double n 241 | | JsonValue.String s -> 242 | match Double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 243 | | true, d -> d 244 | | false, _ -> raise (InvalidCastException("Cannot convert to double")) 245 | | _ -> raise (InvalidCastException("Cannot convert to double")) 246 | 247 | /// 248 | /// Converts the BsonDocument's value to a . 249 | /// It can convert from numbers or numeric strings. 250 | /// 251 | /// The decimal representation of the value. 252 | /// Thrown if the value cannot be converted to a decimal. 253 | member this.ToDecimal() = 254 | match json with 255 | | JsonValue.Number n -> n 256 | | JsonValue.String s -> 257 | match Decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 258 | | true, d -> d 259 | | false, _ -> raise (InvalidCastException("Cannot convert to decimal")) 260 | | _ -> raise (InvalidCastException("Cannot convert to decimal")) 261 | 262 | /// 263 | /// Deserializes a JSON string into a . 264 | /// 265 | /// The JSON string to parse. 266 | /// A new instance. 267 | static member Deserialize (jsonString: string) = 268 | BsonDocument (JsonValue.Parse (jsonString)) 269 | 270 | /// 271 | /// Returns the JSON string representation of the BsonDocument. 272 | /// 273 | /// A JSON string that represents the current object. 274 | override this.ToString () = this.ToJsonString () 275 | 276 | /// 277 | /// Gets or sets the value associated with the specified key. 278 | /// 279 | /// The key of the value to get or set. 280 | /// A new wrapping the value. 281 | member this.Item 282 | with get (key: string) : BsonDocument = 283 | json.[key] |> BsonDocument 284 | and set (key: string) (value: BsonDocument) = 285 | json.[key] <- value.Json 286 | 287 | /// 288 | /// Returns an enumerator that iterates through the key-value pairs of the BsonDocument. 289 | /// 290 | /// An enumerator for the collection. 291 | interface IEnumerable> with 292 | override this.GetEnumerator () = 293 | (json :> IEnumerable>).GetEnumerator () 294 | 295 | /// 296 | /// Returns an enumerator that iterates through the collection. 297 | /// 298 | /// An object that can be used to iterate through the collection. 299 | override this.GetEnumerator (): IEnumerator = 300 | (this :> IEnumerable>).GetEnumerator () :> IEnumerator 301 | 302 | /// 303 | /// Internal method used by the dynamic meta object for property binding. 304 | /// 305 | /// The name of the property to get. 306 | /// The property value, wrapped in a if it's a JsonValue. 307 | member internal this.GetPropertyForBinder(name: string) = 308 | match json.GetPropertyForBinder name with 309 | | :? JsonValue as v -> v |> BsonDocument |> box 310 | | other -> other 311 | 312 | /// 313 | /// Provides the implementation for dynamic operations on this BsonDocument. 314 | /// 315 | /// The expression representing this in the dynamic binding process. 316 | /// A to bind this dynamic operation. 317 | interface IDynamicMetaObjectProvider with 318 | member this.GetMetaObject(expression: Linq.Expressions.Expression): DynamicMetaObject = 319 | BsonDocumentMetaObject(expression, BindingRestrictions.Empty, this) 320 | 321 | /// 322 | /// Internal class that provides the dynamic behavior for . 323 | /// It intercepts dynamic member access, method calls, and conversions. 324 | /// 325 | and internal BsonDocumentMetaObject(expression: Expression, restrictions: BindingRestrictions, value: BsonDocument) = 326 | inherit DynamicMetaObject(expression, restrictions, value) 327 | 328 | /// Cached method info for GetPropertyForBinder. 329 | static member val private GetPropertyMethod = typeof.GetMethod("GetPropertyForBinder", BindingFlags.NonPublic ||| BindingFlags.Instance) 330 | /// Cached method info for the Item setter. 331 | static member val private SetPropertyMethod = typeof.GetMethod("set_Item") 332 | /// Cached method info for ToJsonString. 333 | static member val private ToJsonMethod = typeof.GetMethod("ToJsonString") 334 | /// Cached method info for Object.ToString. 335 | static member val private ToStringMethod = typeof.GetMethod("ToString") 336 | 337 | /// 338 | /// Binds the dynamic get member operation. 339 | /// 340 | override this.BindGetMember(binder: GetMemberBinder) : DynamicMetaObject = 341 | let resultExpression = Expression.Call( 342 | Expression.Convert(this.Expression, typeof), 343 | BsonDocumentMetaObject.GetPropertyMethod, 344 | Expression.Constant(binder.Name) 345 | ) 346 | DynamicMetaObject(resultExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 347 | 348 | /// 349 | /// Binds the dynamic set member operation. 350 | /// 351 | override this.BindSetMember(binder: SetMemberBinder, value: DynamicMetaObject) : DynamicMetaObject = 352 | let setExpression = Expression.Call( 353 | Expression.Convert(this.Expression, typeof), 354 | BsonDocumentMetaObject.SetPropertyMethod, 355 | Expression.Constant(binder.Name), 356 | value.Expression 357 | ) 358 | let returnExpression = Expression.Block(setExpression, value.Expression) 359 | DynamicMetaObject(returnExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 360 | 361 | /// 362 | /// Binds the dynamic convert operation. 363 | /// 364 | override this.BindConvert(binder: ConvertBinder) : DynamicMetaObject = 365 | let convertExpression = Expression.Call( 366 | Expression.Convert(this.Expression, typeof), 367 | BsonDocumentMetaObject.ToJsonMethod 368 | ) 369 | DynamicMetaObject(convertExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 370 | 371 | /// 372 | /// Binds the dynamic get index operation. 373 | /// 374 | override this.BindGetIndex(binder: GetIndexBinder, indexes: DynamicMetaObject[]) : DynamicMetaObject = 375 | if indexes.Length <> 1 then 376 | failwithf "BSON does not support indexes length <> 1: %i" indexes.Length 377 | let indexExpr = indexes.[0].Expression 378 | let resultExpression = Expression.Call( 379 | Expression.Convert(this.Expression, typeof), 380 | BsonDocumentMetaObject.GetPropertyMethod, 381 | Expression.Call(indexExpr, BsonDocumentMetaObject.ToStringMethod) 382 | ) 383 | DynamicMetaObject(resultExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 384 | 385 | /// 386 | /// Binds the dynamic set index operation. 387 | /// 388 | override this.BindSetIndex(binder: SetIndexBinder, indexes: DynamicMetaObject[], value: DynamicMetaObject) : DynamicMetaObject = 389 | if indexes.Length <> 1 then 390 | failwithf "BSON does not support indexes length <> 1: %i" indexes.Length 391 | let indexExpr = indexes.[0].Expression 392 | let setExpression = Expression.Call( 393 | Expression.Convert(this.Expression, typeof), 394 | BsonDocumentMetaObject.SetPropertyMethod, 395 | Expression.Call(indexExpr, BsonDocumentMetaObject.ToStringMethod), 396 | value.Expression 397 | ) 398 | let returnExpression = Expression.Block(setExpression, value.Expression) 399 | DynamicMetaObject(returnExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 400 | 401 | /// 402 | /// Returns the enumeration of all dynamic member names. 403 | /// 404 | override this.GetDynamicMemberNames() : IEnumerable = 405 | match value.Json with 406 | | JsonValue.Object o -> seq { for kv in o do yield kv.Key } 407 | | _ -> Seq.empty 408 | 409 | /// 410 | /// Represents the result of an InsertMany operation. 411 | /// 412 | type InsertManyResult = { 413 | /// 414 | /// The list of IDs of the inserted documents. 415 | /// 416 | Ids: IList 417 | /// 418 | /// The number of documents inserted. 419 | /// 420 | Count: int64 421 | } 422 | 423 | /// 424 | /// An internal proxy class to resolve method overloading ambiguity for the F# compiler. 425 | /// This specifically helps differentiate between ReplaceOne/ReplaceMany methods. 426 | /// 427 | [] 428 | type internal ProxyRef = 429 | /// 430 | /// Calls the ReplaceOne method on the collection with a specific parameter order. 431 | /// 432 | static member ReplaceOne (collection: SoloDatabase.ISoloDBCollection<'a>) (document: 'a) (filter: Expression>) = 433 | collection.ReplaceOne (filter, document) 434 | 435 | /// 436 | /// Calls the ReplaceMany method on the collection with a specific parameter order. 437 | /// 438 | static member ReplaceMany (collection: SoloDatabase.ISoloDBCollection<'a>) (document: 'a) (filter: Expression>) = 439 | collection.ReplaceMany (filter, document) 440 | 441 | /// 442 | /// Provides extension methods for 443 | /// to offer a MongoDB-like driver API. 444 | /// 445 | [] 446 | type CollectionExtensions = 447 | /// 448 | /// Counts the number of documents matching the given filter. 449 | /// 450 | /// The type of the document. 451 | /// The collection to query. 452 | /// The LINQ expression filter. 453 | /// The number of matching documents. 454 | [] 455 | static member CountDocuments<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) : int64 = 456 | collection.Where(filter).LongCount() 457 | 458 | /// 459 | /// Counts all documents in the collection. 460 | /// 461 | /// The type of the document. 462 | /// The collection to query. 463 | /// The total number of documents in the collection. 464 | [] 465 | static member CountDocuments<'a>(collection: SoloDatabase.ISoloDBCollection<'a>) : int64 = 466 | collection.LongCount() 467 | 468 | /// 469 | /// Finds all documents in the collection that match the given filter. 470 | /// 471 | /// The type of the document. 472 | /// The collection to query. 473 | /// The LINQ expression filter. 474 | /// An for the matching documents. 475 | [] 476 | static member Find<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 477 | collection.Where(filter) 478 | 479 | /// 480 | /// Inserts a single document into the collection. 481 | /// 482 | /// The type of the document. 483 | /// The collection to insert into. 484 | /// The document to insert. 485 | /// The ID of the inserted document. 486 | [] 487 | static member InsertOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, document: 'a) = 488 | collection.Insert document 489 | 490 | /// 491 | /// Inserts a sequence of documents into the collection. 492 | /// 493 | /// The type of the document. 494 | /// The collection to insert into. 495 | /// The documents to insert. 496 | /// An containing the IDs and count of inserted documents. 497 | [] 498 | static member InsertMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, documents: 'a seq) = 499 | let result = (collection.InsertBatch documents) 500 | { 501 | Ids = result 502 | Count = result.Count 503 | } 504 | 505 | /// 506 | /// Replaces a single document that matches the filter. 507 | /// 508 | /// The type of the document. 509 | /// The collection to update. 510 | /// The filter to find the document to replace. 511 | /// The new document. 512 | /// The number of documents replaced (0 or 1). 513 | [] 514 | static member ReplaceOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>, document: 'a) = 515 | ProxyRef.ReplaceOne collection document filter 516 | 517 | /// 518 | /// Replaces all documents that match the filter. 519 | /// 520 | /// The type of the document. 521 | /// The collection to update. 522 | /// The filter to find the documents to replace. 523 | /// The new document. 524 | /// The number of documents replaced. 525 | [] 526 | static member ReplaceMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>, document: 'a) = 527 | ProxyRef.ReplaceMany collection document filter 528 | 529 | /// 530 | /// Deletes a single document that matches the filter. Note: This implementation calls DeleteMany and may delete more than one if the filter is not specific enough. 531 | /// 532 | /// The type of the document. 533 | /// The collection to delete from. 534 | /// The filter to find the document to delete. 535 | /// The number of documents deleted. 536 | [] 537 | static member DeleteOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 538 | collection.DeleteMany(filter) 539 | 540 | /// 541 | /// Deletes all documents that match the filter. 542 | /// 543 | /// The type of the document. 544 | /// The collection to delete from. 545 | /// The filter to find the documents to delete. 546 | /// The number of documents deleted. 547 | [] 548 | static member DeleteMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 549 | collection.DeleteMany(filter) 550 | 551 | /// 552 | /// A private module containing helper functions for building expressions. 553 | /// 554 | module private Helper = 555 | /// 556 | /// Creates a property access expression from a string field path (e.g., "Customer.Address.Street"). 557 | /// Handles nested properties and indexer access for dynamic types like BsonDocument. 558 | /// Also handles mapping "_id" to "Id". 559 | /// 560 | /// The dot-separated path to the property. 561 | /// An expression tree representing the property access. 562 | let internal getPropertyExpression (fieldPath: string) : Expression> = 563 | let parameter = Expression.Parameter(typeof<'T>, "x") 564 | let fields = fieldPath.Split('.') 565 | 566 | let rec buildExpression (expr: Expression) (fields: string list) : Expression = 567 | match fields with 568 | | [] -> expr 569 | | field :: rest -> // Handle intermediate fields 570 | let field = 571 | if field = "_id" then "Id" 572 | else field 573 | 574 | let propertyOrField = 575 | if expr.Type.GetField(field) <> null || expr.Type.GetProperty(field) <> null then 576 | Expression.PropertyOrField(expr, field) :> Expression 577 | else 578 | // Fallback to indexer for dynamic types 579 | let indexExpr = 580 | Expression.MakeIndex(expr, expr.Type.GetProperty "Item", [Expression.Constant field]) :> Expression 581 | 582 | // If this is the last part of the path, we might need to convert the result 583 | if rest.IsEmpty && field <> "Id" && (expr.Type = typeof || expr.Type = typeof) then 584 | Expression.Call(indexExpr, expr.Type.GetMethod("ToObject").MakeGenericMethod(typeof<'TField>)) 585 | elif field = "Id" then 586 | Expression.Convert(Expression.Convert(indexExpr, typeof), typeof<'TField>) 587 | else 588 | indexExpr 589 | 590 | buildExpression propertyOrField rest 591 | 592 | let finalExpr = buildExpression (parameter :> Expression) (fields |> List.ofArray) 593 | Expression.Lambda>(finalExpr, parameter) 594 | 595 | /// 596 | /// A fluent builder for creating LINQ filter expressions. 597 | /// 598 | /// The type of the document to filter. 599 | type FilterDefinitionBuilder<'T> () = 600 | /// 601 | /// Internal list of accumulated filter expressions. 602 | /// 603 | let mutable filters = [] 604 | 605 | /// 606 | /// Gets an empty filter that matches all documents. 607 | /// 608 | member val Empty = Expression.Lambda>(Expression.Constant(true), Expression.Parameter(typeof<'T>, "x")) 609 | 610 | /// 611 | /// Adds an equality filter (field == value). 612 | /// 613 | /// The type of the field. 614 | /// An expression specifying the field. 615 | /// The value to compare against. 616 | /// The builder instance for chaining. 617 | member this.Eq<'TField>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 618 | let parameter = field.Parameters.[0] 619 | let body = Expression.Equal(field.Body, Expression.Constant(value, typeof<'TField>)) 620 | let lambda = Expression.Lambda>(body, parameter) 621 | filters <- lambda :: filters 622 | this 623 | 624 | /// 625 | /// Adds a greater-than filter (field > value). 626 | /// 627 | /// The type of the field, which must be comparable. 628 | /// An expression specifying the field. 629 | /// The value to compare against. 630 | /// The builder instance for chaining. 631 | member this.Gt<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 632 | let parameter = field.Parameters.[0] 633 | let body = Expression.GreaterThan(field.Body, Expression.Constant(value, typeof<'TField>)) 634 | let lambda = Expression.Lambda>(body, parameter) 635 | filters <- lambda :: filters 636 | this 637 | 638 | /// 639 | /// Adds a less-than filter (field < value). 640 | /// 641 | /// The type of the field, which must be comparable. 642 | /// An expression specifying the field. 643 | /// The value to compare against. 644 | /// The builder instance for chaining. 645 | member this.Lt<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 646 | let parameter = field.Parameters.[0] 647 | let body = Expression.LessThan(field.Body, Expression.Constant(value, typeof<'TField>)) 648 | let lambda = Expression.Lambda>(body, parameter) 649 | filters <- lambda :: filters 650 | this 651 | 652 | /// 653 | /// Adds an "in" filter, which checks if a field's value is in a given set of values. 654 | /// 655 | /// The type of the field. 656 | /// An expression specifying the field. 657 | /// The collection of values to check against. 658 | /// The builder instance for chaining. 659 | member this.In<'TField>(field: Expression>, values: IEnumerable<'TField>) : FilterDefinitionBuilder<'T> = 660 | let parameter = field.Parameters.[0] 661 | let expressions = 662 | values 663 | |> Seq.map (fun value -> 664 | Expression.Equal(field.Body, Expression.Constant(value, typeof<'TField>)) 665 | ) 666 | |> Seq.toList 667 | 668 | let combinedBody = 669 | expressions 670 | |> List.reduce (fun acc expr -> Expression.OrElse(acc, expr)) 671 | 672 | let lambda = Expression.Lambda>(combinedBody, parameter) 673 | filters <- lambda :: filters 674 | this 675 | 676 | /// 677 | /// Adds an equality filter using a string field name. 678 | /// 679 | /// The type of the field. 680 | /// The name of the field (can be a nested path). 681 | /// The value to compare against. 682 | /// The builder instance for chaining. 683 | member this.Eq<'TField>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 684 | this.Eq<'TField>(Helper.getPropertyExpression (field), value) 685 | 686 | /// 687 | /// Adds a greater-than filter using a string field name. 688 | /// 689 | /// The type of the field, which must be comparable. 690 | /// The name of the field (can be a nested path). 691 | /// The value to compare against. 692 | /// The builder instance for chaining. 693 | member this.Gt<'TField when 'TField :> IComparable>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 694 | this.Gt<'TField>(Helper.getPropertyExpression (field), value) 695 | 696 | /// 697 | /// Adds a less-than filter using a string field name. 698 | /// 699 | /// The type of the field, which must be comparable. 700 | /// The name of the field (can be a nested path). 701 | /// The value to compare against. 702 | /// The builder instance for chaining. 703 | member this.Lt<'TField when 'TField :> IComparable>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 704 | this.Lt<'TField>(Helper.getPropertyExpression (field), value) 705 | 706 | /// 707 | /// Adds an "in" filter using a string field name. 708 | /// 709 | /// The type of the field. 710 | /// The name of the field (can be a nested path). 711 | /// The collection of values to check against. 712 | /// The builder instance for chaining. 713 | member this.In<'TField>(field: string, values: IEnumerable<'TField>) : FilterDefinitionBuilder<'T> = 714 | this.In<'TField>(Helper.getPropertyExpression (field), values) 715 | 716 | /// 717 | /// Combines all registered filters into a single LINQ expression using 'AndAlso'. 718 | /// 719 | /// A single representing the combined filters. 720 | member this.Build() : Expression> = 721 | if filters.IsEmpty then 722 | Expression.Lambda>(Expression.Constant(true), Expression.Parameter(typeof<'T>, "x")) 723 | else 724 | let parameter = Expression.Parameter(typeof<'T>, "x") 725 | let combined = 726 | filters 727 | |> List.reduce (fun acc filter -> 728 | Expression.AndAlso(acc.Body, Expression.Invoke(filter, parameter)) 729 | |> Expression.Lambda>) 730 | combined 731 | 732 | /// 733 | /// Allows the builder to be implicitly converted to its resulting expression. 734 | /// 735 | /// The builder instance. 736 | /// The result of calling . 737 | static member op_Implicit(builder: FilterDefinitionBuilder<'T>) : Expression> = 738 | builder.Build() 739 | 740 | /// 741 | /// A fluent builder for creating query expressions. This is a wrapper around . 742 | /// 743 | /// The type of the document to query. 744 | type QueryDefinitionBuilder<'T>() = 745 | /// 746 | /// The underlying filter builder instance. 747 | /// 748 | let filterBuilder = new FilterDefinitionBuilder<'T>() 749 | 750 | /// 751 | /// Gets an empty query that matches all documents. 752 | /// 753 | member this.Empty = filterBuilder.Empty 754 | 755 | /// 756 | /// Adds an equality query condition (field == value). 757 | /// 758 | member this.EQ<'TField>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 759 | filterBuilder.Eq<'TField>(field, value) |> ignore 760 | this 761 | 762 | /// 763 | /// Adds a greater-than query condition (field > value). 764 | /// 765 | member this.GT<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 766 | filterBuilder.Gt<'TField>(field, value) |> ignore 767 | this 768 | 769 | /// 770 | /// Adds a less-than query condition (field < value). 771 | /// 772 | member this.LT<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 773 | filterBuilder.Lt<'TField>(field, value) |> ignore 774 | this 775 | 776 | /// 777 | /// Adds an "in" query condition. 778 | /// 779 | member this.IN<'TField>(field: Expression>, values: IEnumerable<'TField>) : QueryDefinitionBuilder<'T> = 780 | filterBuilder.In<'TField>(field, values) |> ignore 781 | this 782 | 783 | /// 784 | /// Adds an equality query condition using a string field name. 785 | /// 786 | member this.EQ<'TField>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 787 | filterBuilder.Eq<'TField>(field, value) |> ignore 788 | this 789 | 790 | /// 791 | /// Adds a greater-than query condition using a string field name. 792 | /// 793 | member this.GT<'TField when 'TField :> IComparable>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 794 | filterBuilder.Gt<'TField>(field, value) |> ignore 795 | this 796 | 797 | /// 798 | /// Adds a less-than query condition using a string field name. 799 | /// 800 | member this.LT<'TField when 'TField :> IComparable>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 801 | filterBuilder.Lt<'TField>(field, value) |> ignore 802 | this 803 | 804 | /// 805 | /// Adds an "in" query condition using a string field name. 806 | /// 807 | member this.IN<'TField>(field: string, values: IEnumerable<'TField>) : QueryDefinitionBuilder<'T> = 808 | filterBuilder.In<'TField>(field, values) |> ignore 809 | this 810 | 811 | /// 812 | /// Combines all query conditions into a single LINQ expression. 813 | /// 814 | /// A single representing the combined query. 815 | member this.Build() : Expression> = 816 | filterBuilder.Build() 817 | 818 | /// 819 | /// Allows the builder to be implicitly converted to its resulting expression. 820 | /// 821 | /// The builder instance. 822 | /// The result of calling . 823 | static member op_Implicit(builder: QueryDefinitionBuilder<'T>) : Expression> = 824 | builder.Build() 825 | 826 | /// 827 | /// Provides static access to filter and query builders, mimicking the MongoDB driver's `Builders` class. 828 | /// 829 | /// The document type for which to build queries. 830 | [] 831 | type Builders<'T> = 832 | /// 833 | /// Gets a new . 834 | /// 835 | static member Filter with get() = FilterDefinitionBuilder<'T> () 836 | 837 | /// 838 | /// Gets a new . 839 | /// 840 | static member Query with get() = QueryDefinitionBuilder<'T> () 841 | 842 | /// 843 | /// Represents a database, providing access to its collections. This is an internal wrapper around a instance. 844 | /// 845 | /// The underlying SoloDB instance. 846 | type MongoDatabase internal (soloDB: SoloDB) = 847 | /// 848 | /// Gets a collection with a specific document type. 849 | /// 850 | /// The document type. 851 | /// The name of the collection. 852 | /// An instance of . 853 | member this.GetCollection<'doc> (name: string) = 854 | soloDB.GetCollection<'doc> name 855 | 856 | /// 857 | /// Creates or gets an untyped collection, which will work with . 858 | /// 859 | /// The name of the collection. 860 | /// An untyped collection instance. 861 | member this.CreateCollection (name: string) = 862 | soloDB.GetUntypedCollection name 863 | 864 | /// 865 | /// Lists the names of all collections in the database. 866 | /// 867 | /// A sequence of collection names. 868 | member this.ListCollections () = 869 | soloDB.ListCollectionNames () 870 | 871 | /// 872 | /// Disposes the underlying database connection and resources. 873 | /// 874 | member this.Dispose () = 875 | soloDB.Dispose () 876 | 877 | /// 878 | /// Disposes the object. 879 | /// 880 | interface IDisposable with 881 | override this.Dispose (): unit = 882 | this.Dispose () 883 | 884 | /// 885 | /// Internal discriminated union to represent the storage location of a MongoClient. 886 | /// 887 | type internal MongoClientLocation = 888 | /// An on-disk database with a directory and a lock file. 889 | | Disk of {| Directory: DirectoryInfo; LockingFile: FileStream |} 890 | /// An in-memory database identified by a source string. 891 | | Memory of string 892 | 893 | /// 894 | /// The main client for connecting to a SoloDB data source with a MongoDB-like API. 895 | /// It can connect to on-disk or in-memory databases. 896 | /// 897 | /// 898 | /// The data source. For on-disk, this is a directory path. For in-memory, it should start with "memory:". 899 | /// 900 | /// Thrown if the source string starts with "mongodb://". 901 | type MongoClient(directoryDatabaseSource: string) = 902 | do if directoryDatabaseSource.StartsWith "mongodb://" then failwithf "SoloDB does not support mongo connections." 903 | 904 | /// 905 | /// The determined location (Disk or Memory) of the database. 906 | /// 907 | let location = 908 | if directoryDatabaseSource.StartsWith "memory:" then 909 | Memory directoryDatabaseSource 910 | else 911 | let directoryDatabasePath = Path.GetFullPath directoryDatabaseSource 912 | let directory = Directory.CreateDirectory directoryDatabasePath 913 | Disk {| 914 | Directory = directory 915 | LockingFile = File.Open(Path.Combine (directory.FullName, ".lock"), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite) 916 | |} 917 | 918 | /// 919 | /// A list of weak references to the databases created by this client. 920 | /// 921 | let connectedDatabases = ResizeArray> () 922 | /// 923 | /// A flag to indicate if the client has been disposed. 924 | /// 925 | let mutable disposed = false 926 | 927 | /// 928 | /// Gets a handle to a database. 929 | /// 930 | /// The name of the database. Defaults to "Master" if null. 931 | /// A instance. 932 | /// Thrown if the client has been disposed. 933 | member this.GetDatabase([] name : string) = 934 | if disposed then raise (ObjectDisposedException(nameof(MongoClient))) 935 | 936 | let name = match name with null -> "Master" | n -> n 937 | let name = name + ".solodb" 938 | let dbSource = 939 | match location with 940 | | Disk disk -> 941 | Path.Combine (disk.Directory.FullName, name) 942 | | Memory source -> 943 | source + "-" + name 944 | 945 | let db = new SoloDB (dbSource) 946 | let db = new MongoDatabase (db) 947 | 948 | let _remoteCount = connectedDatabases.RemoveAll(fun x -> match x.TryGetTarget() with false, _ -> true | true, _ -> false) 949 | connectedDatabases.Add (WeakReference db) 950 | 951 | db 952 | 953 | 954 | /// 955 | /// Disposes the client, along with all databases it has created and releases any file locks. 956 | /// 957 | interface IDisposable with 958 | override this.Dispose () = 959 | disposed <- true 960 | for x in connectedDatabases do 961 | match x.TryGetTarget() with 962 | | true, soloDB -> soloDB.Dispose() 963 | | false, _ -> () 964 | 965 | connectedDatabases.Clear() 966 | match location with 967 | | Disk disk -> 968 | disk.LockingFile.Dispose () 969 | | _other -> () --------------------------------------------------------------------------------