├── .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 | [