├── .gitattributes ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SoloDB.sln ├── SoloDB ├── Attributes.fs ├── Connections.fs ├── CustomTypeId.fs ├── Extensions.fs ├── FileStorage.fs ├── IdGenerator.fs ├── JsonFunctions.fs ├── JsonSerializator.fs ├── MongoEmulation.fs ├── QueryTranslator.fs ├── Queryable.fs ├── SQLiteTools.fs ├── SoloDB.fs ├── SoloDB.fsproj ├── SoloDBInterfaces.fs ├── SoloDBOperators.fs ├── Types.fs └── Utils.fs └── icon.png /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoloDB 2 | 3 | SoloDB is a light, fast and robust NoSQL and SQL embedded .NET database built on top of SQLite using the [JSONB](https://sqlite.org/jsonb.html) data type. 4 | 5 | ## Features 6 | 7 | Imagine the power of MongoDB and SQL combined. 8 | 9 | - [SQLite](https://sqlite.org/) at the core. 10 | - Serverless, it is a .NET library. 11 | - Simple API, similar to MongoDB, see the [below](#usage). 12 | - Thread safe using a connection pool. 13 | - [ACID](https://www.sqlite.org/transactional.html) with [full transaction support](#transactions). 14 | - File System for large files storage. 15 | - Support for polymorphic types. 16 | - [Reliable](https://sqlite.org/hirely.html) with a [WAL log file](https://www.sqlite.org/wal.html). 17 | - Support for [indexes](https://www.sqlite.org/expridx.html) for fast search. 18 | - Full [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/) and [IQueryable](https://learn.microsoft.com/en-us/dotnet/api/system.linq.iqueryable-1?view=net-9.0) support. 19 | - MongoDB inspired Custom ID Generation. 20 | - Direct SQL support. 21 | - [Open source](./LICENSE.txt). 22 | - [.NET Standard 2.0 and 2.1](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) 23 | - Pretty well tested: 600+ of tests, but in the tradition of SQLite, we keep them private. 24 | 25 | I wrote a detailed comparison with [LiteDB](https://github.com/litedb-org/LiteDB) — including benchmarks, API differences, and developer experience. I aimed for objectivity, but of course, it's subjective all the way down. 26 | [Read the article](https://unconcurrent.com/articles/SoloDBvsLiteDB.html). 27 | 28 | ## How to install 29 | 30 | #### From NuGet 31 | ```cmd 32 | dotnet add package SoloDB 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Initializing the Database 38 | 39 | You can specify either a file path or an in-memory database. 40 | 41 | ```csharp 42 | using SoloDatabase; 43 | 44 | using var onDiskDB = new SoloDB("path/to/database.db"); 45 | using var inMemoryDB = new SoloDB("memory:database-name"); 46 | ``` 47 | ### Creating and Accessing Collections 48 | 49 | ```csharp 50 | public class User 51 | { 52 | // Any int64 'Id' property is automatically synced with the SQLite's primary key. 53 | public long Id { get; set; } 54 | public string Name { get; set; } 55 | public int Age { get; set; } 56 | } 57 | 58 | using var db = new SoloDB("memory:my-app"); 59 | 60 | // Get a strongly-typed collection 61 | var users = db.GetCollection(); 62 | 63 | // Get an untyped collection (useful for dynamic scenarios) 64 | var untypedUsers = db.GetUntypedCollection("User"); 65 | ``` 66 | 67 | ### Custom ID Generation 68 | 69 | ```csharp 70 | using SoloDatabase.Attributes; 71 | using SoloDatabase.Types; 72 | using System.Linq; 73 | 74 | public class MyStringIdGenerator : IIdGenerator 75 | { 76 | public object GenerateId(ISoloDBCollection col, MyCustomIdType item) 77 | { 78 | var lastItem = col.OrderByDescending(x => long.Parse(x.Id)).FirstOrDefault(); 79 | long maxId = (lastItem == null) ? 0 : long.Parse(lastItem.Id); 80 | return (maxId + 1).ToString(); 81 | } 82 | 83 | public bool IsEmpty(object id) => string.IsNullOrEmpty(id as string); 84 | } 85 | 86 | public class MyCustomIdType 87 | { 88 | [SoloId(typeof(MyStringIdGenerator))] 89 | public string Id { get; set; } 90 | public string Data { get; set; } 91 | } 92 | 93 | var customIdCollection = db.GetCollection(); 94 | var newItem = new MyCustomIdType { Data = "Custom ID Test" }; 95 | customIdCollection.Insert(newItem); // newItem.Id will be populated by MyStringIdGenerator 96 | System.Console.WriteLine($"Generated ID: {newItem.Id}"); 97 | ``` 98 | 99 | ### Indexing Documents 100 | 101 | ```csharp 102 | using SoloDatabase.Attributes; 103 | 104 | public class IndexedProduct 105 | { 106 | public long Id { get; set; } // Implicitly indexed by SoloDB 107 | 108 | [Indexed(/* unique = */ true)] // Create a unique index on SKU 109 | public string SKU { get; set; } 110 | 111 | [Indexed(false)] // Create a non-unique index on Category 112 | public string Category { get; set; } 113 | public decimal Price { get; set; } 114 | } 115 | 116 | // ... 117 | var products = db.GetCollection(); 118 | products.Insert(new IndexedProduct { SKU = "BOOK-123", Category = "Books", Price = 29.99m }); 119 | 120 | // Verify unique index constraint. This will throw a unique constraint violation exception. 121 | try 122 | { 123 | products.Insert(new IndexedProduct { SKU = "BOOK-123", Category = "Fiction", Price = 19.99m }); 124 | } 125 | catch (Microsoft.Data.Sqlite.SqliteException ex) 126 | { 127 | System.Console.WriteLine($"Successfully caught expected exception: {ex.Message}"); 128 | } 129 | 130 | // Test querying with indexes 131 | var book = products.FirstOrDefault(p => p.SKU == "BOOK-123"); 132 | System.Console.WriteLine($"Found book with SKU BOOK-123: Price {book.Price}"); 133 | 134 | 135 | products.Insert(new IndexedProduct { SKU = "BOOK-456", Category = "Books", Price = 14.99m }); 136 | var booksInCategory = products.Where(p => p.Category == "Books").ToList(); 137 | System.Console.WriteLine($"Found {booksInCategory.Count} books in the 'Books' category."); 138 | 139 | ``` 140 | 141 | ### Transactions 142 | 143 | Use the `WithTransaction` method to execute a function within a transaction. 144 | 145 | ```csharp 146 | try 147 | { 148 | db.WithTransaction(tx => { 149 | var collection = tx.GetCollection(); 150 | // Perform operations within the transaction. 151 | collection.Insert(420); 152 | throw new System.OperationCanceledException("Simulating a rollback."); // Simulate a fail. 153 | }); 154 | } catch (System.OperationCanceledException) {} 155 | 156 | System.Console.WriteLine($"Collection exists after rollback: {db.CollectionExists()}"); // False 157 | ``` 158 | 159 | ### Polymorphic Types 160 | ```csharp 161 | public abstract class Shape 162 | { 163 | public long Id { get; set; } 164 | public string Color { get; set; } 165 | public abstract double CalculateArea(); 166 | } 167 | 168 | public class Circle : Shape 169 | { 170 | public double Radius { get; set; } 171 | public override double CalculateArea() => System.Math.PI * Radius * Radius; 172 | } 173 | 174 | public class Rectangle : Shape 175 | { 176 | public double Width { get; set; } 177 | public double Height { get; set; } 178 | public override double CalculateArea() => Width * Height; 179 | } 180 | 181 | // ... 182 | var shapes = db.GetCollection(); // Store as the base type 'Shape' 183 | 184 | shapes.Insert(new Circle { Color = "Red", Radius = 5.0 }); 185 | shapes.Insert(new Rectangle { Color = "Blue", Width = 4.0, Height = 6.0 }); 186 | 187 | // Get all circles 188 | var circles = shapes.OfType().ToList(); 189 | foreach (var circle in circles) 190 | { 191 | System.Console.WriteLine($"Red Circle - Radius: {circle.Radius}, Area: {circle.CalculateArea()}"); 192 | } 193 | 194 | // Get all shapes with Color "Blue" 195 | var blueShapes = shapes.Where(s => s.Color == "Blue").ToList(); 196 | foreach (var shape in blueShapes) 197 | { 198 | if (shape is Rectangle rect) 199 | { 200 | System.Console.WriteLine($"Blue Rectangle - Width: {rect.Width}, Height: {rect.Height}, Area: {rect.CalculateArea()}"); 201 | } 202 | } 203 | 204 | ``` 205 | 206 | 207 | ### Direct SQLite access using the build-in SoloDatabase.SQLiteTools.IDbConnectionExtensions. 208 | 209 | ```csharp 210 | using static SoloDatabase.SQLiteTools.IDbConnectionExtensions; 211 | ... 212 | 213 | using var pooledConnection = db.Connection.Borrow(); 214 | pooledConnection.Execute( 215 | "CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT, Age INTEGER)"); 216 | 217 | var insertSql = "INSERT INTO Users (Name, Age) VALUES (@Name, @Age) RETURNING Id;"; 218 | var userId = pooledConnection.QueryFirst(insertSql, new { Name = "John Doe", Age = 30 }); 219 | 220 | Assert.IsTrue(userId > 0, "Failed to insert new user and get a valid ID."); 221 | 222 | var queriedAge = pooledConnection.QueryFirst("SELECT Age FROM Users WHERE Name = 'John Doe'"); 223 | Assert.AreEqual(30, queriedAge); 224 | ``` 225 | 226 | 227 | ### Backing Up the Database 228 | 229 | You can create a backup of the database using the [`BackupTo`](https://www.sqlite.org/backup.html) or [`VacuumTo`](https://www.sqlite.org/lang_vacuum.html#vacuuminto) methods. 230 | 231 | ```csharp 232 | db.BackupTo(otherDb); 233 | db.VacuumTo("path/to/backup.db"); 234 | ``` 235 | 236 | 237 | ### Optimizing the Database 238 | 239 | The [`Optimize`](https://www.sqlite.org/pragma.html#pragma_optimize) method can optimize the database using statistically information, it runs automatically on startup. 240 | 241 | ```csharp 242 | db.Optimize(); 243 | ``` 244 | ### File storage 245 | 246 | ```csharp 247 | using SoloDatabase; 248 | using SoloDatabase.FileStorage; 249 | using System.IO; 250 | using System.Text; 251 | 252 | var fs = db.FileSystem; 253 | var randomBytes = new byte[256]; 254 | System.Random.Shared.NextBytes(randomBytes); 255 | 256 | // Create a directory and set metadata 257 | var directory = fs.GetOrCreateDirAt("/my_documents/reports"); 258 | fs.SetDirectoryMetadata(directory, "Sensitivity", "Confidential"); 259 | var dirInfo = fs.GetDirAt("/my_documents/reports"); 260 | System.Console.WriteLine($"Directory '/my_documents/reports' metadata 'Sensitivity': {dirInfo.Metadata["Sensitivity"]}"); 261 | 262 | // Upload a file and set metadata 263 | string filePath = "/my_documents/reports/annual_report.txt"; 264 | using (var ms = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes("This is a test report."))) 265 | { 266 | fs.Upload(filePath, ms); 267 | } 268 | fs.SetMetadata(filePath, "Author", "Jane Doe"); 269 | fs.SetFileCreationDate(filePath, System.DateTimeOffset.UtcNow.AddDays(-7)); 270 | var fileInfo = fs.GetAt(filePath); 271 | System.Console.WriteLine($"File '{fileInfo.Name}' author: {fileInfo.Metadata["Author"]}"); 272 | 273 | // Write at a specific offset (sparse file) 274 | string sparseFilePath = "/my_documents/sparse_file.dat"; 275 | fs.WriteAt(sparseFilePath, 1024 * 1024, randomBytes); // Write at 1MB offset 276 | var readData = fs.ReadAt(sparseFilePath, 1024 * 1024, randomBytes.Length); 277 | System.Console.WriteLine($"Sparse file write/read successful: {System.Linq.Enumerable.SequenceEqual(randomBytes, readData)}"); 278 | 279 | // Download file content 280 | using (var targetStream = new System.IO.MemoryStream()) 281 | { 282 | fs.Download(filePath, targetStream); 283 | targetStream.Position = 0; 284 | string content = new System.IO.StreamReader(targetStream).ReadToEnd(); 285 | System.Console.WriteLine($"Downloaded content: {content}"); 286 | } 287 | 288 | // Recursively list entries 289 | var entries = fs.RecursiveListEntriesAt("/my_documents"); 290 | System.Console.WriteLine($"Found {entries.Count} entries recursively under /my_documents."); 291 | 292 | // Move a file to a new location (and rename it) 293 | fs.MoveFile(filePath, "/archive/annual_report_2023.txt"); 294 | bool originalExists = fs.Exists(filePath); // false 295 | bool newExists = fs.Exists("/archive/annual_report_2023.txt"); // true 296 | System.Console.WriteLine($"Original file exists after move: {originalExists}. New file exists: {newExists}"); 297 | 298 | // Bulk upload multiple files 299 | var bulkFiles = new System.Collections.Generic.List 300 | { 301 | // The constructor allows setting path, data, and optional timestamps. 302 | new("/bulk_uploads/file1.log", System.Text.Encoding.UTF8.GetBytes("Log entry 1"), null, null), 303 | new("/bulk_uploads/images/pic.jpg", randomBytes, null, null) 304 | }; 305 | fs.UploadBulk(bulkFiles); 306 | System.Console.WriteLine($"Bulk upload successful. File exists: {fs.Exists("/bulk_uploads/images/pic.jpg")}"); 307 | 308 | // Use SoloFileStream for controlled writing 309 | using (var fileStream = fs.OpenOrCreateAt("/important_data/critical.bin")) 310 | { 311 | fileStream.Write(randomBytes, 0, 10); 312 | } 313 | var criticalFileInfo = fs.GetAt("/important_data/critical.bin"); 314 | System.Console.WriteLine($"SoloFileStream created file with size: {criticalFileInfo.Size} and a valid hash."); 315 | 316 | ``` 317 | 318 | 319 | ### Example Usage 320 | 321 | Here is an example of how to use SoloDB to manage a collection of documents in C#: 322 | 323 | #### SoloDB 324 | ```csharp 325 | using SoloDatabase; 326 | using SoloDatabase.Attributes; 327 | using System.Linq; 328 | 329 | public class MyDataType 330 | { 331 | public long Id { get; set; } 332 | [Indexed(/* unique = */ false)] 333 | public string Name { get; set; } 334 | public string Data { get; set; } 335 | } 336 | 337 | // using var db = new SoloDB(...); 338 | 339 | var collection = db.GetCollection(); 340 | 341 | // Insert a document 342 | var newDoc = new MyDataType { Name = "Document 1", Data = "Some data" }; 343 | collection.Insert(newDoc); // Id will be auto-generated and set on newDoc.Id 344 | System.Console.WriteLine($"Inserted document with ID: {newDoc.Id}"); 345 | 346 | 347 | // If the Id property does not exist, then you can use the return value of Insert. 348 | var dataToInsert = new MyDataType { Name = "Document 2", Data = "More data" }; 349 | var docId = collection.Insert(dataToInsert); 350 | System.Console.WriteLine($"Inserted document, ID from object: {docId}"); 351 | 352 | // Query all documents into a C# list 353 | var allDocuments = collection.ToList(); 354 | System.Console.WriteLine($"Total documents: {allDocuments.Count}"); 355 | 356 | var documentsData = collection.Where(d => d.Name.StartsWith("Document")) 357 | .Select(d => d.Data) 358 | .ToList(); 359 | 360 | // Update a document 361 | var docToUpdate = collection.GetById(docId); 362 | docToUpdate.Data = "Updated data for Document 2"; 363 | collection.Update(docToUpdate); 364 | 365 | // Verify the update 366 | var updatedDoc = collection.GetById(docId); 367 | System.Console.WriteLine($"Updated data: {updatedDoc.Data}"); // "Updated data for Document 2" 368 | 369 | // Delete the first document by its primary key 370 | int deleteCount = collection.Delete(docId); 371 | System.Console.WriteLine($"Documents deleted: {deleteCount}"); // 1 372 | 373 | // Verify the final count 374 | System.Console.WriteLine($"Final document count: {collection.Count()}"); // 1 375 | 376 | ``` 377 | 378 | And a simple one in F#: 379 | 380 | #### SoloDB 381 | ```fsharp 382 | [] 383 | type MyType = { Id: int64; Name: string; Data: string } 384 | 385 | use db = new SoloDB("./mydatabase.db") 386 | let collection = db.GetCollection() 387 | 388 | // Insert a document 389 | let docId = collection.Insert({ Id = 0; Name = "Document 1"; Data = "Some data" }) 390 | 391 | // Or 392 | 393 | let data = { Id = 0; Name = "Document 1"; Data = "Some data" } 394 | collection.Insert(data) |> ignore 395 | printfn "%A" data.Id // 2 396 | 397 | // Query all documents into a F# list 398 | let documents = collection.ToList() 399 | 400 | // Query the Data property, where Name starts with 'Document' 401 | let documentsData = collection.Where(fun d -> d.Name.StartsWith "Document").Select(fun d -> d.Data).ToList() 402 | 403 | let data = {data with Data = "Updated data"} 404 | 405 | // Update a document 406 | collection.Update(data) 407 | 408 | // Delete a document 409 | let count = collection.Delete(data.Id) // 1 410 | ``` 411 | ### Licence 412 | This project is licensed under [LGPL-3.0](./LICENSE.txt), with the additional permission to distribute applications that incorporate an unmodified DLL of this library in Single-file deployment, Native AOT, and other bundling technologies that embed the library into the executable. 413 | 414 | ## FAQ 415 | 416 | ### Why create this project? 417 | - For fun and profit, and to have a more simple alternative to MongoDB with the reliability of SQLite. 418 | 419 | ##### Footnote 420 | 421 | ###### API is subject to change. 422 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SoloDB/Attributes.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase.Attributes 2 | open System 3 | 4 | /// 5 | /// If included, the DB will index this property, on: 6 | /// a) Only on the first initialization of the collection for this type in the storage medium(disk or memory); 7 | /// b) On calling the SoloDatabase.Collection.EnsureAddedAttributeIndexes(); 8 | /// 9 | [] 10 | type IndexedAttribute(unique: bool) = 11 | inherit System.Attribute() 12 | 13 | new() = IndexedAttribute(false) 14 | 15 | member val Unique = unique 16 | 17 | /// 18 | /// If included, the DB will store the type information. 19 | /// 20 | [] 21 | type PolimorphicAttribute() = 22 | inherit System.Attribute() 23 | 24 | [] 25 | [] 26 | type SoloId(idGenerator: Type) = 27 | inherit IndexedAttribute(true) 28 | 29 | member val IdGenerator = idGenerator -------------------------------------------------------------------------------- /SoloDB/Connections.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System.Data 4 | 5 | module Connections = 6 | open Microsoft.Data.Sqlite 7 | open SQLiteTools 8 | open System 9 | open System.Collections.Concurrent 10 | open System.Threading.Tasks 11 | open Utils 12 | 13 | type TransactionalConnection internal (connectionStr: string) = 14 | inherit SqliteConnection(connectionStr) 15 | 16 | member internal this.DisposeReal(disposing) = 17 | base.Dispose disposing 18 | 19 | override this.Dispose(disposing) = 20 | // Noop 21 | () 22 | 23 | and ConnectionManager internal (connectionStr: string, setup: SqliteConnection -> unit, config: Types.SoloDBConfiguration) = 24 | let all = ConcurrentStack() 25 | let pool = ConcurrentStack() 26 | let mutable disposed = false 27 | 28 | let checkDisposed () = 29 | if disposed then raise (ObjectDisposedException(nameof(ConnectionManager))) 30 | 31 | member internal this.TakeBack(pooledConn: CachingDbConnection) = 32 | // SQLite does not support nested transactions, therefore we can use it to check if the user forgot to 33 | // end the transaction before returning it to the pool. 34 | try pooledConn.Execute("BEGIN; ROLLBACK;") |> ignore 35 | with 36 | | :? SqliteException as se when se.SqliteErrorCode = 1 && se.SqliteExtendedErrorCode = 1 -> 37 | ("The transaction must be finished before you return the connection to the pool.", se) |> InvalidOperationException |> raise 38 | 39 | pool.Push pooledConn 40 | 41 | member this.Borrow() = 42 | checkDisposed() 43 | match pool.TryPop() with 44 | | true, c -> 45 | if c.Inner.State <> ConnectionState.Open then 46 | c.Inner.Open() 47 | c 48 | | false, _ -> 49 | let c = new CachingDbConnection(new SqliteConnection(connectionStr), this.TakeBack, config) 50 | c.Inner.Open() 51 | setup c.Inner 52 | all.Push c 53 | c 54 | 55 | member internal this.All = all 56 | 57 | member internal this.CreateForTransaction() = 58 | checkDisposed() 59 | let c = new TransactionalConnection(connectionStr) 60 | c.Open() 61 | setup c 62 | c 63 | 64 | member private this.WithTransactionBorrowed(f: CachingDbConnection -> 'T) = 65 | use connectionForTransaction = this.Borrow() 66 | connectionForTransaction.Execute("BEGIN IMMEDIATE;") |> ignore 67 | try 68 | connectionForTransaction.InsideTransaction <- true 69 | try 70 | let ret = f connectionForTransaction 71 | connectionForTransaction.Execute "COMMIT;" |> ignore 72 | ret 73 | with ex -> 74 | connectionForTransaction.Execute "ROLLBACK;" |> ignore 75 | reraise() 76 | finally 77 | connectionForTransaction.InsideTransaction <- false 78 | 79 | member private this.WithTransactionBorrowedAsync(f: CachingDbConnection -> Task<'T>) = task { 80 | use connectionForTransaction = this.Borrow() 81 | connectionForTransaction.Execute("BEGIN IMMEDIATE;") |> ignore 82 | try 83 | connectionForTransaction.InsideTransaction <- true 84 | try 85 | let! ret = f connectionForTransaction 86 | connectionForTransaction.Execute "COMMIT;" |> ignore 87 | return ret 88 | with ex -> 89 | connectionForTransaction.Execute "ROLLBACK;" |> ignore 90 | return reraiseAnywhere ex 91 | finally 92 | connectionForTransaction.InsideTransaction <- false 93 | } 94 | 95 | member internal this.WithTransaction(f: CachingDbConnection -> 'T) = 96 | this.WithTransactionBorrowed f 97 | 98 | member internal this.WithAsyncTransaction(f: CachingDbConnection -> Task<'T>) = task { 99 | return! this.WithTransactionBorrowedAsync f 100 | } 101 | 102 | interface IDisposable with 103 | override this.Dispose() = 104 | disposed <- true 105 | for c in all do 106 | c.DisposeReal() 107 | all.Clear() 108 | pool.Clear() 109 | () 110 | 111 | 112 | and [] Connection = 113 | | Pooled of pool: ConnectionManager 114 | | Transactional of conn: TransactionalConnection 115 | | Transitive of tc: IDbConnection 116 | 117 | member this.Get() : IDbConnection = 118 | match this with 119 | | Pooled pool -> pool.Borrow() 120 | | Transactional conn -> conn 121 | | Transitive c -> c 122 | 123 | member this.WithTransaction(f: IDbConnection -> 'T) = 124 | match this with 125 | | Pooled pool -> pool.WithTransaction f 126 | | Transactional conn -> 127 | f conn 128 | | Transitive _conn -> 129 | raise (InvalidOperationException "A Transitive Connection should never be used with a transation.") 130 | 131 | member this.WithAsyncTransaction(f: IDbConnection -> Task<'T>) = 132 | match this with 133 | | Pooled pool -> 134 | pool.WithAsyncTransaction f 135 | | Transactional conn -> 136 | f conn 137 | | Transitive _conn -> 138 | raise (InvalidOperationException "A Transitive Connection should never be used with a transation.") 139 | 140 | 141 | type IDbConnection with 142 | member this.IsWithinTransaction() = 143 | match this with 144 | | :? TransactionalConnection -> true 145 | // All pure DirectConnection usage is inside a transaction 146 | | :? DirectConnection as dc -> dc.InsideTransaction 147 | | other -> false -------------------------------------------------------------------------------- /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 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/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." -------------------------------------------------------------------------------- /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/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 | 93 | | :? float32 as x -> x :> obj, false 94 | | :? float as x -> x :> obj, false 95 | 96 | | _other -> 97 | 98 | let element = JsonValue.Serialize item 99 | match element with 100 | | Boolean b -> b :> obj, false 101 | | Null -> null, false 102 | | Number _ 103 | | String _ 104 | -> element.ToObject(), false 105 | | other -> 106 | // Cannot remove the Id value from here, maybe it is needed. 107 | other.ToJsonString(), true 108 | 109 | 110 | let internal fromJson<'T> (json: JsonValue) = 111 | match json with 112 | | Null when typeof = typeof<'T> -> 113 | nan :> obj :?> 'T 114 | | Null when typeof = typeof<'T> -> 115 | nanf :> obj :?> 'T 116 | | Null when typeof<'T>.IsValueType -> 117 | raise (InvalidOperationException "Invalid operation on a value type.") 118 | | json -> json.ToObject<'T>() 119 | 120 | /// Used internally, do not touch! 121 | let fromIdJson<'T> (element: JsonValue) = 122 | let id = element.["Id"].ToObject() 123 | let value = element.GetProperty("Value") |> fromJson<'T> 124 | 125 | id, value 126 | 127 | let 128 | #if RELEASE 129 | inline 130 | #endif 131 | 132 | internal fromSQLite<'R when 'R :> obj> (row: DbObjectRow) : 'R = 133 | let inline asR (a: 'a when 'a : unmanaged) = 134 | // No allocations. 135 | Unsafe.As<'a, 'R>(&Unsafe.AsRef(&a)) 136 | 137 | if row :> obj = null then 138 | Unchecked.defaultof<'R> 139 | else 140 | 141 | // If the Id is NULL then the ValueJSON is a error message encoded in a JSON string. 142 | if not row.Id.HasValue then 143 | let exc = toJson row.ValueJSON 144 | raise (exn exc) 145 | 146 | // Checking if the SQLite returned a raw string. 147 | if typeof<'R> = typeof then 148 | row.ValueJSON :> obj :?> 'R 149 | else 150 | 151 | match typeof<'R> with 152 | | OfType float when row.ValueJSON <> null -> (asR << float) row.ValueJSON 153 | | OfType float32 when row.ValueJSON <> null -> (asR << float32) row.ValueJSON 154 | | OfType decimal -> (asR << decimal) row.ValueJSON 155 | 156 | | OfType int8 -> (asR << int8) row.ValueJSON 157 | | OfType uint8 -> (asR << uint8) row.ValueJSON 158 | | OfType int16 -> (asR << int16) row.ValueJSON 159 | | OfType uint16 -> (asR << uint16) row.ValueJSON 160 | | OfType int32 -> (asR << int32) row.ValueJSON 161 | | OfType uint32 -> (asR << uint32) row.ValueJSON 162 | | OfType int64 -> (asR << int64) row.ValueJSON 163 | | OfType uint64 -> (asR << uint64) row.ValueJSON 164 | | OfType nativeint -> (asR << nativeint) row.ValueJSON 165 | | OfType unativeint -> (asR << unativeint) row.ValueJSON 166 | 167 | | _ -> 168 | 169 | 170 | match JsonValue.Parse row.ValueJSON with 171 | | Null when typeof = typeof<'R> -> 172 | Unchecked.defaultof<'R> 173 | | Null when typeof<'R>.IsValueType && typeof <> typeof<'R> && typeof <> typeof<'R> -> 174 | Unchecked.defaultof<'R> 175 | | json when typeof = typeof<'R> -> 176 | let id = row.Id.Value 177 | if json.JsonType = JsonValueType.Object && not (json.Contains "Id") && id >= 0 then 178 | json.["Id"] <- id 179 | 180 | json :> obj :?> 'R 181 | | json -> 182 | 183 | let mutable obj = fromJson<'R> json 184 | 185 | // An Id of -1 mean that it is an inserted object inside the IQueryable. 186 | if row.Id.Value <> -1 && HasTypeId<'R>.Value then 187 | HasTypeId<'R>.Write obj row.Id.Value 188 | obj -------------------------------------------------------------------------------- /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 | type BsonDocument (json: JsonValue) = 18 | new () = BsonDocument (JsonValue.New()) 19 | new (objToSerialize: obj) = BsonDocument (JsonValue.Serialize objToSerialize) 20 | 21 | member this.Json = json 22 | 23 | // Method to add or update a key-value pair 24 | member this.Add (key: string, value: obj) = 25 | json.[key] <- JsonValue.Serialize value 26 | 27 | // Method to retrieve a value by key 28 | member this.GetValue (key: string) : JsonValue = 29 | match json.TryGetProperty(key) with 30 | | true, v -> v 31 | | false, _ -> JsonValue.Null 32 | 33 | // Method to remove a key-value pair 34 | member this.Remove (key: string) : bool = 35 | match json with 36 | | JsonValue.Object(map) -> map.Remove(key) 37 | | JsonValue.List(l) -> 38 | let index = Int32.Parse(key, CultureInfo.InvariantCulture) 39 | if index < l.Count then 40 | l.RemoveAt(index) 41 | true 42 | else false 43 | 44 | | _ -> failwith "Invalid operation: the internal data structure is not an object." 45 | 46 | member this.Contains(item: obj) = 47 | let itemJson = JsonValue.Serialize item 48 | match json with 49 | | List l -> l.Contains itemJson 50 | | Object o -> o.Keys.Contains (itemJson.ToObject()) 51 | | other -> raise (InvalidOperationException (sprintf "Cannot call Contains(%A) on %A" itemJson other)) 52 | 53 | member this.ToObject<'T>() = json.ToObject<'T>() 54 | 55 | // Method to serialize this BsonDocument to a JSON string 56 | member this.ToJsonString () = json.ToJsonString () 57 | 58 | member this.AsBoolean = match json with JsonValue.Boolean b -> b | _ -> raise (InvalidCastException("Not a boolean")) 59 | member this.AsBsonArray = match json with JsonValue.List _l -> BsonDocument(json) | _ -> raise (InvalidCastException("Not a BSON array")) 60 | member this.AsBsonDocument = match json with JsonValue.Object _o -> BsonDocument(json) | _ -> raise (InvalidCastException("Not a BSON document")) 61 | member this.AsString = match json with JsonValue.String s -> s | _ -> raise (InvalidCastException("Not a string")) 62 | 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")) 63 | 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")) 64 | member this.AsDouble = match json with JsonValue.Number n -> double n | _ -> raise (InvalidCastException("Not a double")) 65 | member this.AsDecimal = match json with JsonValue.Number n -> n | _ -> raise (InvalidCastException("Not a decimal")) 66 | 67 | member this.IsBoolean = match json with JsonValue.Boolean _ -> true | _ -> false 68 | member this.IsBsonArray = match json with JsonValue.List _ -> true | _ -> false 69 | member this.IsBsonDocument = match json with JsonValue.Object _ -> true | _ -> false 70 | member this.IsString = match json with JsonValue.String _ -> true | _ -> false 71 | member this.IsInt32 = match json with JsonValue.Number n when n <= decimal Int32.MaxValue && n >= decimal Int32.MinValue -> true | _ -> false 72 | member this.IsInt64 = match json with JsonValue.Number n when n <= decimal Int64.MaxValue && n >= decimal Int64.MinValue -> true | _ -> false 73 | member this.IsDouble = match json with JsonValue.Number _ -> true | _ -> false 74 | 75 | member this.ToBoolean() = 76 | match json with 77 | | JsonValue.Boolean b -> b 78 | | JsonValue.String "" -> false 79 | | JsonValue.String _s -> true 80 | | JsonValue.Number n when n = decimal 0 -> false 81 | | JsonValue.Number _n -> true 82 | | JsonValue.List _l -> true 83 | | JsonValue.Object _o -> true 84 | | JsonValue.Null -> false 85 | 86 | member this.ToInt32() = 87 | match json with 88 | | JsonValue.Number n when n <= decimal Int32.MaxValue && n >= decimal Int32.MinValue -> int n 89 | | JsonValue.String s -> 90 | match Int32.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 91 | | true, i -> i 92 | | false, _ -> raise (InvalidCastException("Cannot convert to int32")) 93 | | _ -> raise (InvalidCastException("Cannot convert to int32")) 94 | 95 | member this.ToInt64() = 96 | match json with 97 | | JsonValue.Number n when n <= decimal Int64.MaxValue && n >= decimal Int64.MinValue -> int64 n 98 | | JsonValue.String s -> 99 | match Int64.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 100 | | true, i -> i 101 | | false, _ -> raise (InvalidCastException("Cannot convert to int64")) 102 | | _ -> raise (InvalidCastException("Cannot convert to int64")) 103 | 104 | member this.ToDouble() = 105 | match json with 106 | | JsonValue.Number n -> double n 107 | | JsonValue.String s -> 108 | match Double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 109 | | true, d -> d 110 | | false, _ -> raise (InvalidCastException("Cannot convert to double")) 111 | | _ -> raise (InvalidCastException("Cannot convert to double")) 112 | 113 | member this.ToDecimal() = 114 | match json with 115 | | JsonValue.Number n -> n 116 | | JsonValue.String s -> 117 | match Decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture) with 118 | | true, d -> d 119 | | false, _ -> raise (InvalidCastException("Cannot convert to decimal")) 120 | | _ -> raise (InvalidCastException("Cannot convert to decimal")) 121 | 122 | // Method to deserialize a JSON string to update this BsonDocument 123 | static member Deserialize (jsonString: string) = 124 | BsonDocument (JsonValue.Parse (jsonString)) 125 | 126 | // Override of ToString to return the JSON string representation of the document 127 | override this.ToString () = this.ToJsonString () 128 | 129 | member this.Item 130 | with get (key: string) : BsonDocument = 131 | json.[key] |> BsonDocument 132 | and set (key: string) (value: BsonDocument) = 133 | json.[key] <- value.Json 134 | 135 | interface IEnumerable> with 136 | override this.GetEnumerator () = 137 | (json :> IEnumerable>).GetEnumerator () 138 | override this.GetEnumerator (): IEnumerator = 139 | (this :> IEnumerable>).GetEnumerator () :> IEnumerator 140 | 141 | member internal this.GetPropertyForBinder(name: string) = 142 | match json.GetPropertyForBinder name with 143 | | :? JsonValue as v -> v |> BsonDocument |> box 144 | | other -> other 145 | 146 | interface IDynamicMetaObjectProvider with 147 | member this.GetMetaObject(expression: Linq.Expressions.Expression): DynamicMetaObject = 148 | BsonDocumentMetaObject(expression, BindingRestrictions.Empty, this) 149 | 150 | and internal BsonDocumentMetaObject(expression: Expression, restrictions: BindingRestrictions, value: BsonDocument) = 151 | inherit DynamicMetaObject(expression, restrictions, value) 152 | 153 | static member val private GetPropertyMethod = typeof.GetMethod("GetPropertyForBinder", BindingFlags.NonPublic ||| BindingFlags.Instance) 154 | static member val private SetPropertyMethod = typeof.GetMethod("set_Item") 155 | static member val private ToJsonMethod = typeof.GetMethod("ToJsonString") 156 | static member val private ToStringMethod = typeof.GetMethod("ToString") 157 | 158 | override this.BindGetMember(binder: GetMemberBinder) : DynamicMetaObject = 159 | let resultExpression = Expression.Call( 160 | Expression.Convert(this.Expression, typeof), 161 | BsonDocumentMetaObject.GetPropertyMethod, 162 | Expression.Constant(binder.Name) 163 | ) 164 | DynamicMetaObject(resultExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 165 | 166 | override this.BindSetMember(binder: SetMemberBinder, value: DynamicMetaObject) : DynamicMetaObject = 167 | let setExpression = Expression.Call( 168 | Expression.Convert(this.Expression, typeof), 169 | BsonDocumentMetaObject.SetPropertyMethod, 170 | Expression.Constant(binder.Name), 171 | value.Expression 172 | ) 173 | let returnExpression = Expression.Block(setExpression, value.Expression) 174 | DynamicMetaObject(returnExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 175 | 176 | override this.BindConvert(binder: ConvertBinder) : DynamicMetaObject = 177 | let convertExpression = Expression.Call( 178 | Expression.Convert(this.Expression, typeof), 179 | BsonDocumentMetaObject.ToJsonMethod 180 | ) 181 | DynamicMetaObject(convertExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 182 | 183 | override this.BindGetIndex(binder: GetIndexBinder, indexes: DynamicMetaObject[]) : DynamicMetaObject = 184 | if indexes.Length <> 1 then 185 | failwithf "BSON does not support indexes length <> 1: %i" indexes.Length 186 | let indexExpr = indexes.[0].Expression 187 | let resultExpression = Expression.Call( 188 | Expression.Convert(this.Expression, typeof), 189 | BsonDocumentMetaObject.GetPropertyMethod, 190 | Expression.Call(indexExpr, BsonDocumentMetaObject.ToStringMethod) 191 | ) 192 | DynamicMetaObject(resultExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 193 | 194 | override this.BindSetIndex(binder: SetIndexBinder, indexes: DynamicMetaObject[], value: DynamicMetaObject) : DynamicMetaObject = 195 | if indexes.Length <> 1 then 196 | failwithf "BSON does not support indexes length <> 1: %i" indexes.Length 197 | let indexExpr = indexes.[0].Expression 198 | let setExpression = Expression.Call( 199 | Expression.Convert(this.Expression, typeof), 200 | BsonDocumentMetaObject.SetPropertyMethod, 201 | Expression.Call(indexExpr, BsonDocumentMetaObject.ToStringMethod), 202 | value.Expression 203 | ) 204 | let returnExpression = Expression.Block(setExpression, value.Expression) 205 | DynamicMetaObject(returnExpression, BindingRestrictions.GetTypeRestriction(this.Expression, this.LimitType)) 206 | 207 | override this.GetDynamicMemberNames() : IEnumerable = 208 | match value.Json with 209 | | JsonValue.Object o -> seq { for kv in o do yield kv.Key } 210 | | _ -> Seq.empty 211 | 212 | 213 | type InsertManyResult = { 214 | Ids: IList 215 | Count: int64 216 | } 217 | 218 | /// The F# compiler cannot decide which method to call. 219 | [] 220 | type internal ProxyRef = 221 | static member ReplaceOne (collection: SoloDatabase.ISoloDBCollection<'a>) (document: 'a) (filter: Expression>) = 222 | collection.ReplaceOne (filter, document) 223 | 224 | static member ReplaceMany (collection: SoloDatabase.ISoloDBCollection<'a>) (document: 'a) (filter: Expression>) = 225 | collection.ReplaceMany (filter, document) 226 | 227 | [] 228 | type CollectionExtensions = 229 | [] 230 | static member CountDocuments<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) : int64 = 231 | collection.Where(filter).LongCount() 232 | 233 | [] 234 | static member CountDocuments<'a>(collection: SoloDatabase.ISoloDBCollection<'a>) : int64 = 235 | collection.LongCount() 236 | 237 | [] 238 | static member Find<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 239 | collection.Where(filter) 240 | 241 | [] 242 | static member InsertOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, document: 'a) = 243 | collection.Insert document 244 | 245 | [] 246 | static member InsertMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, documents: 'a seq) = 247 | let result = (collection.InsertBatch documents) 248 | { 249 | Ids = result 250 | Count = result.Count 251 | } 252 | 253 | [] 254 | static member ReplaceOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>, document: 'a) = 255 | ProxyRef.ReplaceOne collection document filter 256 | 257 | [] 258 | static member ReplaceMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>, document: 'a) = 259 | ProxyRef.ReplaceMany collection document filter 260 | 261 | [] 262 | static member DeleteOne<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 263 | collection.DeleteMany(filter) 264 | 265 | [] 266 | static member DeleteMany<'a>(collection: SoloDatabase.ISoloDBCollection<'a>, filter: Expression>) = 267 | collection.DeleteMany(filter) 268 | 269 | module private Helper = 270 | /// Helper to create an expression from a string field name 271 | let internal getPropertyExpression (fieldPath: string) : Expression> = 272 | let parameter = Expression.Parameter(typeof<'T>, "x") 273 | let fields = fieldPath.Split('.') 274 | 275 | let rec buildExpression (expr: Expression) (fields: string list) : Expression = 276 | match fields with 277 | | [] -> expr 278 | | field :: rest -> // Handle intermediate fields 279 | let field = 280 | if field = "_id" then "Id" 281 | else field 282 | 283 | let propertyOrField = 284 | if expr.Type.GetField(field) <> null || expr.Type.GetProperty(field) <> null then 285 | Expression.PropertyOrField(expr, field) :> Expression 286 | else 287 | let indexExpr = 288 | Expression.MakeIndex(expr, expr.Type.GetProperty "Item", [Expression.Constant field]) :> Expression 289 | 290 | if rest.IsEmpty && field <> "Id" && (expr.Type = typeof || expr.Type = typeof) then 291 | Expression.Call(indexExpr, expr.Type.GetMethod("ToObject").MakeGenericMethod(typeof<'TField>)) 292 | elif field = "Id" then 293 | Expression.Convert(Expression.Convert(indexExpr, typeof), typeof<'TField>) 294 | else 295 | indexExpr 296 | 297 | buildExpression propertyOrField rest 298 | 299 | let finalExpr = buildExpression (parameter :> Expression) (fields |> List.ofArray) 300 | Expression.Lambda>(finalExpr, parameter) 301 | 302 | type FilterDefinitionBuilder<'T> () = 303 | let mutable filters = [] 304 | 305 | member val Empty = Expression.Lambda>(Expression.Constant(true), Expression.Parameter(typeof<'T>, "x")) 306 | 307 | // Add an equality filter 308 | member this.Eq<'TField>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 309 | let parameter = field.Parameters.[0] 310 | let body = Expression.Equal(field.Body, Expression.Constant(value, typeof<'TField>)) 311 | let lambda = Expression.Lambda>(body, parameter) 312 | filters <- lambda :: filters 313 | this 314 | 315 | // Add a greater-than filter 316 | member this.Gt<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 317 | let parameter = field.Parameters.[0] 318 | let body = Expression.GreaterThan(field.Body, Expression.Constant(value, typeof<'TField>)) 319 | let lambda = Expression.Lambda>(body, parameter) 320 | filters <- lambda :: filters 321 | this 322 | 323 | // Add a less-than filter 324 | member this.Lt<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : FilterDefinitionBuilder<'T> = 325 | let parameter = field.Parameters.[0] 326 | let body = Expression.LessThan(field.Body, Expression.Constant(value, typeof<'TField>)) 327 | let lambda = Expression.Lambda>(body, parameter) 328 | filters <- lambda :: filters 329 | this 330 | 331 | // Add a contains filter (for collections or strings) 332 | member this.In<'TField>(field: Expression>, values: IEnumerable<'TField>) : FilterDefinitionBuilder<'T> = 333 | let parameter = field.Parameters.[0] 334 | let expressions = 335 | values 336 | |> Seq.map (fun value -> 337 | Expression.Equal(field.Body, Expression.Constant(value, typeof<'TField>)) 338 | ) 339 | |> Seq.toList 340 | 341 | let combinedBody = 342 | expressions 343 | |> List.reduce (fun acc expr -> Expression.OrElse(acc, expr)) 344 | 345 | let lambda = Expression.Lambda>(combinedBody, parameter) 346 | filters <- lambda :: filters 347 | this 348 | 349 | // Overloaded method for string field names 350 | member this.Eq<'TField>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 351 | this.Eq<'TField>(Helper.getPropertyExpression (field), value) 352 | 353 | member this.Gt<'TField when 'TField :> IComparable>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 354 | this.Gt<'TField>(Helper.getPropertyExpression (field), value) 355 | 356 | member this.Lt<'TField when 'TField :> IComparable>(field: string, value: 'TField) : FilterDefinitionBuilder<'T> = 357 | this.Lt<'TField>(Helper.getPropertyExpression (field), value) 358 | 359 | member this.In<'TField>(field: string, values: IEnumerable<'TField>) : FilterDefinitionBuilder<'T> = 360 | this.In<'TField>(Helper.getPropertyExpression (field), values) 361 | 362 | // Combine all filters into a single LINQ expression 363 | member this.Build() : Expression> = 364 | if filters.IsEmpty then 365 | Expression.Lambda>(Expression.Constant(true), Expression.Parameter(typeof<'T>, "x")) 366 | else 367 | let parameter = Expression.Parameter(typeof<'T>, "x") 368 | let combined = 369 | filters 370 | |> List.reduce (fun acc filter -> 371 | Expression.AndAlso(acc.Body, Expression.Invoke(filter, parameter)) 372 | |> Expression.Lambda>) 373 | combined 374 | 375 | static member op_Implicit(builder: FilterDefinitionBuilder<'T>) : Expression> = 376 | builder.Build() 377 | 378 | type QueryDefinitionBuilder<'T>() = 379 | let filterBuilder = new FilterDefinitionBuilder<'T>() 380 | 381 | member this.Empty = filterBuilder.Empty 382 | 383 | member this.EQ<'TField>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 384 | filterBuilder.Eq<'TField>(field, value) |> ignore 385 | this 386 | 387 | member this.GT<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 388 | filterBuilder.Gt<'TField>(field, value) |> ignore 389 | this 390 | 391 | member this.LT<'TField when 'TField :> IComparable>(field: Expression>, value: 'TField) : QueryDefinitionBuilder<'T> = 392 | filterBuilder.Lt<'TField>(field, value) |> ignore 393 | this 394 | 395 | member this.IN<'TField>(field: Expression>, values: IEnumerable<'TField>) : QueryDefinitionBuilder<'T> = 396 | filterBuilder.In<'TField>(field, values) |> ignore 397 | this 398 | 399 | member this.EQ<'TField>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 400 | filterBuilder.Eq<'TField>(field, value) |> ignore 401 | this 402 | 403 | member this.GT<'TField when 'TField :> IComparable>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 404 | filterBuilder.Gt<'TField>(field, value) |> ignore 405 | this 406 | 407 | member this.LT<'TField when 'TField :> IComparable>(field: string, value: 'TField) : QueryDefinitionBuilder<'T> = 408 | filterBuilder.Lt<'TField>(field, value) |> ignore 409 | this 410 | 411 | member this.IN<'TField>(field: string, values: IEnumerable<'TField>) : QueryDefinitionBuilder<'T> = 412 | filterBuilder.In<'TField>(field, values) |> ignore 413 | this 414 | 415 | // Method to combine all filters into a single LINQ expression and build the query. 416 | member this.Build() : Expression> = 417 | filterBuilder.Build() 418 | 419 | static member op_Implicit(builder: QueryDefinitionBuilder<'T>) : Expression> = 420 | builder.Build() 421 | 422 | [] 423 | type Builders<'T> = 424 | static member Filter with get() = FilterDefinitionBuilder<'T> () 425 | 426 | static member Query with get() = QueryDefinitionBuilder<'T> () 427 | 428 | type MongoDatabase internal (soloDB: SoloDB) = 429 | member this.GetCollection<'doc> (name: string) = 430 | soloDB.GetCollection<'doc> name 431 | 432 | member this.CreateCollection (name: string) = 433 | soloDB.GetUntypedCollection name 434 | 435 | member this.ListCollections () = 436 | soloDB.ListCollectionNames () 437 | 438 | member this.Dispose () = 439 | soloDB.Dispose () 440 | 441 | interface IDisposable with 442 | override this.Dispose (): unit = 443 | this.Dispose () 444 | 445 | type internal MongoClientLocation = 446 | | Disk of {| Directory: DirectoryInfo; LockingFile: FileStream |} 447 | | Memory of string 448 | 449 | type MongoClient(directoryDatabaseSource: string) = 450 | do if directoryDatabaseSource.StartsWith "mongodb://" then failwithf "SoloDB does not support mongo connections." 451 | let location = 452 | if directoryDatabaseSource.StartsWith "memory:" then 453 | Memory directoryDatabaseSource 454 | else 455 | let directoryDatabasePath = Path.GetFullPath directoryDatabaseSource 456 | let directory = Directory.CreateDirectory directoryDatabasePath 457 | Disk {| 458 | Directory = directory 459 | LockingFile = File.Open(Path.Combine (directory.FullName, ".lock"), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite) 460 | |} 461 | 462 | let connectedDatabases = ResizeArray> () 463 | let mutable disposed = false 464 | 465 | member this.GetDatabase( [] name : string) = 466 | if disposed then raise (ObjectDisposedException(nameof(MongoClient))) 467 | 468 | let name = match name with null -> "Master" | n -> n 469 | let name = name + ".solodb" 470 | let dbSource = 471 | match location with 472 | | Disk disk -> 473 | Path.Combine (disk.Directory.FullName, name) 474 | | Memory source -> 475 | source + "-" + name 476 | 477 | let db = new SoloDB (dbSource) 478 | let db = new MongoDatabase (db) 479 | 480 | let _remoteCount = connectedDatabases.RemoveAll(fun x -> match x.TryGetTarget() with false, _ -> true | true, _ -> false) 481 | connectedDatabases.Add (WeakReference db) 482 | 483 | db 484 | 485 | 486 | interface IDisposable with 487 | override this.Dispose () = 488 | disposed <- true 489 | for x in connectedDatabases do 490 | match x.TryGetTarget() with 491 | | true, soloDB -> soloDB.Dispose() 492 | | false, _ -> () 493 | 494 | connectedDatabases.Clear() 495 | match location with 496 | | Disk disk -> 497 | disk.LockingFile.Dispose () 498 | | _other -> () 499 | 500 | -------------------------------------------------------------------------------- /SoloDB/SQLiteTools.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System 4 | open System.Reflection 5 | open System.Collections 6 | open System.Collections.Generic 7 | open SoloDatabase 8 | open System.Data 9 | open System.Runtime.CompilerServices 10 | open System.Runtime.InteropServices 11 | open Utils 12 | open System.Linq.Expressions 13 | open Microsoft.FSharp.Reflection 14 | open System.Runtime.Serialization 15 | open SoloDatabase.JsonSerializator 16 | open System.Collections.Concurrent 17 | open Microsoft.Data.Sqlite 18 | 19 | module SQLiteTools = 20 | let private nullablePropsCache = ConcurrentDictionary() 21 | 22 | let private getNullableProperties (nullableType: Type) = 23 | nullablePropsCache.GetOrAdd(nullableType, fun t -> 24 | struct (t.GetProperty("HasValue"), t.GetProperty("Value"))) 25 | 26 | [] 27 | type TrimmedArray = { 28 | Array: Array 29 | TrimmedLen: int 30 | } 31 | 32 | 33 | let private processParameter (value: obj) = 34 | match value with 35 | | null -> 36 | struct (null, -1) 37 | | :? DateTimeOffset as dto -> 38 | struct (dto.ToUnixTimeMilliseconds() |> box, sizeof) 39 | | :? TrimmedArray as ta -> 40 | struct (ta.Array, ta.TrimmedLen) 41 | | _ -> 42 | let valType = value.GetType() 43 | if valType.Name.StartsWith "Nullable`" then 44 | let struct (hasValueProp, valueProp) = getNullableProperties valType 45 | if hasValueProp.GetValue value :?> bool then 46 | struct (valueProp.GetValue value, -1) 47 | else 48 | struct (null, -1) 49 | else 50 | struct (value, -1) 51 | 52 | let private addParameter (command: IDbCommand) (key: string) (value: obj) = 53 | let struct (value, size) = processParameter value 54 | 55 | let par = command.CreateParameter() 56 | par.ParameterName <- key 57 | par.Value <- value 58 | 59 | if size > 0 then 60 | par.Size <- size 61 | 62 | command.Parameters.Add par |> ignore 63 | 64 | let private setOrAddParameter (command: IDbCommand) (key: string) (value: obj) = 65 | let struct (value, size) = processParameter value 66 | 67 | let par = 68 | if command.Parameters.Contains key then 69 | command.Parameters.[key] :?> IDbDataParameter 70 | else 71 | let p = command.CreateParameter() 72 | command.Parameters.Add p |> ignore 73 | p.ParameterName <- key 74 | p 75 | 76 | par.Value <-value 77 | 78 | if size > 0 then 79 | par.Size <- size 80 | 81 | let private dynamicParameterCache = ConcurrentDictionary>>() 82 | let private processParameters processFn (command: IDbCommand) (parameters: obj) = 83 | match parameters with 84 | | null -> () 85 | | :? IDictionary as dict -> 86 | for key in dict.Keys do 87 | let value = dict.[key] 88 | let key = key :?> string 89 | processFn command key value 90 | 91 | | parameters -> 92 | let fn = dynamicParameterCache.GetOrAdd(parameters.GetType(), Func>>( 93 | fun t -> 94 | let props = t.GetProperties() |> Array.filter(_.CanRead) 95 | let dbCmdPar = Expression.Parameter typeof 96 | let parametersPar = Expression.Parameter typeof 97 | let actionPar = Expression.Parameter typeof> 98 | 99 | let meth = typeof>.GetMethod "Invoke" 100 | 101 | let l = Expression.Lambda>>( 102 | Expression.Block([| 103 | for p in props do 104 | Expression.Call(actionPar, meth, [|dbCmdPar :> Expression; Expression.Constant(p.Name); Expression.Convert(Expression.Property(Expression.Convert(parametersPar, t), p), typeof)|]) :> Expression 105 | |]), 106 | [|dbCmdPar; parametersPar; actionPar|]) 107 | 108 | l.Compile(false) 109 | )) 110 | fn.Invoke(command, parameters, processFn) 111 | 112 | let private createCommand (this: IDbConnection) (sql: string) (parameters: obj) = 113 | let command = this.CreateCommand() 114 | command.CommandText <- sql 115 | processParameters addParameter command parameters 116 | 117 | command 118 | 119 | type private TypeMapper<'T> = 120 | static member val Map = 121 | let matchMethodWithPropertyType (prop: PropertyInfo) (readerParam: Expression) (columnVar: Expression) = 122 | // Get the appropriate method and conversion for each property type 123 | let (getMethodName, needsConversion, conversionFunc) = 124 | match prop.PropertyType with 125 | | t when t = typeof || t = typeof -> 126 | "GetByte", false, None 127 | | t when t = typeof -> 128 | "GetByte", true, Some (fun (expr: Expression) -> Expression.Convert(expr, typeof) :> Expression) 129 | | t when t = typeof -> 130 | "GetInt16", false, None 131 | | t when t = typeof -> 132 | "GetInt32", true, Some (fun expr -> 133 | Expression.Convert(Expression.Call( 134 | null, 135 | typeof.GetMethod("op_Explicit", [|typeof|]), 136 | expr), 137 | typeof)) 138 | | t when t = typeof -> 139 | "GetInt32", false, None 140 | | t when t = typeof -> 141 | "GetInt64", true, Some (fun expr -> 142 | Expression.Convert(Expression.Call( 143 | null, 144 | typeof.GetMethod("op_Explicit", [|typeof|]), 145 | expr), 146 | typeof)) 147 | | t when t = typeof -> 148 | "GetInt64", false, None 149 | | t when t = typeof -> 150 | "GetInt64", true, Some (fun expr -> 151 | Expression.Convert(Expression.Call( 152 | null, 153 | typeof.GetMethod("op_Explicit", [|typeof|]), 154 | expr), 155 | typeof)) 156 | | t when t = typeof || t = typeof -> 157 | "GetFloat", false, None 158 | | t when t = typeof -> 159 | "GetDouble", false, None 160 | | t when t = typeof -> 161 | "GetDecimal", false, None 162 | | t when t = typeof -> 163 | "GetString", false, None 164 | | t when t = typeof -> 165 | "GetBoolean", false, None 166 | | t when t = typeof -> 167 | "GetValue", true, Some (fun expr -> Expression.TypeAs(expr, typeof)) 168 | | t when t = typeof -> 169 | "GetGuid", false, None 170 | | t when t = typeof -> 171 | "GetDateTime", false, None 172 | | t when t = typeof -> 173 | "GetInt64", true, Some (fun (expr: Expression) -> Expression.Call(typeof.GetMethod("FromUnixTimeMilliseconds", [|typeof|]), expr)) 174 | | _ -> 175 | "GetValue", true, Some (fun expr -> Expression.Convert(expr, prop.PropertyType)) 176 | 177 | 178 | // Call the appropriate method 179 | let valueExpr = Expression.Call( 180 | readerParam, 181 | typeof.GetMethod(getMethodName), 182 | [| columnVar |] 183 | ) 184 | 185 | // Apply conversion if needed 186 | let finalValueExpr = 187 | match needsConversion, conversionFunc with 188 | | true, Some convFunc -> convFunc(valueExpr) 189 | | _ -> valueExpr 190 | 191 | finalValueExpr 192 | 193 | 194 | match typeof<'T> with 195 | | OfType int8 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 196 | reader.GetByte(startIndex) :> obj :?> 'T 197 | | OfType uint8 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 198 | reader.GetInt16(startIndex) |> uint8 :> obj :?> 'T 199 | | OfType int16 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 200 | reader.GetInt16(startIndex) :> obj :?> 'T 201 | | OfType uint16 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 202 | reader.GetInt32(startIndex) |> uint16 :> obj :?> 'T 203 | | OfType int32 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 204 | reader.GetInt32(startIndex) :> obj :?> 'T 205 | | OfType uint32 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 206 | reader.GetInt64(startIndex) |> uint32 :> obj :?> 'T 207 | | OfType int64 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 208 | reader.GetInt64(startIndex) :> obj :?> 'T 209 | | OfType uint64 -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 210 | reader.GetInt64(startIndex) |> uint64 :> obj :?> 'T 211 | | OfType float -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 212 | reader.GetDouble(startIndex) |> float :> obj :?> 'T 213 | | OfType double -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 214 | reader.GetDouble(startIndex) :> obj :?> 'T 215 | | OfType decimal -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 216 | reader.GetDecimal(startIndex) :> obj :?> 'T 217 | | OfType string -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 218 | reader.GetString(startIndex) :> obj :?> 'T 219 | | OfType bool -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 220 | reader.GetBoolean(startIndex) :> obj :?> 'T 221 | | OfType (id: byte array -> byte array) -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 222 | reader.GetValue(startIndex) :?> 'T 223 | | OfType (id: Guid -> Guid) -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 224 | reader.GetGuid(startIndex) :> obj :?> 'T 225 | | OfType DateTime -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 226 | reader.GetDateTime(startIndex) :> obj :?> 'T 227 | | OfType DateTimeOffset -> fun (reader: IDataReader) (startIndex: int) (_columns: IDictionary) -> 228 | reader.GetInt64(startIndex) |> DateTimeOffset.FromUnixTimeMilliseconds :> obj :?> 'T 229 | | t when t = typeof -> 230 | fun (reader: IDataReader) (startIndex: int) (columns: IDictionary) -> 231 | let jsonObj = JsonSerializator.JsonValue.Object(Dictionary(columns.Count)) 232 | for key in columns.Keys do 233 | if startIndex <= columns.[key] then 234 | let value = reader.GetValue(columns.[key]) 235 | jsonObj.[key] <- JsonSerializator.JsonValue.Serialize value 236 | 237 | jsonObj.ToObject<'T>() 238 | 239 | | t when t = typeof -> 240 | fun (reader: IDataReader) (startIndex: int) (columns: IDictionary) -> 241 | if columns.Count = 1 && (columns.Keys |> Seq.head).StartsWith "json_object" then 242 | JsonSerializator.JsonValue.Parse (reader.GetString(0)) :> obj :?> 'T 243 | else 244 | 245 | let jsonObj = JsonSerializator.JsonValue.Object(Dictionary(columns.Count)) 246 | for key in columns.Keys do 247 | if startIndex <= columns.[key] then 248 | let value = reader.GetValue(columns.[key]) 249 | jsonObj.[key] <- JsonSerializator.JsonValue.Serialize value 250 | 251 | jsonObj :> obj :?> 'T 252 | 253 | | t when FSharpType.IsRecord t -> 254 | 255 | // Parameter declarations 256 | let readerParam = Expression.Parameter(typeof, "reader") 257 | let startIndexParam = Expression.Parameter(typeof, "startIndex") 258 | let columnsParam = Expression.Parameter(typeof>, "columns") 259 | 260 | let columnVar = Expression.Variable typeof 261 | 262 | let recordFields = FSharpType.GetRecordFields t 263 | let recordFieldsType = recordFields |> Array.map(_.PropertyType) 264 | let ctor = t.GetConstructors() |> Array.find(fun c -> c.GetParameters() |> Array.map(_.ParameterType) = recordFieldsType) 265 | 266 | 267 | // Create parameter expressions for constructor 268 | let parameterExprs = recordFields |> Array.map (fun prop -> 269 | // Check 270 | let hasPropertyExpr = 271 | Expression.AndAlso( 272 | Expression.Call( 273 | columnsParam, 274 | typeof>.GetMethod("TryGetValue"), 275 | [ 276 | Expression.Constant(prop.Name) :> Expression; 277 | columnVar :> Expression 278 | ] 279 | ), 280 | Expression.AndAlso( 281 | Expression.GreaterThanOrEqual( 282 | columnVar, 283 | startIndexParam 284 | ), 285 | Expression.Equal( 286 | Expression.Call( 287 | readerParam, 288 | typeof.GetMethod("IsDBNull"), 289 | [| columnVar :> Expression |] 290 | ), 291 | Expression.Constant(false) 292 | ) 293 | )) 294 | 295 | let getValueAndDeserialize = matchMethodWithPropertyType prop readerParam columnVar 296 | 297 | // Default value if property not found 298 | let defaultValue = 299 | if prop.PropertyType.IsValueType then 300 | Expression.Default(prop.PropertyType) :> Expression 301 | else 302 | Expression.Constant(null, prop.PropertyType) :> Expression 303 | 304 | // Return value from condition 305 | Expression.Condition( 306 | hasPropertyExpr, 307 | getValueAndDeserialize, 308 | defaultValue 309 | ) :> Expression 310 | ) 311 | 312 | 313 | // Build the complete expression 314 | let body = Expression.Block( 315 | [|columnVar|], 316 | [|Expression.New(ctor, parameterExprs) :> Expression|]) 317 | 318 | let lambda = Expression.Lambda, 'T>>( 319 | body, 320 | [| readerParam; startIndexParam; columnsParam |] 321 | ) 322 | 323 | let fn = lambda.Compile() 324 | 325 | fun (reader: IDataReader) (startIndex: int) (columns: IDictionary) -> 326 | try let value = fn.Invoke(reader, startIndex, columns) in value 327 | with _ex -> reraise() 328 | 329 | | t -> 330 | // Parameter declarations 331 | let readerParam = Expression.Parameter(typeof, "reader") 332 | let startIndexParam = Expression.Parameter(typeof, "startIndex") 333 | let columnsParam = Expression.Parameter(typeof>, "columns") 334 | 335 | // Create appropriate mapper based on the type 336 | let expr = 337 | // Handle class types with properties 338 | let props = t.GetProperties() |> Array.filter (fun p -> p.CanWrite) 339 | 340 | // Variables for the expression 341 | let resultVar = Expression.Variable(t, "result") 342 | let statements = ResizeArray() 343 | 344 | // Create a new instance 345 | let ctor = t.GetConstructor([||]) 346 | let createInstanceExpr = 347 | if ctor <> null then 348 | Expression.New(ctor) :> Expression 349 | else 350 | // Use FormatterServices.GetSafeUninitializedObject 351 | let fn = Func<'T>(fun () -> 352 | FormatterServices.GetSafeUninitializedObject(t) :?> 'T) 353 | Expression.Call( 354 | Expression.Constant(fn), 355 | typeof>.GetMethod("Invoke"), 356 | [||] 357 | ) :> Expression 358 | 359 | statements.Add(Expression.Assign(resultVar, createInstanceExpr) :> Expression) 360 | 361 | // Set each property from the reader 362 | for i, prop in props |> Array.indexed do 363 | let columnVar = Expression.Variable(typeof, "columnIndex") 364 | 365 | let finalValueExpr = matchMethodWithPropertyType prop readerParam columnVar 366 | 367 | let propExpr = Expression.Block( 368 | [| columnVar |], 369 | [| 370 | Expression.DebugInfo(Expression.SymbolDocument(prop.Name), i + 1, 1, i + 1, 2) :> Expression; 371 | // Try to get column index 372 | Expression.IfThen( 373 | Expression.Call( 374 | columnsParam, 375 | typeof>.GetMethod("TryGetValue"), 376 | [ 377 | Expression.Constant(prop.Name) :> Expression; 378 | columnVar :> Expression 379 | ] 380 | ), 381 | 382 | // If column exists and index >= startIndex and not null 383 | Expression.IfThen( 384 | Expression.AndAlso( 385 | Expression.GreaterThanOrEqual( 386 | columnVar, 387 | startIndexParam 388 | ), 389 | Expression.Equal( 390 | Expression.Call( 391 | readerParam, 392 | typeof.GetMethod("IsDBNull"), 393 | [| columnVar :> Expression |] 394 | ), 395 | Expression.Constant(false) 396 | ) 397 | ), 398 | Expression.Assign( 399 | Expression.Property(resultVar, prop), 400 | finalValueExpr 401 | ) 402 | )) :> Expression 403 | |] 404 | ) 405 | 406 | statements.Add(propExpr) 407 | 408 | // Return the result 409 | statements.Add(resultVar :> Expression) 410 | 411 | Expression.Block([| resultVar |], statements) :> Expression 412 | 413 | // Create and compile lambda 414 | let lambda = Expression.Lambda, 'T>>( 415 | expr, 416 | [| readerParam; startIndexParam; columnsParam |] 417 | ) 418 | 419 | let fn = lambda.Compile() 420 | 421 | fun (reader: IDataReader) (startIndex: int) (columns: IDictionary) -> 422 | try fn.Invoke(reader, startIndex, columns) 423 | with _ex -> reraise() 424 | 425 | let private queryCommand<'T> (command: IDbCommand) (nullableCachedDict: Dictionary) = seq { 426 | use reader = command.ExecuteReader() 427 | let dict = 428 | if isNull nullableCachedDict then 429 | Dictionary(reader.FieldCount) 430 | else 431 | nullableCachedDict 432 | 433 | if dict.Count = 0 then 434 | for i in 0..(reader.FieldCount - 1) do 435 | dict.Add(reader.GetName(i), i) 436 | 437 | 438 | while reader.Read() do 439 | yield TypeMapper<'T>.Map reader 0 dict 440 | } 441 | 442 | let private queryInner<'T> this (sql: string) (parameters: obj) = seq { 443 | use command = createCommand this sql parameters 444 | yield! queryCommand<'T> command null 445 | } 446 | 447 | 448 | /// Redirect all the method calls to the connection provided in the constructor, and on Dispose, it is a noop. 449 | type DirectConnection internal (connection: IDbConnection, insideTransaction: bool) = 450 | member this.BeginTransaction() = connection.BeginTransaction() 451 | member this.BeginTransaction (il: IsolationLevel) = connection.BeginTransaction il 452 | member this.ChangeDatabase (databaseName: string) = connection.ChangeDatabase databaseName 453 | member this.Close() = connection.Close() 454 | member this.CreateCommand() = connection.CreateCommand() 455 | member this.Open() = connection.Open() 456 | 457 | member this.ConnectionString with get() = connection.ConnectionString and set (cs) = connection.ConnectionString <- cs 458 | member this.ConnectionTimeout: int = connection.ConnectionTimeout 459 | member this.Database: string = connection.Database 460 | member this.State: ConnectionState = connection.State 461 | 462 | member val InsideTransaction = insideTransaction with get, set 463 | 464 | interface IDbConnection with 465 | override this.BeginTransaction() = connection.BeginTransaction() 466 | override this.BeginTransaction (il: IsolationLevel) = connection.BeginTransaction il 467 | override this.ChangeDatabase (databaseName: string) = connection.ChangeDatabase databaseName 468 | override this.Close() = connection.Close() 469 | override this.CreateCommand() = connection.CreateCommand() 470 | override this.Open() = connection.Open() 471 | member this.ConnectionString with get() = connection.ConnectionString and set (cs) = connection.ConnectionString <- cs 472 | member this.ConnectionTimeout: int = connection.ConnectionTimeout 473 | member this.Database: string = connection.Database 474 | member this.State: ConnectionState = connection.State 475 | 476 | interface IDisposable with 477 | override this.Dispose (): unit = 478 | () 479 | 480 | member this.DisposeReal() = 481 | connection.Dispose() 482 | 483 | type CachingDbConnectionTransaction internal (connection: CachingDbConnection, isolationLevel: IsolationLevel, deferred: bool) = 484 | let mutable _connection = Some connection 485 | let mutable _completed = false 486 | let mutable _externalRollback = false 487 | 488 | // Handle isolation level mapping like in C# version 489 | let actualIsolationLevel = 490 | match isolationLevel with 491 | | IsolationLevel.ReadUncommitted when not deferred -> IsolationLevel.Serializable 492 | | IsolationLevel.ReadCommitted 493 | | IsolationLevel.RepeatableRead 494 | | IsolationLevel.Unspecified -> IsolationLevel.Serializable 495 | | _ -> isolationLevel 496 | 497 | do 498 | // Set read_uncommitted pragma for ReadUncommitted isolation 499 | if actualIsolationLevel = IsolationLevel.ReadUncommitted then 500 | connection.Execute("PRAGMA read_uncommitted = 1;") |> ignore 501 | elif actualIsolationLevel <> IsolationLevel.Serializable then 502 | invalidArg "isolationLevel" $"Invalid isolation level: {actualIsolationLevel}" 503 | 504 | // Begin transaction with appropriate mode 505 | let sql = 506 | if actualIsolationLevel = IsolationLevel.Serializable && not deferred then 507 | "BEGIN IMMEDIATE;" 508 | else 509 | "BEGIN;" 510 | connection.Execute(sql) |> ignore 511 | 512 | member this.Connection = _connection 513 | member this.ExternalRollback = _externalRollback 514 | member this.IsolationLevel = actualIsolationLevel 515 | 516 | member this.Commit() = 517 | match _connection with 518 | | None -> invalidOp "Transaction completed" 519 | | Some conn when _externalRollback || _completed || conn.State <> ConnectionState.Open -> 520 | invalidOp "Transaction completed" 521 | | Some conn -> 522 | conn.Execute("COMMIT;") |> ignore 523 | this.Complete() 524 | 525 | member this.Rollback() = 526 | match _connection with 527 | | None -> invalidOp "Transaction completed" 528 | | Some conn when _completed || conn.State <> ConnectionState.Open -> 529 | invalidOp "Transaction completed" 530 | | Some _ -> 531 | this.RollbackInternal() 532 | 533 | member this.Save(savepointName: string) = 534 | if savepointName = null then nullArg "savepointName" 535 | match _connection with 536 | | None -> invalidOp "Transaction completed" 537 | | Some conn when _completed || conn.State <> ConnectionState.Open -> 538 | invalidOp "Transaction completed" 539 | | Some conn -> 540 | let escapedName = savepointName.Replace("\"", "\"\"") 541 | let sql = $"SAVEPOINT \"{escapedName}\";" 542 | conn.Execute(sql) |> ignore 543 | 544 | member this.Rollback(savepointName: string) = 545 | if savepointName = null then nullArg "savepointName" 546 | match _connection with 547 | | None -> invalidOp "Transaction completed" 548 | | Some conn when _completed || conn.State <> ConnectionState.Open -> 549 | invalidOp "Transaction completed" 550 | | Some conn -> 551 | let escapedName = savepointName.Replace("\"", "\"\"") 552 | let sql = $"ROLLBACK TO SAVEPOINT \"{escapedName}\";" 553 | conn.Execute(sql) |> ignore 554 | 555 | member this.Release(savepointName: string) = 556 | if savepointName = null then nullArg "savepointName" 557 | match _connection with 558 | | None -> invalidOp "Transaction completed" 559 | | Some conn when _completed || conn.State <> ConnectionState.Open -> 560 | invalidOp "Transaction completed" 561 | | Some conn -> 562 | let escapedName = savepointName.Replace("\"", "\"\"") 563 | let sql = $"RELEASE SAVEPOINT \"{escapedName}\";" 564 | conn.Execute(sql) |> ignore 565 | 566 | member private this.Complete() = 567 | if actualIsolationLevel = IsolationLevel.ReadUncommitted then 568 | try 569 | _connection 570 | |> Option.iter (fun conn -> conn.Execute("PRAGMA read_uncommitted = 0;") |> ignore) 571 | with 572 | | _ -> () // Ignore failure attempting to clean up 573 | 574 | _connection <- None 575 | _completed <- true 576 | 577 | member private this.RollbackInternal() = 578 | try 579 | if not _externalRollback then 580 | _connection 581 | |> Option.iter (fun conn -> conn.Execute("ROLLBACK;") |> ignore) 582 | finally 583 | this.Complete() 584 | 585 | interface IDbTransaction with 586 | member this.Dispose() = 587 | if not _completed then 588 | match _connection with 589 | | Some conn when conn.State = ConnectionState.Open -> 590 | this.RollbackInternal() 591 | | _ -> () 592 | 593 | member this.Connection 594 | with get() = match _connection with Some x -> x | None -> null 595 | 596 | member this.IsolationLevel 597 | with get() = actualIsolationLevel 598 | 599 | member this.Commit() = this.Commit() 600 | 601 | member this.Rollback() = this.Rollback() 602 | 603 | 604 | and [] CachingDbConnection internal (connection: SqliteConnection, onDispose, config: Types.SoloDBConfiguration) = 605 | inherit DirectConnection(connection, false) 606 | let mutable preparedCache = Dictionary; CallCount: int64 ref; InUse : bool ref |}>() 607 | let maxCacheSize = 1000 608 | 609 | let tryCachedCommand (sql: string) (parameters: obj) = 610 | // @VAR variable names are randomly generated, so caching them is not possible. 611 | if sql.Contains "@VAR" then ValueNone else 612 | if not config.CachingEnabled then 613 | ValueNone 614 | else 615 | 616 | if preparedCache.Count >= maxCacheSize then 617 | let arr = preparedCache |> Seq.toArray 618 | arr |> Array.sortInPlaceBy (fun (KeyValue(_sql, item)) -> !item.CallCount) 619 | 620 | for i in 1..(maxCacheSize / 4) do 621 | preparedCache.Remove (arr.[i].Key) |> ignore 622 | arr.[i].Value.Command.Dispose() 623 | 624 | 625 | let item = 626 | match preparedCache.TryGetValue sql with 627 | | true, x -> x 628 | | false, _ -> 629 | let command = connection.CreateCommand() 630 | command.CommandText <- sql 631 | processParameters addParameter command parameters 632 | command.Prepare() 633 | 634 | let item = {| Command = command :> IDbCommand; ColumnDict = Dictionary(); CallCount = ref 0L; InUse = ref false |} 635 | preparedCache.[sql] <- item 636 | item 637 | 638 | if !item.InUse then ValueNone else 639 | 640 | item.CallCount := !item.CallCount + 1L 641 | item.InUse := true 642 | 643 | processParameters setOrAddParameter item.Command parameters 644 | struct (item.Command, item.ColumnDict, item.InUse) |> ValueSome 645 | 646 | member this.Inner = connection 647 | 648 | /// Clears the cache while waiting the all cached connections to not be in use. 649 | member this.ClearCache() = 650 | if preparedCache.Count = 0 then () else 651 | 652 | let oldCache = preparedCache 653 | preparedCache <- Dictionary; CallCount: int64 ref; InUse : bool ref |}>() 654 | 655 | while oldCache.Count > 0 do 656 | for KeyValue(k, v) in oldCache |> Seq.toArray do 657 | if (not !v.InUse) then 658 | v.Command.Dispose() 659 | ignore (oldCache.Remove k) 660 | 661 | member this.Execute(sql: string, [] parameters: obj) = 662 | match tryCachedCommand sql parameters with 663 | | ValueSome struct (command, _columnDict, inUse) -> 664 | try command.ExecuteNonQuery() 665 | finally inUse := false 666 | | ValueNone -> 667 | 668 | use command = createCommand connection sql parameters 669 | command.Prepare() // To throw all errors, not silently fail them. 670 | command.ExecuteNonQuery() 671 | 672 | member this.Query<'T>(sql: string, [] parameters: obj) = seq { 673 | match tryCachedCommand sql parameters with 674 | | ValueSome struct (command, columnDict, inUse) -> 675 | try yield! queryCommand<'T> command columnDict 676 | finally inUse := false 677 | | ValueNone -> 678 | use command = createCommand connection sql parameters 679 | yield! queryCommand<'T> command null 680 | } 681 | 682 | member this.QueryFirst<'T>(sql: string, [] parameters: obj) = 683 | match tryCachedCommand sql parameters with 684 | | ValueSome struct (command, columnDict, inUse) -> 685 | try queryCommand<'T> command columnDict |> Seq.head 686 | finally inUse := false 687 | | ValueNone -> 688 | use command = createCommand connection sql parameters 689 | queryCommand<'T> command null |> Seq.head 690 | 691 | member this.QueryFirstOrDefault<'T>(sql: string, [] parameters: obj) = 692 | match tryCachedCommand sql parameters with 693 | | ValueSome struct (command, columnDict, inUse) -> 694 | try 695 | match queryCommand<'T> command columnDict |> Seq.tryHead with 696 | | Some x -> x 697 | | None -> Unchecked.defaultof<'T> 698 | finally inUse := false 699 | 700 | | ValueNone -> 701 | use command = createCommand connection sql parameters 702 | 703 | match queryCommand<'T> command null |> Seq.tryHead with 704 | | Some x -> x 705 | | None -> Unchecked.defaultof<'T> 706 | 707 | member this.Query<'T1, 'T2, 'TReturn>(sql: string, map: Func<'T1, 'T2, 'TReturn>, parameters: obj, splitOn: string) = seq { 708 | let struct (command, dict, dispose, inUse) = 709 | match tryCachedCommand sql parameters with 710 | | ValueSome struct (command, columnDict, inUse) -> 711 | struct (command, columnDict, false, Some inUse) 712 | | ValueNone -> 713 | struct (createCommand connection sql parameters, Dictionary(), true, None) 714 | try 715 | use reader = command.ExecuteReader() 716 | 717 | if dict.Count = 0 then 718 | for i in 0..(reader.FieldCount - 1) do 719 | dict.Add(reader.GetName(i), i) 720 | 721 | let splitIndex = reader.GetOrdinal(splitOn) 722 | 723 | while reader.Read() do 724 | let t1 = TypeMapper<'T1>.Map reader 0 dict 725 | let t2 = 726 | if reader.IsDBNull(splitIndex) then Unchecked.defaultof<'T2> 727 | else TypeMapper<'T2>.Map reader splitIndex dict 728 | 729 | yield map.Invoke (t1, t2) 730 | finally 731 | match inUse with 732 | | Some inUse -> inUse := false 733 | | _ -> () 734 | if dispose then command.Dispose() 735 | } 736 | 737 | member this.BeginTransaction() = this.BeginTransaction(IsolationLevel.Unspecified) 738 | member this.BeginTransaction(deferred: bool) = connection.BeginTransaction(IsolationLevel.Unspecified, deferred) 739 | member this.BeginTransaction(isolation: IsolationLevel) = this.BeginTransaction(isolation, (isolation = IsolationLevel.ReadUncommitted)) 740 | member this.BeginTransaction(isolation: IsolationLevel, deferred: bool) = 741 | if this.State <> ConnectionState.Open then 742 | (raise << InvalidOperationException) "BeginTransaction requires call to Open()." 743 | 744 | new CachingDbConnectionTransaction(this, isolation, deferred) 745 | 746 | interface IDisposable with 747 | override this.Dispose (): unit = 748 | onDispose this 749 | 750 | [] 751 | type IDbConnectionExtensions = 752 | [] 753 | static member Execute(this: IDbConnection, sql: string, [] parameters: obj) = 754 | match this with 755 | | :? CachingDbConnection as c -> c.Execute(sql, parameters) 756 | | _ -> 757 | use command = createCommand this sql parameters 758 | command.Prepare() // To throw all errors, not silently fail them. 759 | command.ExecuteNonQuery() 760 | 761 | [] 762 | static member Query<'T>(this: IDbConnection, sql: string, [] parameters: obj) = 763 | match this with 764 | | :? CachingDbConnection as c -> c.Query<'T>(sql, parameters) 765 | | _ -> 766 | queryInner<'T> this sql parameters 767 | 768 | [] 769 | static member QueryFirst<'T>(this: IDbConnection, sql: string, [] parameters: obj) = 770 | match this with 771 | | :? CachingDbConnection as c -> c.QueryFirst<'T>(sql, parameters) 772 | | _ -> 773 | queryInner<'T> this sql parameters |> Seq.head 774 | 775 | [] 776 | static member QueryFirstOrDefault<'T>(this: IDbConnection, sql: string, [] parameters: obj) = 777 | match this with 778 | | :? CachingDbConnection as c -> c.QueryFirstOrDefault<'T>(sql, parameters) 779 | | _ -> 780 | match queryInner<'T> this sql parameters |> Seq.tryHead with 781 | | Some x -> x 782 | | None -> Unchecked.defaultof<'T> 783 | 784 | [] 785 | static member Query<'T1, 'T2, 'TReturn>(this: IDbConnection, sql: string, map: Func<'T1, 'T2, 'TReturn>, parameters: obj, splitOn: string) = 786 | match this with 787 | | :? CachingDbConnection as c -> c.Query<'T1, 'T2, 'TReturn>(sql, map, parameters, splitOn) 788 | | _ -> 789 | 790 | seq { 791 | use command = createCommand this sql parameters 792 | use reader = command.ExecuteReader() 793 | 794 | let dict = Dictionary(reader.FieldCount) 795 | 796 | for i in 0..(reader.FieldCount - 1) do 797 | dict.Add(reader.GetName(i), i) 798 | 799 | let splitIndex = reader.GetOrdinal(splitOn) 800 | 801 | while reader.Read() do 802 | let t1 = TypeMapper<'T1>.Map reader 0 dict 803 | let t2 = 804 | if reader.IsDBNull(splitIndex) then Unchecked.defaultof<'T2> 805 | else TypeMapper<'T2>.Map reader splitIndex dict 806 | 807 | yield map.Invoke (t1, t2) 808 | } -------------------------------------------------------------------------------- /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.2.2 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/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 -> IDbConnection 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/SoloDBOperators.fs: -------------------------------------------------------------------------------- 1 | namespace SoloDatabase 2 | 3 | open System.Linq.Expressions 4 | open System.Linq 5 | open System 6 | 7 | module Operators = 8 | type SoloDB with 9 | // The pipe operators. 10 | // If you want to use Expression> fluently, 11 | // by just defining a normal function (fun (...) -> ...) you need to use a static member. 12 | 13 | static member collection<'T> (db: SoloDB) = db.GetCollection<'T>() 14 | static member collectionUntyped name (db: SoloDB) = db.GetUntypedCollection name 15 | 16 | static member drop<'T> (db: SoloDB) = db.DropCollection<'T>() 17 | static member dropByName name (db: SoloDB) = db.DropCollection name 18 | 19 | static member tryDrop<'T> (db: SoloDB) = db.DropCollectionIfExists<'T>() 20 | static member tryDropByName name (db: SoloDB) = db.DropCollectionIfExists name 21 | 22 | static member withTransaction func (db: SoloDB) = db.WithTransaction func 23 | static member optimize (db: SoloDB) = db.Optimize() 24 | 25 | static member ensureIndex<'T, 'R> (func: Expression>) (collection: ISoloDBCollection<'T>) = collection.EnsureIndex func 26 | static member tryDropIndex<'T, 'R> (func: Expression>) (collection: ISoloDBCollection<'T>) = collection.DropIndexIfExists func 27 | 28 | static member countWhere<'T> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> collection.Where(func).LongCount() 29 | static member count<'T> (collection: ISoloDBCollection<'T>) = collection.LongCount() 30 | static member countAll<'T> (collection: ISoloDBCollection<'T>) = collection.LongCount() 31 | static member countAllLimit<'T> (limit: int32) (collection: ISoloDBCollection<'T>) = collection.Take(limit).LongCount() 32 | 33 | static member insert<'T> (item: 'T) (collection: ISoloDBCollection<'T>) = collection.Insert item 34 | static member insertBatch<'T> (items: 'T seq) (collection: ISoloDBCollection<'T>) = collection.InsertBatch items 35 | 36 | static member update<'T> (item: 'T) (collection: ISoloDBCollection<'T>) = collection.Update item 37 | static member replace<'T> (item: 'T) (collection: ISoloDBCollection<'T>) = collection.Update item 38 | 39 | static member delete<'T> (collection: ISoloDBCollection<'T>) = collection.DeleteMany(fun _ -> true) 40 | static member deleteById<'T> id (collection: ISoloDBCollection<'T>) = collection.Delete id 41 | 42 | static member select<'T, 'R> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> collection.Select func 43 | static member select<'T> () = fun (collection: ISoloDBCollection<'T>) -> collection 44 | static member selectUnique<'T, 'R> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> collection.Select(func).Distinct() 45 | 46 | static member where<'T> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> collection.Where func 47 | static member whereId (id: int64) (builder: ISoloDBCollection<'T>) = builder.GetById id 48 | 49 | static member limit (count: int) (builder: IQueryable<'T>) = builder.Take count 50 | static member offset (count: int) (builder: IQueryable<'T>) = builder.Skip count 51 | 52 | static member orderAsc (func: Expression>) = fun (builder: IQueryable<'T>) -> builder.OrderBy func 53 | static member orderDesc (func: Expression>) = fun (builder: IQueryable<'T>) -> builder.OrderByDescending func 54 | 55 | static member explain (builder: IQueryable<'T>) = raise(NotImplementedException ()) 56 | 57 | static member getById (id: int64) (collection: ISoloDBCollection<'T>) = collection.GetById id 58 | static member tryGetById (id: int64) (collection: ISoloDBCollection<'T>) = collection.TryGetById id 59 | 60 | static member tryFirst<'T> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> func |> collection.Select |> Seq.tryHead 61 | 62 | static member any<'T> (func: Expression>) = fun (collection: ISoloDBCollection<'T>) -> func |> collection.Any 63 | 64 | static member toSeq (collection: ISoloDBCollection<'T>) = collection.AsEnumerable() 65 | static member toList (collection: ISoloDBCollection<'T>) = collection.ToList() -------------------------------------------------------------------------------- /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 | Hash: byte array 48 | Metadata: IReadOnlyDictionary 49 | } 50 | 51 | [] 52 | type SoloDBDirectoryHeader = { 53 | Id: int64 54 | Name: string 55 | FullPath: string 56 | ParentId: Nullable 57 | Created: DateTimeOffset 58 | Modified: DateTimeOffset 59 | Metadata: IReadOnlyDictionary 60 | } 61 | 62 | [] 63 | type SoloDBEntryHeader = 64 | | File of file: SoloDBFileHeader 65 | | Directory of directory: SoloDBDirectoryHeader 66 | 67 | member this.Name = 68 | match this with 69 | | File f -> f.Name 70 | | Directory d -> d.Name 71 | 72 | member this.FullPath = 73 | match this with 74 | | File f -> f.FullPath 75 | | Directory d -> d.FullPath 76 | 77 | member this.DirectoryId = 78 | match this with 79 | | File f -> f.DirectoryId |> Nullable 80 | | Directory d -> d.ParentId 81 | 82 | member this.Created = 83 | match this with 84 | | File f -> f.Created 85 | | Directory d -> d.Created 86 | 87 | member this.Modified = 88 | match this with 89 | | File f -> f.Modified 90 | | Directory d -> d.Modified 91 | 92 | member this.Metadata = 93 | match this with 94 | | File f -> f.Metadata 95 | | Directory d -> d.Metadata 96 | 97 | [] 98 | type internal SoloDBFileChunk = { 99 | Id: int64 100 | FileId: int64 101 | Number: int64 102 | Data: byte array 103 | } 104 | 105 | type SoloDBConfiguration = internal { 106 | /// The general switch to enable or disable caching. 107 | /// By disabling it, any cached data will be automatically cleared. 108 | mutable CachingEnabled: bool 109 | } -------------------------------------------------------------------------------- /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 | typeof.IsAssignableFrom t || typeof.IsAssignableFrom t || t.Name.StartsWith "Tuple`" || t.Name.StartsWith "ValueTuple`" 41 | 42 | type System.Char with 43 | static member IsAsciiLetterOrDigit this = 44 | (this >= '0' && this <= '9') || 45 | (this >= 'A' && this <= 'Z') || 46 | (this >= 'a' && this <= 'z') 47 | 48 | static member IsAsciiLetter this = 49 | (this >= 'A' && this <= 'Z') || 50 | (this >= 'a' && this <= 'z') 51 | 52 | type System.Decimal with 53 | static member IsInteger (this: Decimal) = 54 | this = (Decimal.Floor this) 55 | 56 | [] 57 | let internal (|OfType|_|) (_typ: 'x -> 'a) (objType: Type) = 58 | if typeof<'a>.Equals objType || typeof<'a>.IsAssignableFrom(objType) then ValueSome () else ValueNone 59 | 60 | let internal isNumber (value: obj) = 61 | match value with 62 | | :? int8 63 | | :? uint8 64 | | :? int16 65 | | :? uint16 66 | | :? int32 67 | | :? uint32 68 | | :? int64 69 | | :? uint64 70 | | :? nativeint 71 | 72 | | :? float32 73 | | :? float 74 | | :? decimal -> true 75 | | _ -> false 76 | 77 | let internal isIntegerBasedType (t: Type) = 78 | match t with 79 | | OfType int8 80 | | OfType uint8 81 | | OfType int16 82 | | OfType uint16 83 | | OfType int32 84 | | OfType uint32 85 | | OfType int64 86 | | OfType uint64 87 | | OfType nativeint 88 | -> true 89 | | _ -> false 90 | 91 | let inline internal isFloatBasedType (t: Type) = 92 | match t with 93 | | OfType float32 94 | | OfType float 95 | | OfType decimal 96 | -> true 97 | | _ -> false 98 | 99 | let internal isIntegerBased (value: obj) = 100 | match value with 101 | | :? int8 102 | | :? uint8 103 | | :? int16 104 | | :? uint16 105 | | :? int32 106 | | :? uint32 107 | | :? int64 108 | | :? uint64 109 | | :? nativeint 110 | -> true 111 | | _other -> false 112 | 113 | let internal typeToName (t: Type) = 114 | let fullname = t.FullName 115 | if fullname.Length > 0 && Char.IsAsciiLetter fullname.[0] // To not insert auto generated classes. 116 | then Some fullname 117 | else None 118 | 119 | let private nameToTypeCache = ConcurrentDictionary() 120 | 121 | let internal nameToType (typeName: string) = 122 | nameToTypeCache.GetOrAdd(typeName, fun typeName -> 123 | match typeName with 124 | | "Double" | "double" -> typeof 125 | | "Single" | "float" -> typeof 126 | | "Byte" | "byte" -> typeof 127 | | "SByte" | "sbyte" -> typeof 128 | | "Int16" | "short" -> typeof 129 | | "UInt16" | "ushort" -> typeof 130 | | "Int32" | "int" -> typeof 131 | | "UInt32" | "uint" -> typeof 132 | | "Int64" | "long" -> typeof 133 | | "UInt64" | "ulong" -> typeof 134 | | "Char" | "char" -> typeof 135 | | "Boolean" | "bool" -> typeof 136 | | "Object" | "object" -> typeof 137 | | "String" | "string" -> typeof 138 | | "Decimal" | "decimal" -> typeof 139 | | "DateTime" -> typeof 140 | | "Guid" -> typeof 141 | | "TimeSpan" -> typeof 142 | | "IntPtr" -> typeof 143 | | "UIntPtr" -> typeof 144 | | "Array" -> typeof 145 | | "Delegate" -> typeof 146 | | "MulticastDelegate" -> typeof 147 | | "IDisposable" -> typeof 148 | | "Stream" -> typeof 149 | | "Exception" -> typeof 150 | | "Thread" -> typeof 151 | | typeName -> 152 | 153 | match Type.GetType(typeName) with 154 | | null -> 155 | match Type.GetType("System." + typeName) with 156 | | null -> 157 | AppDomain.CurrentDomain.GetAssemblies() 158 | |> Seq.collect(fun a -> a.GetTypes()) 159 | |> Seq.find(fun t -> t.FullName = typeName) 160 | | fastType -> fastType 161 | | fastType -> fastType 162 | ) 163 | 164 | let private shaHashBytes (bytes: byte array) = 165 | use sha = SHA1.Create() 166 | sha.ComputeHash(bytes) 167 | 168 | let internal shaHash (o: obj) = 169 | match o with 170 | | :? (byte array) as bytes -> 171 | shaHashBytes(bytes) 172 | | :? string as str -> 173 | shaHashBytes(str |> Encoding.UTF8.GetBytes) 174 | | other -> raise (InvalidDataException(sprintf "Cannot hash object of type: %A" (other.GetType()))) 175 | 176 | let internal bytesToHex (hash: byte array) = 177 | let sb = new StringBuilder() 178 | for b in hash do 179 | sb.Append (b.ToString("x2")) |> ignore 180 | 181 | sb.ToString() 182 | 183 | 184 | // State machine states 185 | type internal State = 186 | | Valid // Currently valid base64 sequence 187 | | Invalid // Invalid sequence 188 | | PaddingOne // Seen one padding character 189 | | PaddingTwo // Seen two padding characters (max allowed) 190 | 191 | /// 192 | /// Finds the last index of valid base64 content in a string using a state machine approach 193 | /// 194 | /// String to check for base64 content 195 | /// Index of the last valid base64 character, or -1 if no valid base64 found 196 | let internal findLastValidBase64Index (input: string) = 197 | if System.String.IsNullOrEmpty(input) then 198 | -1 199 | else 200 | // Base64 alphabet check - optimized with lookup array 201 | let isBase64Char c = 202 | let code = c 203 | (code >= 'A' && code <= 'Z') || 204 | (code >= 'a' && code <= 'z') || 205 | (code >= '0' && code <= '9') || 206 | c = '+' || c = '/' || c = '=' 207 | 208 | 209 | // Track the last valid position 210 | let mutable lastValidPos = -1 211 | // Current position in the quadruplet (base64 works in groups of 4) 212 | let mutable quadPos = 0 213 | // Current state 214 | let mutable state = State.Valid 215 | 216 | for i = 0 to input.Length - 1 do 217 | let c = input.[i] 218 | 219 | match state with 220 | | State.Valid -> 221 | if not (isBase64Char c) then 222 | // Non-base64 character found 223 | state <- State.Invalid 224 | elif c = '=' then 225 | // Padding can only appear at positions 2 or 3 in a quadruplet 226 | if quadPos = 2 then 227 | state <- State.PaddingOne 228 | lastValidPos <- i 229 | elif quadPos = 3 then 230 | state <- State.PaddingTwo 231 | lastValidPos <- i 232 | else 233 | state <- State.Invalid 234 | else 235 | lastValidPos <- i 236 | quadPos <- (quadPos + 1) % 4 237 | 238 | | State.PaddingOne -> 239 | if c = '=' && quadPos = 3 then 240 | // Second padding character is only valid at position 3 241 | state <- State.PaddingTwo 242 | lastValidPos <- i 243 | quadPos <- 0 // Reset for next quadruplet 244 | else 245 | state <- State.Invalid 246 | 247 | | State.PaddingTwo -> 248 | // After two padding characters, we should start a new quadruplet 249 | if isBase64Char c && c <> '=' then 250 | state <- State.Valid 251 | quadPos <- 1 // Position 0 is this character 252 | lastValidPos <- i 253 | else 254 | state <- State.Invalid 255 | 256 | | State.Invalid -> 257 | // Once invalid, check if we can start a new valid sequence 258 | if isBase64Char c && c <> '=' then 259 | state <- State.Valid 260 | quadPos <- 1 // Position 0 is this character 261 | lastValidPos <- i 262 | 263 | // Final validation: for a complete valid base64 string, we need quadPos = 0 264 | // or a valid padding situation at the end 265 | match state with 266 | | State.Valid when quadPos = 0 -> lastValidPos 267 | | State.PaddingOne | State.PaddingTwo -> lastValidPos 268 | | _ -> 269 | // If we don't end with complete quadruplet, find the last complete one 270 | if lastValidPos >= 0 then 271 | let remainingChars = (lastValidPos + 1) % 4 272 | if remainingChars = 0 then 273 | lastValidPos 274 | else 275 | lastValidPos - remainingChars + 1 276 | else 277 | -1 278 | 279 | let internal trimToValidBase64 (input: string) = 280 | if System.String.IsNullOrEmpty(input) then 281 | input 282 | else 283 | let lastValidIndex = findLastValidBase64Index input 284 | 285 | if lastValidIndex >= 0 then 286 | if lastValidIndex = input.Length - 1 then 287 | // Already valid, no need to create a new string 288 | input 289 | else 290 | // Extract only the valid part 291 | input.Substring(0, lastValidIndex + 1) 292 | else 293 | // No valid base64 found 294 | "" 295 | 296 | let internal sqlBase64 (data: obj) = 297 | match data with 298 | | null -> null 299 | | :? (byte array) as bytes -> 300 | // Requirement #2: If BLOB, encode to base64 TEXT 301 | System.Convert.ToBase64String(bytes) :> obj 302 | | :? string as str -> 303 | // Requirement #6: Ignore leading and trailing whitespace 304 | let trimmedStr = str.Trim() 305 | let trimmedStr = trimToValidBase64 trimmedStr 306 | 307 | match trimmedStr.Length with 308 | | 0 -> Array.empty :> obj 309 | | _ -> 310 | 311 | System.Convert.FromBase64String trimmedStr :> obj 312 | | _ -> 313 | // Requirement #5: Raise an error for types other than TEXT, BLOB, or NULL 314 | failwith "The base64() function requires a TEXT, BLOB, or NULL argument" 315 | 316 | [] 317 | type internal GenericMethodArgCache = 318 | static member val private cache = ConcurrentDictionary({ 319 | new IEqualityComparer with 320 | override this.Equals (x: MethodInfo, y: MethodInfo): bool = 321 | x.MethodHandle.Value = y.MethodHandle.Value 322 | override this.GetHashCode (obj: MethodInfo): int = 323 | obj.MethodHandle.Value |> int 324 | }) 325 | 326 | static member Get(method: MethodInfo) = 327 | let args = GenericMethodArgCache.cache.GetOrAdd(method, (fun m -> m.GetGenericArguments())) 328 | args 329 | 330 | [] 331 | type internal GenericTypeArgCache = 332 | static member val private cache = ConcurrentDictionary({ 333 | new IEqualityComparer with 334 | override this.Equals (x: Type, y: Type): bool = 335 | x.TypeHandle.Value = y.TypeHandle.Value 336 | override this.GetHashCode (obj: Type): int = 337 | obj.TypeHandle.Value |> int 338 | }) 339 | 340 | static member Get(t: Type) = 341 | let args = GenericTypeArgCache.cache.GetOrAdd(t, (fun m -> m.GetGenericArguments())) 342 | args 343 | 344 | /// For the F# compiler to allow the implicit use of 345 | /// the .NET Expression we need to use it in a C# style class. 346 | [][] // make it static 347 | type internal ExpressionHelper = 348 | static member inline internal get<'a, 'b>(expression: Expression>) = expression 349 | 350 | static member inline internal id (x: Type) = 351 | let parameter = Expression.Parameter x 352 | Expression.Lambda(parameter, [|parameter|]) 353 | 354 | static member inline internal eq (x: Type) (b: obj) = 355 | let parameter = Expression.Parameter x 356 | Expression.Lambda(Expression.Equal(parameter, Expression.Constant(b, x)), [|parameter|]) 357 | 358 | module internal SeqExt = 359 | let internal sequentialGroupBy keySelector (sequence: seq<'T>) = 360 | seq { 361 | use enumerator = sequence.GetEnumerator() 362 | if enumerator.MoveNext() then 363 | let mutable currentKey = keySelector enumerator.Current 364 | let mutable currentList = System.Collections.Generic.List<'T>() 365 | let mutable looping = true 366 | 367 | while looping do 368 | let current = enumerator.Current 369 | let key = keySelector current 370 | 371 | if key = currentKey then 372 | currentList.Add current 373 | else 374 | yield currentList :> IList<'T> 375 | currentList.Clear() 376 | currentList.Add current 377 | currentKey <- key 378 | 379 | if not (enumerator.MoveNext()) then 380 | yield currentList :> IList<'T> 381 | looping <- false 382 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unconcurrent/SoloDB/43949b8a6ebb05ddff455d7045812acd7c60f081/icon.png --------------------------------------------------------------------------------