├── .gitattributes ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── README.md ├── SliccDB.Tests ├── DatabaseConnectionTests.cs ├── SliccDB.Tests.csproj └── data │ └── sample.sliccdb ├── SliccDB.sln ├── SliccDB ├── Core │ ├── ConnectionStatus.cs │ ├── GraphEntity.cs │ ├── Node.cs │ ├── QueryResult.cs │ ├── Relation.cs │ └── Schema.cs ├── Database.cs ├── Exceptions │ ├── RelationExistsException.cs │ ├── SchemaExistsException.cs │ ├── SchemaValidationException.cs │ ├── SliccDbException.cs │ └── TargetOrSourceNotFoundException.cs ├── Fluent │ └── DatabaseConnectionExtensions.cs ├── SLICC_128.png ├── Serialization │ └── DatabaseConnection.cs └── SliccDB.csproj ├── license.md └── screenshots └── Explorer.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 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 6.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore /property:Version=0.1.1.${{github.run_number}} 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | - name: Upload a Build Artifact 30 | uses: actions/upload-artifact@v3.1.2 31 | with: 32 | name: Latest 33 | path: ~/work/SliccDB/SliccDB/SliccDB/bin/Debug/SliccDB.0.1.1.${{github.run_number}}.nupkg 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.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 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SliccDB 2 | ![C#](https://img.shields.io/badge/c%23-%23239120.svg?style=for-the-badge&logo=c-sharp&logoColor=white) ![.Net](https://img.shields.io/badge/.NET-5C2D91?style=for-the-badge&logo=.net&logoColor=white) ![Visual Studio](https://img.shields.io/badge/Visual%20Studio-5C2D91.svg?style=for-the-badge&logo=visual-studio&logoColor=white) 3 | ![Version](https://img.shields.io/badge/Version-0.1.1-brightgreen?style=for-the-badge) 4 | 5 | Light Embedded Graph Database for .net 6 | 7 | 8 | ## What is it? 9 | Think of it like SQLite for graph databases. There were some efforts to build something like this (such as Graph Engine) but Microsoft decided to abandon all of their graph database related projects. Be aware that I have no intention to make this some sort of Neo4J .net Clone. It will be not fully compliant with features of mentioned database. 10 | 11 | ## Installation 12 | You have three options to get this package. 13 | ### Nuget Package 14 | You can download nuget package from [here](https://www.nuget.org/packages/SliccDB/) or by using this command 15 | ``` 16 | NuGet\Install-Package SliccDB 17 | ``` 18 | 19 | ### Unstable Builds 20 | If you feel adventurous, you can download SliccDB as a build artifact from github actions. Note that these are not official releases, so they may contain bugs. 21 | But, they can also contain features not yet found on nuget... you never know ¯\_(ツ)_/¯ 22 | 23 | ### Compiling from source 24 | You can always just clone this repo, open the solution with visual studio and compile it yourself. The build will produce both the dll and nuget package. 25 | You can also just build it with dotnet tool thingy. You're the boss here! 26 | 27 | 28 | ### Examples 29 | Here are some simple examples: 30 | 31 | #### Basic Operations 32 | 33 | ##### Create Node Programatically 34 | 35 | ```Csharp 36 | DatabaseConnection connection = new DatabaseConnection("filename"); 37 | var properties = new Dictionary(); 38 | var labels = new HashSet(); 39 | properties.Add("Name", "Alice"); 40 | labels.Add("Person"); 41 | 42 | var nodeOne = connection.CreateNode(properties, labels); 43 | properties.Clear(); 44 | labels.Clear(); 45 | properties.Add("Name", "Steve"); 46 | labels.Add("Person"); 47 | 48 | var nodeTwo = connection.CreateNode(properties, labels); 49 | ``` 50 | 51 | ##### Create Relations between two nodes 52 | Note that relations also have ability to hold data like Properties and Labels 53 | ```Csharp 54 | var properties = new Dictionary(); 55 | var labels = new HashSet(); 56 | properties.Add("How Much", "Very Much"); 57 | labels.Add("Label on a node!"); 58 | connection.CreateRelation( 59 | "Likes", 60 | sn => sn.First(x => x.Hash == nodeOne.Hash), tn => tn.First(a => a.Hash == nodeTwo.Hash), properties, labels); 61 | ``` 62 | 63 | #### Queries 64 | 65 | You can query nodes and relations as you would any collection (With Linq). 66 | 67 | ###### Query Nodes 68 | 69 | ```Csharp 70 | var selectedNode = Connection.Nodes().Properties("Name".Value("Tom")).Labels("Person").FirstOrDefault(); 71 | ``` 72 | 73 | ###### Query Relations 74 | 75 | ```Csharp 76 | var queriedRelation = Connection.Relations().Properties("Property".Value("PropertyValue")) 77 | .Labels("RelationLabel"); 78 | ``` 79 | 80 | 81 | #### Mutations 82 | 83 | ##### Updates 84 | 85 | ```Csharp 86 | var relation = Connection.Relations("Likes").ToList().First(); 87 | relation.Properties["How Much"] = "Not So Much"; 88 | relation.Labels.Add("Love Hate Relationship"); 89 | 90 | Connection.Update(relation); 91 | 92 | ... 93 | 94 | var node = Connection.Nodes().Properties("Name".Value("Steve")).First(); 95 | node.Properties["Name"] = "Steve2"; 96 | 97 | Connection.Update(node); 98 | ``` 99 | 100 | ###### Deletions 101 | 102 | ```CSharp 103 | var toDelete = Connection.CreateNode( 104 | new Dictionary() { { "Name", "Tom" } }, 105 | new HashSet() { "Person" } 106 | ); 107 | 108 | var queryToDelete = Connection.Nodes().Properties("Name".Value("Tom")).Labels("Person").FirstOrDefault(); 109 | 110 | Connection.Delete(queryToDelete); 111 | ``` 112 | ```CSharp 113 | var testNode1 = Connection.CreateNode( 114 | new Dictionary() { { "Name", "C" } }, 115 | new HashSet() { "S" } 116 | ); 117 | 118 | var testNode2 = Connection.CreateNode( 119 | new Dictionary() { { "Name", "A" } }, 120 | new HashSet() { "S" } 121 | ); 122 | 123 | var properties = new Dictionary(); 124 | var labels = new HashSet(); 125 | properties.Add("Test", "Test"); 126 | labels.Add("Test on a node!"); 127 | Connection.CreateRelation("Test", sn => sn.First(x => x.Hash == testNode1.Hash), tn => tn.First(a => a.Hash == testNode2.Hash), properties, labels); 128 | 129 | var queriedRelation = Connection.Relations("Test").FirstOrDefault(); 130 | 131 | Connection.Delete(queriedRelation); 132 | ``` 133 | 134 | #### Schemas and Mappings 135 | Since 0.1.1 SliccDb features object - graph entity mapping, allowing to treat Relations and Nodes like regular poco classes. 136 | To ensure correct mapping for both types and property names it was neccessary to implement some sort of **schema enforcement**.Therefore, schemas were born. 137 | 138 | ##### Schemas 139 | Schemas are like recipes for your graph entities, they enforce the existence of certain fields and allow to specify to which type these fields should be mapped. 140 | Schemas are strongly related to labels. Schemas are defined per label. 141 | 142 | Example: 143 | ```Csharp 144 | schema = Connection.CreateSchema("Person", 145 | new List() { new() { FullTypeName = "System.String", Name = "Name" }, new() { FullTypeName = "System.Double", Name = "Age" } }); 146 | ``` 147 | 148 | Now when we label any entity with "Person", missing fields will be automatically added to it. In this example, when we add a label "Person" to a node or relation it will create Fields "Name" and "Age" if those do not exist. 149 | 150 | ##### Mapper 151 | Mapper allows us to manipulate graph by manipulating regular POCOs. 152 | Let's say we have two classes, representing Trains, Stations and a class RidesTo which represents relation between train and station: 153 | ```Csharp 154 | public class Train 155 | { 156 | public string SerialNumber { get; set; } 157 | } 158 | public class Station 159 | { 160 | public string City { get; set; } 161 | } 162 | public class RidesTo 163 | { 164 | public int Platform { get; set; } 165 | } 166 | ``` 167 | 168 | By passing an object as an argument to method CreateNode we can create node with properties equal to the properties of an object: 169 | ```Csharp 170 | Connection.CreateNode(new Train() { SerialNumber = "CE2332" }); 171 | Connection.CreateNode(new Train() { SerialNumber = "CE2356" }); 172 | Connection.CreateNode(new Station() { City = "Katowice" }); 173 | Connection.CreateNode(new Station() { City = "Warsaw" }); 174 | ``` 175 | We can also create a relation connecting two stations (by a query) with a train: 176 | 177 | ```Csharp 178 | Connection.CreateRelation(n => 179 | Connection.Nodes().Properties("SerialNumber".Value("CE2332")).FirstOrDefault(), 180 | a => Connection.Nodes().Properties("SerialNumber".Value("CE2356")).FirstOrDefault(), 181 | new RidesTo(){ Platform = 2 }); 182 | ``` 183 | 184 | ### Why it exists? 185 | I just need embedded Graph Database Solution that supports Cypher Language. 186 | Graph DBMS is a perfect solution for some gamedev ai-related stuff (Procedural Behaviour Design, Perception and context awareness storage), but every solution needs a server. Imagine that in order to play your game you must install Neo4J's server in the first place, or every time your npc makes a decision it queries Azure Cloud to check how much he likes pizza. It's unreliable and forces you to host a game server for the entirety of a game's lifetime even though your game is singleplayer. Sounds stupid? Well, ask EA about Simcity 2013 - they thought it's a great idea! But really, Graph DBMS has other use cases. [Watch this video to know more about Graph DBMS](https://www.youtube.com/watch?v=GekQqFZm7mA) 187 | 188 | ### SliccDB Studio 189 | You can download SliccDB Studio [here](https://github.com/pmikstacki/SliccDBStudio). 190 | 191 | SliccDB Studio is a companion app currently used for interaction with database. It allows for CRUD operations on the database to test its' functionalities. 192 | 193 | ![SliccDB Studio Screenshot](https://raw.githubusercontent.com/pmikstacki/SliccDBStudio/main/Screenshot.png) 194 | 195 | 196 | 197 | **DB Explorer is now considered obsolete, but you can still get the source code [here](https://github.com/pmikstacki/SliccDB.Explorer/)** 198 | 199 | ### Progress 200 | Below you'll find a list of features that will be included in the 1st milestone release of SliccDB: 201 | - [x] Basic Structures (Relations, Nodes) 202 | - [x] Serialization (Reading and Writing) 203 | - [x] Queries 204 | - [x] Basic Debugger App (Ability to add, remove, edit nodes and relations) 205 | - [x] Schemas for Nodes and Relations 206 | - [x] ORM (Object Relational Mapper) for Nodes and Relations 207 | - [x] New Companion App 208 | ### Can I help? 209 | Of course! Any help with the SliccDB will be appreciated. 210 | #### Special Thanks! 211 | - [Oleitao](https://github.com/oleitao) He wrote first unit tests of the database! 212 | - [HoLLy-Hacker (cool name tho) ](https://github.com/HoLLy-HaCKeR) - He performance tested key functionalities of SliccDB and found a few ~~errors~~ bugs. 213 | -------------------------------------------------------------------------------- /SliccDB.Tests/DatabaseConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using NUnit.Framework; 3 | using SliccDB.Core; 4 | using SliccDB.Serialization; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using SliccDB.Exceptions; 11 | using SliccDB.Fluent; 12 | 13 | namespace SliccDB.Tests 14 | { 15 | public class Tests 16 | { 17 | private const string fileName = "sample.siccdb"; 18 | 19 | public Node NodeOne { get; set; } 20 | 21 | public Node NodeTwo { get; set; } 22 | 23 | public DatabaseConnection Connection { get; set; } 24 | 25 | [SetUp] 26 | public void SetupDatabase() 27 | { 28 | Setup(); 29 | } 30 | 31 | [Test] 32 | public void CreateSchemaTest() 33 | { 34 | try 35 | { 36 | Connection.CreateSchema("Person", 37 | new List() { new() { FullTypeName = "System.String", Name = "Name" } }); 38 | } 39 | catch (SchemaExistsException ex) 40 | { 41 | Assert.Fail(ex.Message); 42 | } 43 | 44 | Assert.IsTrue(Connection.Schemas.Exists(x => x.Label == "Person")); 45 | } 46 | 47 | 48 | [Test] 49 | public void CreateNodeTest() 50 | { 51 | CreateNodes(); 52 | 53 | Assert.IsTrue(NodeOne.Labels.FirstOrDefault(x => x.Contains("Person")) != null, "Label Person not found."); 54 | Assert.IsTrue(NodeTwo.Labels.FirstOrDefault(x => x.Contains("Person")) != null, "Label Person not found."); 55 | 56 | Assert.IsTrue(NodeOne.Properties.FirstOrDefault(x => x.Key.Equals("Name")).Value.Equals("Alice"), "Property Name not found."); 57 | Assert.IsTrue(NodeTwo.Properties.FirstOrDefault(x => x.Key.Equals("Name")).Value.Equals("Steve"), "Property Name not found."); 58 | } 59 | 60 | [Test] 61 | public void CreateRelationsBetweenTwoNodesTest() 62 | { 63 | CreateNodes(); 64 | CreateRelations(); 65 | 66 | Assert.IsTrue(Connection.Relations.Any(x => x.RelationName.Equals("Likes")), "Relation not found."); 67 | } 68 | 69 | [Test] 70 | public void SelectNodeTest() 71 | { 72 | CreateNodes(); 73 | 74 | var selectedNode = Connection.QueryNodes(x => x.Where(xa => 75 | { 76 | xa.Properties.TryGetValue("Name", out var name); 77 | return name == "Steve"; 78 | }).ToList()).First(); 79 | Assert.IsTrue(selectedNode.Labels.Count > 0, "Node not found."); 80 | } 81 | 82 | [Test] 83 | public void SelectEdgeTest() 84 | { 85 | CreateNodes(); 86 | CreateRelations(); 87 | 88 | var selectedEdge = Connection.QueryRelations(x => x.Where(xa => xa.RelationName == "Likes").ToList()).First(); 89 | Assert.IsTrue(selectedEdge.Labels.Count > 0, "Edge not found"); 90 | } 91 | 92 | [Test] 93 | public void HaveNodesSchemaEnforced() 94 | { 95 | 96 | CreateNodes(); 97 | CreateRelations(); 98 | UpdateSchema(); 99 | var nodesWithLabel = Connection.Nodes().Where(x => x.Labels.Contains("Person")); 100 | Assert.IsTrue(nodesWithLabel.ToList().Exists(x => x.Properties.ContainsKey("Age"))); 101 | } 102 | 103 | [Test] 104 | public void SaveDatabase() 105 | { 106 | int nodes = 0; 107 | int relations = 0; 108 | 109 | CreateNodes(); 110 | CreateRelations(); 111 | 112 | nodes = Connection.Nodes.Count; 113 | relations = Connection.Relations.Count; 114 | 115 | Connection.SaveDatabase(); 116 | Connection.CloseDatabase(); 117 | 118 | Setup(); 119 | 120 | Assert.IsTrue(Connection.Nodes.Count.Equals(nodes), "Saved nodes are different."); 121 | Assert.IsTrue(Connection.Relations.Count.Equals(relations), "Saved relations are different."); 122 | } 123 | 124 | private void CreateRelations() 125 | { 126 | var properties = new Dictionary(); 127 | var labels = new HashSet(); 128 | properties.Add("How Much", "Very Much"); 129 | labels.Add("Label on a node!"); 130 | Connection.CreateRelation("Likes", sn => sn.First(x => x.Hash == NodeOne.Hash), tn => tn.First(a => a.Hash == NodeTwo.Hash), properties, labels); 131 | } 132 | 133 | private void CreateNodes() 134 | { 135 | NodeOne = Connection.CreateNode( 136 | new Dictionary() { { "Name", "Alice"}, {"Age", "18"} }, 137 | new HashSet() { "Person" } 138 | ); 139 | 140 | NodeTwo = Connection.CreateNode( 141 | new Dictionary() { { "Name", "Steve" }, { "Age", "20" } }, 142 | new HashSet() { "Person" } 143 | ); 144 | } 145 | 146 | [Test] 147 | public void MapperGetNode() 148 | { 149 | MapperCreateNodes(); 150 | var foundTrain = Connection.Nodes(); 151 | Assert.IsTrue(foundTrain.Values.ToList().Exists(x => x.SerialNumber == "CE2356") && foundTrain.Values.ToList().Exists(x => x.SerialNumber == "CE2332"), foundTrain.Count.ToString()); 152 | } 153 | 154 | 155 | [Test] 156 | public void MapperGetRelation() 157 | { 158 | Connection.ClearDatabase(); 159 | MapperCreateNodes(); 160 | MapperCreateRelation(); 161 | var foundTrain = Connection.Relations(); 162 | Assert.IsTrue(foundTrain.Values.ToList().Exists(x => x.Platform == 2)); 163 | } 164 | 165 | private void MapperCreateNodes() 166 | { 167 | Connection.CreateNode(new Train() { SerialNumber = "CE2332" }); 168 | Connection.CreateNode(new Train() { SerialNumber = "CE2356" }); 169 | Connection.CreateNode(new Station() { City = "Katowice" }); 170 | Connection.CreateNode(new Station() { City = "Warsaw" }); 171 | } 172 | 173 | private void MapperCreateRelation() 174 | { 175 | Connection.CreateRelation(n => 176 | Connection.Nodes().Properties("SerialNumber".Value("CE2332")).FirstOrDefault(), 177 | a => Connection.Nodes().Properties("SerialNumber".Value("CE2356")).FirstOrDefault(), 178 | new RidesTo(){ Platform = 2 }); 179 | } 180 | 181 | [Test] 182 | public void TestNodeUpdateTest() 183 | { 184 | Connection.ClearDatabase(); 185 | CreateNodes(); 186 | CreateRelations(); 187 | var node = Connection.Nodes().Properties("Name".Value("Steve")).First(); 188 | node.Properties["Name"] = "Steve2"; 189 | Connection.Update(node); 190 | 191 | var selectedNode = Connection.QueryNodes(Nodes => Nodes.Where(foundNode => 192 | { 193 | foundNode.Properties.TryGetValue("Name", out var newName); 194 | return newName == "Steve2"; 195 | }).ToList()).First(); 196 | Assert.IsTrue(selectedNode != null, "Node not found."); 197 | } 198 | 199 | 200 | [Test] 201 | public void RelationUpdateTest() 202 | { 203 | Connection.ClearDatabase(); 204 | CreateNodes(); 205 | CreateRelations(); 206 | UpdateSchema(); 207 | var relation = Connection.Relations("Likes").ToList().First(); 208 | relation.Properties["How Much"] = "Not So Much"; 209 | relation.Labels.Add("Love Hate Relationship"); 210 | 211 | Connection.Update(relation); 212 | 213 | var selectedRelation = Connection.QueryRelations(relations => relations.Where(foundRelation => foundRelation.RelationName == "Likes").ToList()).First(); 214 | Assert.IsTrue(selectedRelation != null && selectedRelation.Properties["How Much"] == "Not So Much" && selectedRelation.Labels.Contains("Love Hate Relationship"), "Relation not found or properties weren't changed."); 215 | } 216 | 217 | [Test] 218 | public void NodeDeleteTest() 219 | { 220 | Connection.ClearDatabase(); 221 | var toDelete = Connection.CreateNode( 222 | new Dictionary() { { "Name", "Tom" } , {"Age", "0"}}, 223 | new HashSet() { "Person" } 224 | ); 225 | 226 | var queryToDelete = Connection.Nodes() 227 | .Properties("Name".Value("Tom"))? 228 | .Labels("Person") 229 | .FirstOrDefault(); 230 | Connection.Delete(queryToDelete); 231 | 232 | var queryToCheck = Connection.Nodes().Properties("Name".Value("Tom"))?.Labels("Person").FirstOrDefault(); 233 | 234 | Assert.IsTrue(queryToCheck == null, "Node was not removed"); 235 | 236 | } 237 | 238 | 239 | [Test] 240 | public void RelationDeleteTest() 241 | { 242 | var testNode1 = Connection.CreateNode( 243 | new Dictionary() { { "Name", "C" } }, 244 | new HashSet() { "S" } 245 | ); 246 | 247 | var testNode2 = Connection.CreateNode( 248 | new Dictionary() { { "Name", "A" } }, 249 | new HashSet() { "S" } 250 | ); 251 | 252 | var properties = new Dictionary(); 253 | var labels = new HashSet(); 254 | properties.Add("Test", "Test"); 255 | labels.Add("Test on a node!"); 256 | Connection.CreateRelation("Test", sn => sn.First(x => x.Hash == testNode1.Hash), tn => tn.First(a => a.Hash == testNode2.Hash), properties, labels); 257 | 258 | var queriedRelation = Connection.Relations("Test").FirstOrDefault(); 259 | 260 | Connection.Delete(queriedRelation); 261 | 262 | var selectedRelation = Connection.QueryRelations(relations=> relations.Where(rel => rel.RelationName == "Test").ToList()).FirstOrDefault(); 263 | Assert.IsTrue(selectedRelation == null, "Relation still exists"); 264 | } 265 | 266 | public void UpdateSchema() 267 | { 268 | var schema = Connection.Schemas.FirstOrDefault(s => s.Label == "Person"); 269 | if (schema == null) 270 | { 271 | schema = Connection.CreateSchema("Person", 272 | new List() { new() { FullTypeName = "System.String", Name = "Name" } }); 273 | } 274 | schema.Properties.Add(new() { FullTypeName = "System.Double", Name = "Age" }); 275 | Connection.UpdateSchema(schema); 276 | } 277 | 278 | 279 | private void Setup() 280 | { 281 | string filePath = Path.Combine(TestContext.CurrentContext.TestDirectory, fileName); 282 | Connection = new DatabaseConnection(filePath); 283 | } 284 | 285 | 286 | [OneTimeTearDown] 287 | public void TearDown() 288 | { 289 | Connection.ClearDatabase(); 290 | } 291 | } 292 | 293 | public class Train 294 | { 295 | public string SerialNumber { get; set; } 296 | } 297 | 298 | public class Station 299 | { 300 | public string City { get; set; } 301 | } 302 | public class RidesTo 303 | { 304 | public int Platform { get; set; } 305 | } 306 | } -------------------------------------------------------------------------------- /SliccDB.Tests/SliccDB.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /SliccDB.Tests/data/sample.sliccdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmikstacki/SliccDB/f5c1e7c18b4f3ed8fa6a12bc167cec89c46d7540/SliccDB.Tests/data/sample.sliccdb -------------------------------------------------------------------------------- /SliccDB.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SliccDB", "SliccDB\SliccDB.csproj", "{204CCFA4-A64A-4405-B40A-A82F94B9E2FF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SliccDB.Tests", "SliccDB.Tests\SliccDB.Tests.csproj", "{48709885-6B68-4553-8B39-87E191A7E34C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|x64.ActiveCfg = Debug|Any CPU 23 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|x64.Build.0 = Debug|Any CPU 24 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|x86.ActiveCfg = Debug|Any CPU 25 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Debug|x86.Build.0 = Debug|Any CPU 26 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|x64.ActiveCfg = Release|Any CPU 29 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|x64.Build.0 = Release|Any CPU 30 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|x86.ActiveCfg = Release|Any CPU 31 | {204CCFA4-A64A-4405-B40A-A82F94B9E2FF}.Release|x86.Build.0 = Release|Any CPU 32 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|x64.ActiveCfg = Debug|Any CPU 35 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|x64.Build.0 = Debug|Any CPU 36 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|x86.ActiveCfg = Debug|Any CPU 37 | {48709885-6B68-4553-8B39-87E191A7E34C}.Debug|x86.Build.0 = Debug|Any CPU 38 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|x64.ActiveCfg = Release|Any CPU 41 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|x64.Build.0 = Release|Any CPU 42 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|x86.ActiveCfg = Release|Any CPU 43 | {48709885-6B68-4553-8B39-87E191A7E34C}.Release|x86.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {76D275D1-E6B0-4A6C-9E64-B6E9DE9273D5} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /SliccDB/Core/ConnectionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace SliccDB.Core 2 | { 3 | public enum ConnectionStatus 4 | { 5 | Connected, 6 | NotConnected 7 | } 8 | } -------------------------------------------------------------------------------- /SliccDB/Core/GraphEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MessagePack; 3 | 4 | namespace SliccDB.Core 5 | { 6 | [MessagePack.Union(0, typeof(Node))] 7 | [MessagePack.Union(1, typeof(Relation))] 8 | public abstract class GraphEntity 9 | { 10 | /// 11 | /// Properties contained in an entity 12 | /// 13 | [Key(0)] 14 | public Dictionary Properties { get; set; } = new Dictionary(); 15 | /// 16 | /// Labels contained in an entity 17 | /// 18 | [Key(1)] 19 | 20 | public HashSet Labels { get; set; } = new HashSet(); 21 | 22 | [Key(2)] 23 | public virtual string Hash { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /SliccDB/Core/Node.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Text; 5 | using MessagePack; 6 | 7 | namespace SliccDB.Core 8 | { 9 | /// 10 | /// Represents a node in a graph 11 | /// 12 | [MessagePackObject] 13 | public class Node : GraphEntity 14 | { 15 | public Node() 16 | { 17 | Hash = Guid.NewGuid().ToString(); 18 | } 19 | 20 | public Node(Dictionary properties, HashSet labels) 21 | { 22 | Hash = Guid.NewGuid().ToString(); 23 | Properties = properties; 24 | Labels = labels; 25 | } 26 | 27 | public Node(string hash, Dictionary properties, HashSet labels) 28 | { 29 | Hash = hash; 30 | Properties = properties; 31 | Labels = labels; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SliccDB/Core/QueryResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SliccDB.Core 4 | { 5 | public class QueryResult 6 | { 7 | public HashSet Nodes { get; set; } = new HashSet(); 8 | public HashSet Relations { get; set; } = new HashSet(); 9 | } 10 | } -------------------------------------------------------------------------------- /SliccDB/Core/Relation.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SliccDB.Core 6 | { 7 | /// 8 | /// Edge of the graph 9 | /// 10 | [MessagePackObject] 11 | public class Relation : GraphEntity, IComparable, IEquatable 12 | { 13 | [Key(3)] 14 | public virtual string RelationName { get; set; } 15 | [Key(4)] 16 | public virtual string SourceHash { get; set; } 17 | [Key(5)] 18 | public virtual string TargetHash { get; set; } 19 | 20 | 21 | public Relation() 22 | { 23 | Hash = Guid.NewGuid().ToString(); 24 | } 25 | 26 | public Relation(string relationName, Dictionary properties, HashSet labels, string sourceHash, string targetHash) 27 | { 28 | Hash = Guid.NewGuid().ToString(); 29 | RelationName = relationName; 30 | Properties = properties; 31 | Labels = labels; 32 | SourceHash = sourceHash; 33 | TargetHash = targetHash; 34 | } 35 | 36 | public int CompareTo(Relation other) 37 | { 38 | if (ReferenceEquals(this, other)) return 0; 39 | if (ReferenceEquals(null, other)) return 1; 40 | var relationNameComparison = string.Compare(RelationName, other.RelationName, StringComparison.Ordinal); 41 | if (relationNameComparison != 0) return relationNameComparison; 42 | var sourceHashComparison = string.Compare(SourceHash, other.SourceHash, StringComparison.Ordinal); 43 | if (sourceHashComparison != 0) return sourceHashComparison; 44 | return string.Compare(TargetHash, other.TargetHash, StringComparison.Ordinal); 45 | } 46 | 47 | public bool Equals(Relation other) 48 | { 49 | if (ReferenceEquals(null, other)) return false; 50 | if (ReferenceEquals(this, other)) return true; 51 | return RelationName == other.RelationName && Equals(Properties, other.Properties) && Equals(Labels, other.Labels) && SourceHash == other.SourceHash && TargetHash == other.TargetHash; 52 | } 53 | 54 | public override bool Equals(object obj) 55 | { 56 | if (ReferenceEquals(null, obj)) return false; 57 | if (ReferenceEquals(this, obj)) return true; 58 | if (obj.GetType() != this.GetType()) return false; 59 | return Equals((Relation) obj); 60 | } 61 | 62 | } 63 | } -------------------------------------------------------------------------------- /SliccDB/Core/Schema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MessagePack; 3 | using System.Collections.Generic; 4 | 5 | namespace SliccDB.Core; 6 | /// 7 | /// Represents schema with needed properties 8 | /// 9 | [MessagePackObject] 10 | public class Schema 11 | { 12 | 13 | [Key(0)] 14 | public virtual string Label { get; set; } 15 | 16 | [Key(2)] 17 | public List Properties { get; set; } = new List(); 18 | } 19 | 20 | [MessagePackObject] 21 | public class Property 22 | { 23 | 24 | [Key(0)] 25 | public string Name { get; set; } 26 | 27 | [Key(2)] 28 | public string FullTypeName { get; set; } 29 | 30 | public Property() 31 | { 32 | 33 | } 34 | 35 | public Property(string name, Type type) 36 | { 37 | Name = name; 38 | FullTypeName = type.FullName; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /SliccDB/Database.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using MessagePack; 5 | using SliccDB.Core; 6 | using MessagePack; 7 | 8 | namespace SliccDB 9 | { 10 | /// 11 | /// A graph Database 12 | /// 13 | [MessagePackObject] 14 | public class Database : IDisposable 15 | { 16 | [Key(0)] 17 | public HashSet Nodes { get; set; } = new HashSet(); 18 | [Key(1)] 19 | public HashSet Relations { get; set; }= new HashSet(); 20 | [Key(2)] 21 | public List Schemas { get; set; } = new List(); 22 | public void Dispose() 23 | { 24 | Dispose(true); 25 | GC.SuppressFinalize(this); 26 | } 27 | 28 | protected virtual void Dispose(bool disposing) 29 | { 30 | if (disposing) 31 | { 32 | // free managed resources 33 | Nodes.Clear(); 34 | Relations.Clear(); 35 | Schemas.Clear(); 36 | } 37 | // free native resources if there are any. 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SliccDB/Exceptions/RelationExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SliccDB.Exceptions 4 | { 5 | public class RelationExistsException : SliccDbException 6 | { 7 | public RelationExistsException(string targetHash, string sourceHash) : base() 8 | { 9 | Message = $"Relation with Target Hash {targetHash} and Source Hash {sourceHash} already exists!"; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /SliccDB/Exceptions/SchemaExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SliccDB.Core; 3 | 4 | namespace SliccDB.Exceptions; 5 | 6 | public class SchemaExistsException : SliccDbException 7 | { 8 | public override string HelpLink { get; set; } 9 | 10 | public SchemaExistsException(string schemaName) : base() 11 | { 12 | Message = $"Schema for label {schemaName} already exists!"; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /SliccDB/Exceptions/SchemaValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SliccDB.Exceptions; 6 | 7 | public class SchemaValidationException : SliccDbException 8 | { 9 | public SchemaValidationException(string label, Dictionary properties) 10 | { 11 | StringBuilder sb = new StringBuilder(); 12 | sb.AppendLine($"Schema validation for {label} failed. Problematic fields: "); 13 | foreach (var property in properties) 14 | { 15 | sb.AppendLine($"Property: {property.Key}, Reason: {property.Value}"); 16 | } 17 | 18 | Message = sb.ToString(); 19 | } 20 | } -------------------------------------------------------------------------------- /SliccDB/Exceptions/SliccDbException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SliccDB.Exceptions; 4 | 5 | public class SliccDbException : Exception 6 | { 7 | public string Message { get; protected set; } 8 | 9 | } -------------------------------------------------------------------------------- /SliccDB/Exceptions/TargetOrSourceNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SliccDB.Exceptions 4 | { 5 | public class TargetOrSourceNotFoundException : SliccDbException 6 | { 7 | public TargetOrSourceNotFoundException() 8 | { 9 | Message = " or node with provided hashes doesn't exist"; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /SliccDB/Fluent/DatabaseConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using SliccDB.Core; 5 | using SliccDB.Serialization; 6 | 7 | namespace SliccDB.Fluent; 8 | 9 | public static class DatabaseConnectionExtensions 10 | { 11 | public static HashSet Nodes(this DatabaseConnection db) 12 | { 13 | return db.Nodes; 14 | } 15 | /// 16 | /// Returns all nodes in given connection that match the structure of 17 | /// 18 | /// Type used to match against properties in the 19 | /// Database connection which will be queried 20 | /// A dictionary with ids and the nodes of type themselves 21 | public static Dictionary Nodes(this DatabaseConnection db) 22 | { 23 | if (!db.Schemas.Exists(s => s.Label == typeof(T).Name)) 24 | return new Dictionary(); 25 | 26 | var filledObjects = new Dictionary(); 27 | foreach (var node in db.Nodes().Where(x => x.Labels.Contains(typeof(T).Name))) 28 | { 29 | var newTypeInstance = (T) Activator.CreateInstance(typeof(T)); 30 | 31 | db.FillObjectFromProperties(node, newTypeInstance); 32 | filledObjects.Add(node.Hash, newTypeInstance); 33 | } 34 | 35 | return filledObjects; 36 | } 37 | /// 38 | /// Returns all relations in given connection that match the structure of 39 | /// 40 | /// Type used to match against properties in the 41 | /// Database connection which will be queried 42 | /// A dictionary with ids and the relations of type themselves 43 | public static Dictionary Relations(this DatabaseConnection db) 44 | { 45 | if (!db.Schemas.Exists(s => s.Label == typeof(T).Name)) 46 | return new Dictionary(); 47 | 48 | var filledObjects = new Dictionary(); 49 | foreach (var node in db.Relations().Where(x => x.RelationName == typeof(T).Name)) 50 | { 51 | var newTypeInstance = (T) Activator.CreateInstance(typeof(T)); 52 | 53 | db.FillObjectFromProperties(node, newTypeInstance); 54 | filledObjects.Add(node.Hash, (T)newTypeInstance); 55 | } 56 | 57 | return filledObjects; 58 | } 59 | /// 60 | /// Returns all entities in given connection that match the structure of 61 | /// 62 | /// Type used to match against properties in the 63 | /// Database connection which will be queried 64 | /// A dictionary with ids and the entities of type themselves 65 | public static Dictionary Entities(this DatabaseConnection db) 66 | { 67 | if (!db.Schemas.Exists(s => s.Label == typeof(T).Name)) 68 | return new Dictionary(); 69 | 70 | var filledObjects = new Dictionary(); 71 | foreach (var node in db.Entities().Where(x => x.Labels.Contains(typeof(T).Name))) 72 | { 73 | var newTypeInstance = (T) Activator.CreateInstance(typeof(T)); 74 | 75 | db.FillObjectFromProperties(node, newTypeInstance); 76 | filledObjects.Add(node.Hash, (T)newTypeInstance); 77 | } 78 | 79 | return filledObjects; 80 | } 81 | 82 | /// 83 | /// Returns all relations in the database 84 | /// 85 | /// Database Connection 86 | /// All relations in the database 87 | public static HashSet Relations(this DatabaseConnection db) 88 | { 89 | var edges = new HashSet(); 90 | foreach (var edge in db.Relations) 91 | edges.Add(edge); 92 | return edges; 93 | } 94 | /// 95 | /// Returns all relations in the database with 96 | /// 97 | /// 98 | /// 99 | /// All Relations with 100 | public static HashSet Relations(this DatabaseConnection db, string name) 101 | { 102 | var edges = new HashSet(); 103 | foreach (var edge in db.Relations.Where(x => x.RelationName == name)) 104 | edges.Add(edge); 105 | return edges; 106 | } 107 | /// 108 | /// Returns all entities in connection 109 | /// 110 | /// connection 111 | /// All Entities 112 | public static HashSet Entities(this DatabaseConnection db) 113 | { 114 | var entities = new HashSet(); 115 | foreach (var entity in db.Nodes()) 116 | entities.Add(entity); 117 | foreach (var entity in db.Relations()) 118 | entities.Add(entity); 119 | return entities; 120 | } 121 | 122 | /// 123 | /// Replaces the node in the database 124 | /// 125 | /// database 126 | /// entity to replace 127 | /// throws if node with given hash was not found 128 | public static void Replace(this HashSet db, Node entity) 129 | { 130 | var numRemoved = db.RemoveWhere(x => x.Hash == entity.Hash); 131 | if (numRemoved == 1) 132 | db.Add(entity); 133 | else 134 | { 135 | throw new InvalidOperationException("Attempted to replace a node that does not exist"); 136 | } 137 | } 138 | /// 139 | /// Replaces the relation in the database 140 | /// 141 | /// database 142 | /// entity to replace 143 | /// throws if relation with given hash was not found 144 | public static void Replace(this HashSet db, Relation entity) 145 | { 146 | var numRemoved = db.RemoveWhere(x => x.Hash == entity.Hash); 147 | if (numRemoved == 1) 148 | db.Add(entity); 149 | else 150 | { 151 | throw new InvalidOperationException("Attempted to replace a relation that does not exist"); 152 | } 153 | } 154 | /// 155 | /// Founds an entity with all labels 156 | /// 157 | /// all entities to perform query onto 158 | /// all labels to query by 159 | /// 160 | public static HashSet Labels(this HashSet allEntities, params string[] labels) 161 | { 162 | var entities = new HashSet(); 163 | 164 | foreach (var entity in allEntities) 165 | { 166 | var commonLabels = entity.Labels.Count(x => labels.Contains(x)); 167 | if (commonLabels == entity.Labels.Count) 168 | entities.Add(entity); 169 | } 170 | 171 | return entities; 172 | } 173 | 174 | /// 175 | /// Finds entity with properties and their values 176 | /// 177 | /// all entities to perform query onto 178 | /// all properties to query by 179 | /// 180 | public static HashSet Properties(this HashSet allEntities, params KeyValuePair[] properties) 181 | { 182 | var entities = new HashSet(); 183 | foreach(var entity in allEntities) 184 | { 185 | foreach (var keyValuePair in properties) 186 | { 187 | if (!entity.Properties.ContainsKey(keyValuePair.Key)) return null; 188 | 189 | if (entity.Properties[keyValuePair.Key] == keyValuePair.Value) 190 | { 191 | entities.Add(entity); 192 | } 193 | } 194 | }; 195 | return entities; 196 | } 197 | 198 | /// 199 | /// Finds entity with properties and their values 200 | /// 201 | /// all entities to perform query onto 202 | /// all properties to query by 203 | /// 204 | public static HashSet Labels(this HashSet allEntities, params string[] labels) where T : GraphEntity 205 | { 206 | var entities = new HashSet(); 207 | 208 | foreach (var entity in allEntities) 209 | { 210 | 211 | var commonLabels = entity.Labels.Count(x => labels.Contains(x)); 212 | if (commonLabels == labels.Length) 213 | entities.Add(entity); 214 | 215 | } 216 | 217 | return entities; 218 | } 219 | 220 | /// 221 | /// Finds entities with properties 222 | /// 223 | /// 224 | /// all entities in database 225 | /// properties' names and values to match against 226 | /// 227 | public static HashSet Properties(this HashSet allEntities, 228 | params KeyValuePair[] properties) where T : GraphEntity 229 | { 230 | var entities = new HashSet(); 231 | foreach (var entity in allEntities) 232 | { 233 | foreach (var keyValuePair in properties) 234 | { 235 | if (entity.Properties.ContainsKey(keyValuePair.Key) 236 | && entity.Properties[keyValuePair.Key] == keyValuePair.Value) 237 | { 238 | entities.Add(entity); 239 | } 240 | } 241 | } 242 | 243 | return entities; 244 | } 245 | /// 246 | /// Returns all nodes that have outgoing relation ([N]->) with given name 247 | /// 248 | /// database connection 249 | /// name of the relation that should be outgoing 250 | /// 251 | public static HashSet Out(this DatabaseConnection connection, string relationName) 252 | { 253 | var allRelationsWithName = connection.Relations(relationName); 254 | var entities = connection.Nodes().Where(n => allRelationsWithName.ToList().Exists(r => r.SourceHash == n.Hash)); 255 | return new HashSet(entities); 256 | } 257 | 258 | /// 259 | /// Returns all nodes that have incoming relation (->[N]) with given name 260 | /// 261 | /// database connection 262 | /// name of the relation that should be incoming 263 | /// 264 | public static HashSet In(this DatabaseConnection connection, string relationName) 265 | { 266 | var allRelationsWithName = connection.Relations(relationName); 267 | var entities = connection.Nodes().Where(n => allRelationsWithName.ToList().Exists(r => r.TargetHash == n.Hash)); 268 | return new HashSet(entities); 269 | } 270 | 271 | 272 | public static KeyValuePair Value(this string key, string value) 273 | { 274 | return new KeyValuePair(key, value); 275 | } 276 | 277 | 278 | } -------------------------------------------------------------------------------- /SliccDB/SLICC_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmikstacki/SliccDB/f5c1e7c18b4f3ed8fa6a12bc167cec89c46d7540/SliccDB/SLICC_128.png -------------------------------------------------------------------------------- /SliccDB/Serialization/DatabaseConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Security.Cryptography.X509Certificates; 8 | using deniszykov.TypeConversion; 9 | using MessagePack; 10 | using SliccDB.Core; 11 | using SliccDB.Exceptions; 12 | using SliccDB.Fluent; 13 | 14 | namespace SliccDB.Serialization 15 | { 16 | 17 | public class DatabaseConnection 18 | { 19 | public string FilePath { get; private set; } 20 | 21 | internal Database Database { get; private set; } 22 | 23 | public HashSet Nodes => Database?.Nodes; 24 | public HashSet Relations => Database?.Relations; 25 | public List Schemas => Database?.Schemas; 26 | 27 | public ConnectionStatus ConnectionStatus { get; set; } 28 | private readonly bool realtime; 29 | 30 | private TypeConversionProvider typeConversionProvider; 31 | 32 | /// 33 | /// Creates new Database Connection Instance 34 | /// 35 | /// Path to a database file 36 | /// if true, attempts to save the database on every operation. Disabled by default as it is memory intensive 37 | public DatabaseConnection(string filePath, bool realtime = false) 38 | { 39 | FilePath = filePath; 40 | if (File.Exists(filePath)) 41 | { 42 | var bytes = File.ReadAllBytes(filePath); 43 | try 44 | { 45 | Database = MessagePackSerializer.Deserialize(bytes); 46 | this.ConnectionStatus = ConnectionStatus.Connected; 47 | } 48 | catch (Exception exception) 49 | { 50 | Database = new Database(); 51 | this.ConnectionStatus = ConnectionStatus.Connected; 52 | } 53 | } 54 | else 55 | { 56 | Database = new Database(); 57 | this.ConnectionStatus = ConnectionStatus.Connected; 58 | var bytes = MessagePackSerializer.Serialize(Database); 59 | File.WriteAllBytes(filePath, bytes); 60 | } 61 | this.realtime = realtime; 62 | 63 | //init type conversion provider 64 | typeConversionProvider = new TypeConversionProvider(); 65 | typeConversionProvider.RegisterConversion((s, s1, arg3) => s, ConversionQuality.Constructor); 66 | } 67 | /// 68 | /// Queries nodes based on generic delegate type 69 | /// 70 | /// generic delegate type 71 | /// Queried Nodes 72 | public IEnumerable QueryNodes(Func, IEnumerable> query) 73 | { 74 | return query.Invoke(Nodes); 75 | } 76 | /// 77 | /// Queries relations based on generic delegate type 78 | /// 79 | /// generic delegate type 80 | /// Queried Relations 81 | public IEnumerable QueryRelations(Func, IEnumerable> query) 82 | { 83 | return query.Invoke(Relations); 84 | } 85 | 86 | /// 87 | /// Updates Node or Relation 88 | /// 89 | /// 90 | /// Throws when wrong type of GraphEntity is supplied 91 | public void Update(GraphEntity entity) 92 | { 93 | ValidateSchema(entity); 94 | if (entity is Node node) 95 | { 96 | Nodes.Replace(node); 97 | } 98 | else if (entity is Relation relation) 99 | { 100 | Relations.Replace(relation); 101 | } 102 | else 103 | { 104 | throw new InvalidOperationException("Invalid entity type"); 105 | } 106 | SaveDatabase(); 107 | } 108 | 109 | /// 110 | /// Enforces a schema based on amount of schemas 111 | /// 112 | /// entity to check schema enforcement on 113 | /// Throws when schema validation for given label fails 114 | private void ValidateSchema(GraphEntity entity) 115 | { 116 | var schema = Schemas.FirstOrDefault(s => entity.Labels.Contains(s.Label)); 117 | 118 | if (schema == null) 119 | return; 120 | 121 | Dictionary reasons = new Dictionary(); 122 | bool result = true; 123 | foreach (var schemaProperty in schema.Properties) 124 | { 125 | if (!entity.Properties.TryGetValue(schemaProperty.Name, out var foundProperty)) 126 | { 127 | reasons.Add(schemaProperty.Name, "Not Found"); 128 | result = false; 129 | } 130 | 131 | if (foundProperty != null && !typeConversionProvider.TryConvert(typeof(string), Type.GetType(schemaProperty.FullTypeName), 132 | foundProperty, out var convertResult)) 133 | { 134 | reasons.Add(schemaProperty.Name, 135 | "Converter not registered, try registering one with RegisterConversion"); 136 | result = false; 137 | } 138 | } 139 | 140 | if (!result) 141 | { 142 | throw new SchemaValidationException(schema.Label, reasons); 143 | } 144 | } 145 | 146 | /// 147 | /// Bulk updates Node or Relation 148 | /// 149 | /// collection of entities to update 150 | /// Throws when wrong type of GraphEntity is supplied 151 | public void BulkUpdate(List entities) 152 | { 153 | foreach (var entity in entities) 154 | { 155 | if (entity is Node node) 156 | { 157 | Nodes.Replace(node); 158 | } 159 | else if (entity is Relation relation) 160 | { 161 | Relations.Replace(relation); 162 | } 163 | else 164 | { 165 | throw new InvalidOperationException("Invalid entity type"); 166 | } 167 | } 168 | SaveDatabase(); 169 | } 170 | /// 171 | /// Deletes any entity passed and related nodes 172 | /// 173 | /// entity to delete 174 | /// all deleted entities 175 | public int Delete(GraphEntity entity) 176 | { 177 | var nodes = Nodes.RemoveWhere(n => n.Hash == entity.Hash); 178 | var relationsLinkedWithNodes = Relations.RemoveWhere(r => r.SourceHash == entity.Hash || r.TargetHash == entity.Hash); 179 | var relations = Relations.RemoveWhere(r => r.Hash == entity.Hash); 180 | 181 | if (realtime) SaveDatabase(); 182 | 183 | return nodes + relationsLinkedWithNodes + relations; 184 | } 185 | /// 186 | /// Find nodes with relation with name as it's source (node->relation) 187 | /// 188 | /// name of the relation 189 | /// all nodes with relation as their source 190 | public IEnumerable FindNodesWithRelationSource(string relationName) 191 | { 192 | var relationsSources = Relations.AsParallel().Where(x => x.RelationName == relationName).Select(x => x.SourceHash); 193 | return Nodes.AsParallel().Where(x => relationsSources.Contains(x.Hash)); 194 | } 195 | /// 196 | /// Find nodes with relation with name as it's target (relation->node) 197 | /// 198 | /// name of the relation 199 | /// all nodes with relation as their target 200 | public IEnumerable FindNodesWithRelationTarget(string relationName) 201 | { 202 | var relationsSources = Relations.AsParallel().Where(x => x.RelationName == relationName).Select(x => x.TargetHash); 203 | return Nodes.AsParallel().Where(x => relationsSources.Contains(x.Hash)); 204 | } 205 | /// 206 | /// Saves a database 207 | /// 208 | public void SaveDatabase() 209 | { 210 | if (File.Exists(FilePath)) 211 | { 212 | var bytes = MessagePackSerializer.Serialize(Database); 213 | File.WriteAllBytes(FilePath, bytes); 214 | } 215 | } 216 | /// 217 | /// Closes database 218 | /// 219 | public void CloseDatabase() 220 | { 221 | if (Database != null && ConnectionStatus == ConnectionStatus.Connected) 222 | { 223 | this.ConnectionStatus = ConnectionStatus.NotConnected; 224 | Database.Dispose(); 225 | } 226 | } 227 | /// 228 | /// Clears database 229 | /// 230 | public void ClearDatabase() 231 | { 232 | Nodes.Clear(); 233 | Relations.Clear(); 234 | Schemas.Clear(); 235 | SaveDatabase(); 236 | } 237 | /// 238 | /// Creates a new node with given properties and labels 239 | /// 240 | /// properties 241 | /// labels 242 | /// 243 | public Node CreateNode(Dictionary properties = null, HashSet labels = null) 244 | { 245 | Dictionary props = new Dictionary(); 246 | HashSet lablSet = new HashSet(); 247 | var node = new Node(properties ?? props, labels ?? lablSet); 248 | ValidateSchema(node); 249 | Database.Nodes.Add(node); 250 | if(realtime) SaveDatabase(); 251 | return node; 252 | } 253 | /// 254 | /// Creates a new schema 255 | /// 256 | /// 257 | /// 258 | /// 259 | /// 260 | public Schema CreateSchema(string label, List properties) 261 | { 262 | if (Schemas.Exists(x => x.Label == label)) 263 | { 264 | throw new SchemaExistsException(label); 265 | } 266 | 267 | Schema schema = new Core.Schema(); 268 | schema.Label = label; 269 | schema.Properties = properties; 270 | Schemas.Add(schema); 271 | UpdateEntitiesForSchema(schema); 272 | if (realtime) SaveDatabase(); 273 | return schema; 274 | } 275 | /// 276 | /// Creates a new schema 277 | /// 278 | /// Schema to create 279 | /// Schema added to database 280 | /// Throws if schema exists 281 | public Schema CreateSchema(Schema schema) 282 | { 283 | if (Schemas.Exists(x => x.Label == schema.Label)) 284 | { 285 | throw new SchemaExistsException(schema.Label); 286 | } 287 | Schemas.Add(schema); 288 | UpdateEntitiesForSchema(schema); 289 | if (realtime) SaveDatabase(); 290 | return schema; 291 | } 292 | /// 293 | /// Deletes Schema while retaining entity values 294 | /// 295 | /// schema to delete 296 | public void DeleteSchema(Schema schema) 297 | { 298 | Schemas.Remove(schema); 299 | } 300 | 301 | /// 302 | /// Deletes schema using label 303 | /// 304 | /// label for the schema 305 | /// throws if schema with given label was not found 306 | public void DeleteSchemaByLabel(string schemaLabel) 307 | { 308 | var schema = Schemas.FirstOrDefault(s => s.Label == schemaLabel); 309 | if (schema is null) 310 | throw new InvalidOperationException($"Schema with label {schemaLabel} does not exist!"); 311 | Schemas.Remove(schema); 312 | 313 | } 314 | 315 | /// 316 | /// Updates Schema that already exists 317 | /// 318 | /// Schema to update 319 | /// Updated schema 320 | /// Throws if schema for this label doesn't exist 321 | public Schema UpdateSchema(Schema schema) 322 | { 323 | var foundSchema = Schemas.FirstOrDefault(x => x.Label == schema.Label); 324 | if (foundSchema == null) 325 | throw new InvalidOperationException("Schema was not found. Use CreateSchema instead"); 326 | 327 | Schemas.Remove(foundSchema); 328 | 329 | Schemas.Add(schema); 330 | UpdateEntitiesForSchema(schema); 331 | if (realtime) SaveDatabase(); 332 | return schema; 333 | } 334 | /// 335 | /// Creates relation entity 336 | /// 337 | /// relation name 338 | /// source node search func 339 | /// target node search func 340 | /// properties in relation 341 | /// labels in relation 342 | /// Throws if relation already exists 343 | public void CreateRelation(string relationName, Func, Node> sourceNode, Func, Node> targetNode, Dictionary properties = null, HashSet labels = null) 344 | { 345 | var sourceNodeObject = sourceNode.Invoke(Database.Nodes); 346 | var targetNodeObject = targetNode.Invoke(Database.Nodes); 347 | if (sourceNodeObject is null || targetNodeObject is null) 348 | { 349 | 350 | } 351 | bool exists = Relations.AsParallel().ToList().Exists(x => 352 | x.TargetHash == targetNodeObject.Hash && x.SourceHash == sourceNodeObject.Hash); 353 | if (exists) 354 | throw new RelationExistsException(targetNodeObject.Hash, targetNodeObject.Hash); 355 | Dictionary props = new Dictionary(); 356 | HashSet lablSet = new HashSet(); 357 | var relation = new Relation(relationName, properties ?? props, labels ?? lablSet, sourceNodeObject.Hash, 358 | targetNodeObject.Hash); 359 | ValidateSchema(relation); 360 | Database.Relations.Add(relation); 361 | if (realtime) SaveDatabase(); 362 | 363 | } 364 | 365 | /// 366 | /// Registers custom converter using TypeConversion library. More Info /> 367 | /// 368 | /// Type from 369 | /// Type to 370 | /// conversion function 371 | /// quality of the conversion. Click more info in summary 372 | public void RegisterConversion( 373 | Func conversionFunc, 374 | ConversionQuality quality) 375 | { 376 | typeConversionProvider.RegisterConversion(conversionFunc, quality); 377 | 378 | } 379 | /// 380 | /// Updates entities by inserting properties enforced by a given schema 381 | /// 382 | /// Schema to enforce 383 | private void UpdateEntitiesForSchema(Schema schema) 384 | { 385 | var nodesForSchema = Nodes.Where(x => x.Labels.Contains(schema.Label)) as IEnumerable; 386 | var relationsForSchema = Relations.Where(x => x.Labels.Contains(schema.Label)) as IEnumerable; 387 | var entities = new List(); 388 | entities.AddRange(nodesForSchema); 389 | entities.AddRange(relationsForSchema); 390 | foreach (var entity in entities) 391 | { 392 | UpdateMissingFieldsForEntity(entity, schema); 393 | } 394 | 395 | BulkUpdate(entities); 396 | } 397 | /// 398 | /// Updates entity by filling in missing fields from schemas 399 | /// 400 | /// entity to update 401 | private void UpdateSchemasForEntity(GraphEntity entity) 402 | { 403 | var SchemasForEntity = Schemas.Where(s => entity.Labels.Contains(s.Label)); 404 | foreach (var schema in SchemasForEntity) 405 | { 406 | UpdateMissingFieldsForEntity(entity, schema); 407 | } 408 | } 409 | /// 410 | /// enforces missing fields for entity given schema 411 | /// 412 | /// entity to update 413 | /// schema to enforce 414 | private void UpdateMissingFieldsForEntity(GraphEntity entity, Schema schema) 415 | { 416 | var missingFields = schema.Properties.Where(prop => !entity.Properties.ContainsKey(prop.Name)); 417 | foreach (var missingField in missingFields) 418 | { 419 | entity.Properties.Add(missingField.Name, ""); 420 | } 421 | } 422 | /// 423 | /// Attempts to fill object from properties 424 | /// 425 | /// Type of the object to fill 426 | /// entity, in which property values are stored 427 | /// object that will be filled with property values 428 | public void FillObjectFromProperties(GraphEntity entity, T objectToFill) 429 | { 430 | var objectProperties = typeof(T).GetProperties(); 431 | foreach (var property in objectProperties) 432 | { 433 | 434 | entity.Properties.TryGetValue(property.Name, out var propertyValue); 435 | if(typeConversionProvider.TryConvert(typeof(string), property.PropertyType, propertyValue, out var convertedValue)) 436 | property.SetValue(objectToFill, convertedValue); 437 | } 438 | } 439 | 440 | /// 441 | /// Attempts to fill object from properties 442 | /// 443 | /// Type of the object to fill 444 | /// entity, in which property values are stored 445 | /// object that will be filled with property values 446 | public Dictionary GetPropertiesFromObject(T objectToExtractValuesFrom) 447 | { 448 | var objectProperties = typeof(T).GetProperties(); 449 | var result = new Dictionary(); 450 | foreach (var property in objectProperties) 451 | { 452 | 453 | result.Add(property.Name, property.GetValue(objectToExtractValuesFrom).ToString()); 454 | } 455 | 456 | return result; 457 | } 458 | /// 459 | /// Creates a Relation 460 | /// 461 | /// Type of the inserted node 462 | /// object properties 463 | public void CreateNode(T value, params string[] additionalLabels) 464 | { 465 | var label = value.GetType().Name; 466 | if (!Schemas.Exists(s => s.Label == label)) 467 | { 468 | CreateSchema(label, 469 | value.GetType().GetProperties().Select(prop => new Property() 470 | { FullTypeName = prop.PropertyType.FullName, Name = prop.Name }).ToList()); 471 | } 472 | else 473 | { 474 | UpdateSchema(new Schema() 475 | { 476 | Label = label, 477 | Properties = value.GetType().GetProperties().Select(prop => new Property() 478 | { FullTypeName = prop.PropertyType.FullName, Name = prop.Name }).ToList() 479 | }); 480 | } 481 | Debug.WriteLine("Node created"); 482 | var labels = new HashSet(additionalLabels); 483 | labels.Add(label); 484 | CreateNode(GetPropertiesFromObject(value), labels); 485 | } 486 | 487 | /// 488 | /// Creates a Relation given an object. 489 | /// 490 | /// Type of the inserted node 491 | /// object from which property values will be extracted 492 | public void CreateRelation(Func, Node> sourceNode, Func, Node> targetNode, T properties, params string[] additionalLabels) 493 | { 494 | var label = properties.GetType().Name; 495 | if (!Schemas.Exists(s => s.Label == label)) 496 | { 497 | CreateSchema(label, 498 | properties.GetType().GetProperties().Select(prop => new Property() 499 | { FullTypeName = prop.PropertyType.FullName, Name = prop.Name }).ToList()); 500 | } 501 | else 502 | { 503 | UpdateSchema(new Schema() 504 | { 505 | Label = label, 506 | Properties = properties.GetType().GetProperties().Select(prop => new Property() 507 | { FullTypeName = prop.PropertyType.FullName, Name = prop.Name }).ToList() 508 | }); 509 | } 510 | 511 | CreateRelation(label, sourceNode, targetNode, GetPropertiesFromObject(properties), new HashSet(additionalLabels)); 512 | } 513 | 514 | public void Update(string hash, T objectValues) 515 | { 516 | var entity = this.Entities().FirstOrDefault(x => x.Hash == hash); 517 | var propertiesWithValues = GetPropertiesFromObject(objectValues); 518 | foreach (var propertyWithValue in propertiesWithValues) 519 | { 520 | if (entity.Properties.ContainsKey(propertyWithValue.Key)) 521 | entity.Properties[propertyWithValue.Key] = propertyWithValue.Value; 522 | } 523 | 524 | Update(entity); 525 | } 526 | 527 | public int Delete(string hash) 528 | { 529 | var nodes = Nodes.RemoveWhere(n => n.Hash == hash); 530 | var relationsLinkedWithNodes = Relations.RemoveWhere(r => r.SourceHash == hash || r.TargetHash == hash); 531 | var relations = Relations.RemoveWhere(r => r.Hash == hash); 532 | 533 | if(realtime) SaveDatabase(); 534 | return nodes + relationsLinkedWithNodes + relations; 535 | } 536 | 537 | } 538 | } -------------------------------------------------------------------------------- /SliccDB/SliccDB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | latest 6 | true 7 | 8 | 9 | 10 | SliccDB 11 | SLICC_128.png 12 | MIT 13 | SliccDB 14 | 0.1.1 15 | A simple, one file graph database! 16 | Piotr Mikstacki 17 | https://github.com/pmikstacki/SliccDB 18 | https://github.com/pmikstacki/SliccDB 19 | https://github.com/pmikstacki/SliccDB 20 | https://raw.githubusercontent.com/pmikstacki/SliccDB/master/README.md 21 | Graph, Database 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Always 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Piotr Mikstacki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /screenshots/Explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmikstacki/SliccDB/f5c1e7c18b4f3ed8fa6a12bc167cec89c46d7540/screenshots/Explorer.png --------------------------------------------------------------------------------