From 1b5a2a75bc5e4b9549cca666768bef38b64be0ac Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 1 Nov 2015 19:54:40 -0200 Subject: [PATCH 01/91] First v2 commit --- LiteDB-TODO.txt | 7 + LiteDB.Shell/Commands/Help.cs | 1 - LiteDB.Shell/Commands/Spool.cs | 4 +- LiteDB.Shell/Properties/AssemblyInfo.cs | 4 +- {UnitTest => LiteDB.Tests}/AutoIdTest.cs | 0 {UnitTest => LiteDB.Tests}/BigFileTest.cs | 100 ++--- {UnitTest => LiteDB.Tests}/BsonFieldTest.cs | 0 {UnitTest => LiteDB.Tests}/BsonTest.cs | 0 {UnitTest => LiteDB.Tests}/ConcurrentTest.cs | 0 .../DropCollectionTest.cs | 0 {UnitTest => LiteDB.Tests}/FileStorageTest.cs | 0 {UnitTest => LiteDB.Tests}/FilesTest.cs | 0 {UnitTest => LiteDB.Tests}/IncludeTest.cs | 0 {UnitTest => LiteDB.Tests}/IndexOrderTest.cs | 0 {UnitTest => LiteDB.Tests}/IndexTest.cs | 0 {UnitTest => LiteDB.Tests}/JsonTest.cs | 0 {UnitTest => LiteDB.Tests}/LinqTest.cs | 0 .../LiteDB.Tests.csproj | 217 ++++++----- .../MapperInterfaceTest.cs | 0 {UnitTest => LiteDB.Tests}/MapperTest.cs | 362 +++++++++--------- {UnitTest => LiteDB.Tests}/ObjectIdTest.cs | 0 {UnitTest => LiteDB.Tests}/PerformanceTest.cs | 0 .../Properties/AssemblyInfo.cs | 10 +- {UnitTest => LiteDB.Tests}/Utils/DB.cs | 0 LiteDB.sln | 16 +- .../{Database => Core}/Collections/Delete.cs | 0 LiteDB/{Database => Core}/Collections/Drop.cs | 0 LiteDB/{Database => Core}/Collections/Find.cs | 0 .../Collections/FindIndex.cs | 0 .../{Database => Core}/Collections/Include.cs | 0 .../{Database => Core}/Collections/Index.cs | 0 .../{Database => Core}/Collections/Insert.cs | 0 .../Collections/InsertBulk.cs | 2 +- .../Collections/LiteCollection.cs | 0 .../{Database => Core}/Collections/Update.cs | 0 LiteDB/{Database => Core}/LiteDatabase.cs | 59 ++- LiteDB/DataStructure/Disks/FileDiskService.cs | 156 ++++++++ LiteDB/DataStructure/Disks/IDiskService.cs | 19 + .../Pages/BasePage.cs | 6 +- .../Pages/CollectionPage.cs | 4 + .../Pages/DataPage.cs | 36 +- .../Pages/ExtendPage.cs | 4 + .../Pages/HeaderPage.cs | 39 +- .../Pages/IndexPage.cs | 4 + .../Services/CacheService.cs | 71 +--- .../Services/CollectionService.cs | 16 +- .../Services/DataService.cs | 6 +- .../Services/IndexService.cs | 4 +- .../Services/PageService.cs | 18 +- .../Services/TransactionService.cs | 69 ++-- .../Structures/CollectionIndex.cs | 0 .../Structures/DataBlock.cs | 0 .../Structures/IndexNode.cs | 0 .../Structures/IndexOptions.cs | 0 .../Structures/PageAddress.cs | 0 LiteDB/Database/Tools/GetDatabaseInfo.cs | 37 -- LiteDB/Database/Tools/RunCommand.cs | 26 -- LiteDB/Database/Tools/UserVersion.cs | 54 --- .../FileStorage/LiteFileInfo.cs | 0 .../FileStorage/LiteFileStorage.cs | 5 - .../FileStorage/LiteFileStream.cs | 3 - LiteDB/LiteDB.csproj | 74 ++-- LiteDB/Properties/AssemblyInfo.cs | 4 +- .../Tools => Serializer/Mapper}/DbRef.cs | 0 LiteDB/Shell/Commands/Others/Info.cs | 21 - LiteDB/Storage/Services/DiskService.cs | 250 ------------ LiteDB/Storage/Services/JournalService.cs | 105 ----- LiteDB/Storage/Services/RecoveryService.cs | 123 ------ LiteDB/Utils/ConnectionString.cs | 89 ----- LiteDB/Utils/DumpDatabase.cs | 4 +- UnitTest/VersionTest.cs | 66 ---- 71 files changed, 701 insertions(+), 1394 deletions(-) create mode 100644 LiteDB-TODO.txt rename {UnitTest => LiteDB.Tests}/AutoIdTest.cs (100%) rename {UnitTest => LiteDB.Tests}/BigFileTest.cs (96%) rename {UnitTest => LiteDB.Tests}/BsonFieldTest.cs (100%) rename {UnitTest => LiteDB.Tests}/BsonTest.cs (100%) rename {UnitTest => LiteDB.Tests}/ConcurrentTest.cs (100%) rename {UnitTest => LiteDB.Tests}/DropCollectionTest.cs (100%) rename {UnitTest => LiteDB.Tests}/FileStorageTest.cs (100%) rename {UnitTest => LiteDB.Tests}/FilesTest.cs (100%) rename {UnitTest => LiteDB.Tests}/IncludeTest.cs (100%) rename {UnitTest => LiteDB.Tests}/IndexOrderTest.cs (100%) rename {UnitTest => LiteDB.Tests}/IndexTest.cs (100%) rename {UnitTest => LiteDB.Tests}/JsonTest.cs (100%) rename {UnitTest => LiteDB.Tests}/LinqTest.cs (100%) rename UnitTest/UnitTest.csproj => LiteDB.Tests/LiteDB.Tests.csproj (95%) rename {UnitTest => LiteDB.Tests}/MapperInterfaceTest.cs (100%) rename {UnitTest => LiteDB.Tests}/MapperTest.cs (97%) rename {UnitTest => LiteDB.Tests}/ObjectIdTest.cs (100%) rename {UnitTest => LiteDB.Tests}/PerformanceTest.cs (100%) rename {UnitTest => LiteDB.Tests}/Properties/AssemblyInfo.cs (84%) rename {UnitTest => LiteDB.Tests}/Utils/DB.cs (100%) rename LiteDB/{Database => Core}/Collections/Delete.cs (100%) rename LiteDB/{Database => Core}/Collections/Drop.cs (100%) rename LiteDB/{Database => Core}/Collections/Find.cs (100%) rename LiteDB/{Database => Core}/Collections/FindIndex.cs (100%) rename LiteDB/{Database => Core}/Collections/Include.cs (100%) rename LiteDB/{Database => Core}/Collections/Index.cs (100%) rename LiteDB/{Database => Core}/Collections/Insert.cs (100%) rename LiteDB/{Database => Core}/Collections/InsertBulk.cs (96%) rename LiteDB/{Database => Core}/Collections/LiteCollection.cs (100%) rename LiteDB/{Database => Core}/Collections/Update.cs (100%) rename LiteDB/{Database => Core}/LiteDatabase.cs (80%) create mode 100644 LiteDB/DataStructure/Disks/FileDiskService.cs create mode 100644 LiteDB/DataStructure/Disks/IDiskService.cs rename LiteDB/{Storage => DataStructure}/Pages/BasePage.cs (98%) rename LiteDB/{Storage => DataStructure}/Pages/CollectionPage.cs (98%) rename LiteDB/{Storage => DataStructure}/Pages/DataPage.cs (94%) rename LiteDB/{Storage => DataStructure}/Pages/ExtendPage.cs (96%) rename LiteDB/{Storage => DataStructure}/Pages/HeaderPage.cs (77%) rename LiteDB/{Storage => DataStructure}/Pages/IndexPage.cs (98%) rename LiteDB/{Storage => DataStructure}/Services/CacheService.cs (52%) rename LiteDB/{Storage => DataStructure}/Services/CollectionService.cs (83%) rename LiteDB/{Storage => DataStructure}/Services/DataService.cs (97%) rename LiteDB/{Storage => DataStructure}/Services/IndexService.cs (98%) rename LiteDB/{Storage => DataStructure}/Services/PageService.cs (93%) rename LiteDB/{Storage => DataStructure}/Services/TransactionService.cs (53%) rename LiteDB/{Storage => DataStructure}/Structures/CollectionIndex.cs (100%) rename LiteDB/{Storage => DataStructure}/Structures/DataBlock.cs (100%) rename LiteDB/{Storage => DataStructure}/Structures/IndexNode.cs (100%) rename LiteDB/{Storage => DataStructure}/Structures/IndexOptions.cs (100%) rename LiteDB/{Storage => DataStructure}/Structures/PageAddress.cs (100%) delete mode 100644 LiteDB/Database/Tools/GetDatabaseInfo.cs delete mode 100644 LiteDB/Database/Tools/RunCommand.cs delete mode 100644 LiteDB/Database/Tools/UserVersion.cs rename LiteDB/{Database => }/FileStorage/LiteFileInfo.cs (100%) rename LiteDB/{Database => }/FileStorage/LiteFileStorage.cs (96%) rename LiteDB/{Database => }/FileStorage/LiteFileStream.cs (97%) rename LiteDB/{Database/Tools => Serializer/Mapper}/DbRef.cs (100%) delete mode 100644 LiteDB/Shell/Commands/Others/Info.cs delete mode 100644 LiteDB/Storage/Services/DiskService.cs delete mode 100644 LiteDB/Storage/Services/JournalService.cs delete mode 100644 LiteDB/Storage/Services/RecoveryService.cs delete mode 100644 LiteDB/Utils/ConnectionString.cs delete mode 100644 UnitTest/VersionTest.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt new file mode 100644 index 000000000..58b73a963 --- /dev/null +++ b/LiteDB-TODO.txt @@ -0,0 +1,7 @@ +# LiteDB v.2 - TODO + +- Implementar tamanho do BsonDocument +- Implementar ByteReader/Writer +- Converter page read/write para ByteR/W +- Converter BsonSerializer para Byte R/W +- Converter on-demand a serializacao no insert/update \ No newline at end of file diff --git a/LiteDB.Shell/Commands/Help.cs b/LiteDB.Shell/Commands/Help.cs index 8b1751d1c..cfcdcaa1e 100644 --- a/LiteDB.Shell/Commands/Help.cs +++ b/LiteDB.Shell/Commands/Help.cs @@ -105,7 +105,6 @@ public override void Execute(LiteShell shell, StringScanner s, Display d, InputC d.WriteHelp("Other commands"); d.WriteHelp("=============="); - d.WriteHelp("> db.info", "Get database informations"); d.WriteHelp("> dump", "Display dump database information"); } } diff --git a/LiteDB.Shell/Commands/Spool.cs b/LiteDB.Shell/Commands/Spool.cs index 5ebee2e31..01e354f12 100644 --- a/LiteDB.Shell/Commands/Spool.cs +++ b/LiteDB.Shell/Commands/Spool.cs @@ -30,9 +30,7 @@ public override void Execute(LiteShell shell, StringScanner s, Display display, { if (shell.Database == null) throw LiteException.NoDatabase(); - var dbfilename = shell.Database.ConnectionString.Filename; - var path = Path.Combine(Path.GetDirectoryName(dbfilename), - string.Format("{0}-spool-{1:yyyy-MM-dd-HH-mm}.txt", Path.GetFileNameWithoutExtension(dbfilename), DateTime.Now)); + var path = Path.GetFullPath(string.Format("LiteDB-spool-{1:yyyy-MM-dd-HH-mm}.txt", DateTime.Now)); _writer = File.CreateText(path); diff --git a/LiteDB.Shell/Properties/AssemblyInfo.cs b/LiteDB.Shell/Properties/AssemblyInfo.cs index 564b25051..2d80a384a 100644 --- a/LiteDB.Shell/Properties/AssemblyInfo.cs +++ b/LiteDB.Shell/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/UnitTest/AutoIdTest.cs b/LiteDB.Tests/AutoIdTest.cs similarity index 100% rename from UnitTest/AutoIdTest.cs rename to LiteDB.Tests/AutoIdTest.cs diff --git a/UnitTest/BigFileTest.cs b/LiteDB.Tests/BigFileTest.cs similarity index 96% rename from UnitTest/BigFileTest.cs rename to LiteDB.Tests/BigFileTest.cs index a0dc11040..6a7dd83d4 100644 --- a/UnitTest/BigFileTest.cs +++ b/LiteDB.Tests/BigFileTest.cs @@ -1,50 +1,50 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; - -namespace UnitTest -{ - [TestClass] - public class BigFileTest - { - //[TestMethod] - public void BigFile_Test() - { - var fileSize = 8L * 1024L * 1024L * 1024L; // 5Gb - var filename = "C:/Github/LiteDB/TestResults/test-4gb.db"; // DB.Path(); - - //File.Delete(filename); - - while (GetFileSize(filename) < fileSize) - { - using (var db = new LiteDatabase("journal=false;filename="+filename)) - { - var col = db.GetCollection("col1"); - - col.InsertBulk(GetDocs()); - } - } - } - - private IEnumerable GetDocs() - { - for (var i = 0; i < 200000; i++) - { - var doc = new BsonDocument() - .Add("_id", Guid.NewGuid()) - .Add("content", DB.LoremIpsum(50, 150, 3, 5, 2)); - - yield return doc; - } - } - - private long GetFileSize(string filename) - { - return File.Exists(filename) ? new FileInfo(filename).Length : 0L; - } - } -} +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; + +namespace UnitTest +{ + [TestClass] + public class BigFileTest + { + //[TestMethod] + public void BigFile_Test() + { + var fileSize = 8L * 1024L * 1024L * 1024L; // 5Gb + var filename = "C:/Github/LiteDB/TestResults/test-4gb.db"; // DB.Path(); + + //File.Delete(filename); + + while (GetFileSize(filename) < fileSize) + { + using (var db = new LiteDatabase("journal=false;filename="+filename)) + { + var col = db.GetCollection("col1"); + + col.InsertBulk(GetDocs()); + } + } + } + + private IEnumerable GetDocs() + { + for (var i = 0; i < 200000; i++) + { + var doc = new BsonDocument() + .Add("_id", Guid.NewGuid()) + .Add("content", DB.LoremIpsum(50, 150, 3, 5, 2)); + + yield return doc; + } + } + + private long GetFileSize(string filename) + { + return File.Exists(filename) ? new FileInfo(filename).Length : 0L; + } + } +} diff --git a/UnitTest/BsonFieldTest.cs b/LiteDB.Tests/BsonFieldTest.cs similarity index 100% rename from UnitTest/BsonFieldTest.cs rename to LiteDB.Tests/BsonFieldTest.cs diff --git a/UnitTest/BsonTest.cs b/LiteDB.Tests/BsonTest.cs similarity index 100% rename from UnitTest/BsonTest.cs rename to LiteDB.Tests/BsonTest.cs diff --git a/UnitTest/ConcurrentTest.cs b/LiteDB.Tests/ConcurrentTest.cs similarity index 100% rename from UnitTest/ConcurrentTest.cs rename to LiteDB.Tests/ConcurrentTest.cs diff --git a/UnitTest/DropCollectionTest.cs b/LiteDB.Tests/DropCollectionTest.cs similarity index 100% rename from UnitTest/DropCollectionTest.cs rename to LiteDB.Tests/DropCollectionTest.cs diff --git a/UnitTest/FileStorageTest.cs b/LiteDB.Tests/FileStorageTest.cs similarity index 100% rename from UnitTest/FileStorageTest.cs rename to LiteDB.Tests/FileStorageTest.cs diff --git a/UnitTest/FilesTest.cs b/LiteDB.Tests/FilesTest.cs similarity index 100% rename from UnitTest/FilesTest.cs rename to LiteDB.Tests/FilesTest.cs diff --git a/UnitTest/IncludeTest.cs b/LiteDB.Tests/IncludeTest.cs similarity index 100% rename from UnitTest/IncludeTest.cs rename to LiteDB.Tests/IncludeTest.cs diff --git a/UnitTest/IndexOrderTest.cs b/LiteDB.Tests/IndexOrderTest.cs similarity index 100% rename from UnitTest/IndexOrderTest.cs rename to LiteDB.Tests/IndexOrderTest.cs diff --git a/UnitTest/IndexTest.cs b/LiteDB.Tests/IndexTest.cs similarity index 100% rename from UnitTest/IndexTest.cs rename to LiteDB.Tests/IndexTest.cs diff --git a/UnitTest/JsonTest.cs b/LiteDB.Tests/JsonTest.cs similarity index 100% rename from UnitTest/JsonTest.cs rename to LiteDB.Tests/JsonTest.cs diff --git a/UnitTest/LinqTest.cs b/LiteDB.Tests/LinqTest.cs similarity index 100% rename from UnitTest/LinqTest.cs rename to LiteDB.Tests/LinqTest.cs diff --git a/UnitTest/UnitTest.csproj b/LiteDB.Tests/LiteDB.Tests.csproj similarity index 95% rename from UnitTest/UnitTest.csproj rename to LiteDB.Tests/LiteDB.Tests.csproj index 34d439aa9..495ff3795 100644 --- a/UnitTest/UnitTest.csproj +++ b/LiteDB.Tests/LiteDB.Tests.csproj @@ -1,110 +1,109 @@ - - - - Debug - AnyCPU - {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE} - Library - Properties - UnitTest - UnitTest - v4.0 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - AnyCPU - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {e808051a-83b7-4fa9-b004-d064ea162b60} - LiteDB - - - - - - - False - - - False - - - False - - - False - - - - - - - + + + + Debug + AnyCPU + {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE} + Library + Properties + LiteDB.Tests + LiteDB.Tests + v4.0 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AnyCPU + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {e808051a-83b7-4fa9-b004-d064ea162b60} + LiteDB + + + + + + + False + + + False + + + False + + + False + + + + + + + \ No newline at end of file diff --git a/UnitTest/MapperInterfaceTest.cs b/LiteDB.Tests/MapperInterfaceTest.cs similarity index 100% rename from UnitTest/MapperInterfaceTest.cs rename to LiteDB.Tests/MapperInterfaceTest.cs diff --git a/UnitTest/MapperTest.cs b/LiteDB.Tests/MapperTest.cs similarity index 97% rename from UnitTest/MapperTest.cs rename to LiteDB.Tests/MapperTest.cs index a8ddf945b..d64dd0246 100644 --- a/UnitTest/MapperTest.cs +++ b/LiteDB.Tests/MapperTest.cs @@ -1,181 +1,181 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -using System.Collections.Specialized; -using System.Security; - -namespace UnitTest -{ - public enum MyEnum { First, Second } - - public class MyClass - { - [BsonId(false)] - public int MyId { get; set; } - [BsonField("MY-STRING")] - public string MyString { get; set; } - public Guid MyGuid { get; set; } - public DateTime MyDateTime { get; set; } - public DateTime? MyDateTimeNullable { get; set; } - public int? MyIntNullable { get; set; } - public MyEnum MyEnumProp { get; set; } - public char MyChar { get; set; } - public byte MyByte { get; set; } - - [BsonIndex(ignoreCase: true)] - public decimal MyDecimal { get; set; } - public decimal? MyDecimalNullable { get; set; } - - [BsonIndex(true)] - public Uri MyUri { get; set; } - - // serialize this properties - [BsonField] - internal string MyProperty { get; set; } - - // do not serialize this properties - [BsonIgnore] - public string MyIgnore { get; set; } - public string MyReadOnly { get; private set; } - public string MyWriteOnly { set; private get; } - public string MyField = "DoNotSerializeThis"; - internal string MyInternalProperty { get; set; } - - // special types - public NameValueCollection MyNameValueCollection { get; set; } - - // lists - public string[] MyStringArray { get; set; } - public List MyStringList { get; set; } - public Dictionary MyDict { get; set; } - - // interfaces - public IMyInterface MyInterface { get; set; } - public List MyListInterface { get; set; } - public IList MyIListInterface { get; set; } - - // objects - public object MyObjectString { get; set; } - public object MyObjectInt { get; set; } - public object MyObjectImpl { get; set; } - public List MyObjectList { get; set; } - } - - - - public interface IMyInterface - { - string Name { get; set; } - } - - public class MyImpl : IMyInterface - { - public string Name { get; set; } - } - - [TestClass] - public class MapperTest - { - private MyClass CreateModel() - { - var c = new MyClass - { - MyId = 123, - MyString = "John", - MyGuid = Guid.NewGuid(), - MyDateTime = DateTime.Now, - MyProperty = "SerializeTHIS", - MyIgnore = "IgnoreTHIS", - MyIntNullable = 999, - MyStringList = new List() { "String-1", "String-2" }, - MyWriteOnly = "write-only", - MyInternalProperty = "internal-field", - MyNameValueCollection = new NameValueCollection(), - MyDict = new Dictionary() { { 1, "Row1" }, { 2, "Row2" } }, - MyStringArray = new string[] { "One", "Two" }, - MyEnumProp = MyEnum.Second, - MyChar = 'Y', - MyUri = new Uri("http://www.numeria.com.br"), - MyByte = 255, - MyDecimal = 19.9m, - MyDecimalNullable = 25.5m, - - MyInterface = new MyImpl { Name = "John" }, - MyListInterface = new List() { new MyImpl { Name = "John" } }, - MyIListInterface = new List() { new MyImpl { Name = "John" } }, - - MyObjectString = "MyString", - MyObjectInt = 123, - MyObjectImpl = new MyImpl { Name = "John" }, - MyObjectList = new List() { 1, "ola", new MyImpl { Name = "John" }, new Uri("http://www.cnn.com") } - }; - - c.MyNameValueCollection["key-1"] = "value-1"; - c.MyNameValueCollection["KeyNumber2"] = "value-2"; - - return c; - } - - - [TestMethod] - public void Mapper_Test() - { - var mapper = new BsonMapper(); - mapper.UseLowerCaseDelimiter('_'); - - var obj = CreateModel(); - var doc = mapper.ToDocument(obj); - - var json = JsonSerializer.Serialize(doc, true); - - var nobj = mapper.ToObject(doc); - - // compare object to document - Assert.AreEqual(doc["_id"].AsInt32, obj.MyId); - Assert.AreEqual(doc["MY-STRING"].AsString, obj.MyString); - Assert.AreEqual(doc["my_guid"].AsGuid, obj.MyGuid); - - // compare 2 objects - Assert.AreEqual(obj.MyId, nobj.MyId); - Assert.AreEqual(obj.MyString, nobj.MyString); - Assert.AreEqual(obj.MyProperty, nobj.MyProperty); - Assert.AreEqual(obj.MyGuid, nobj.MyGuid); - Assert.AreEqual(obj.MyDateTime, nobj.MyDateTime); - Assert.AreEqual(obj.MyDateTimeNullable, nobj.MyDateTimeNullable); - Assert.AreEqual(obj.MyIntNullable, nobj.MyIntNullable); - Assert.AreEqual(obj.MyEnumProp, nobj.MyEnumProp); - Assert.AreEqual(obj.MyChar, nobj.MyChar); - Assert.AreEqual(obj.MyByte, nobj.MyByte); - Assert.AreEqual(obj.MyDecimal, nobj.MyDecimal); - Assert.AreEqual(obj.MyUri, nobj.MyUri); - Assert.AreEqual(obj.MyNameValueCollection["key-1"], nobj.MyNameValueCollection["key-1"]); - Assert.AreEqual(obj.MyNameValueCollection["KeyNumber2"], nobj.MyNameValueCollection["KeyNumber2"]); - - - // list - Assert.AreEqual(obj.MyStringArray[0], nobj.MyStringArray[0]); - Assert.AreEqual(obj.MyStringArray[1], nobj.MyStringArray[1]); - Assert.AreEqual(obj.MyDict[2], nobj.MyDict[2]); - - // interfaces - Assert.AreEqual(obj.MyInterface.Name, nobj.MyInterface.Name); - Assert.AreEqual(obj.MyListInterface[0].Name, nobj.MyListInterface[0].Name); - Assert.AreEqual(obj.MyIListInterface[0].Name, nobj.MyIListInterface[0].Name); - - // objects - Assert.AreEqual(obj.MyObjectString, nobj.MyObjectString); - Assert.AreEqual(obj.MyObjectInt, nobj.MyObjectInt); - Assert.AreEqual((obj.MyObjectImpl as MyImpl).Name, (nobj.MyObjectImpl as MyImpl).Name); - Assert.AreEqual(obj.MyObjectList[0], obj.MyObjectList[0]); - Assert.AreEqual(obj.MyObjectList[1], obj.MyObjectList[1]); - Assert.AreEqual(obj.MyObjectList[3], obj.MyObjectList[3]); - - Assert.AreEqual(nobj.MyInternalProperty, null); - } - - } -} +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; +using System.Collections.Specialized; +using System.Security; + +namespace UnitTest +{ + public enum MyEnum { First, Second } + + public class MyClass + { + [BsonId(false)] + public int MyId { get; set; } + [BsonField("MY-STRING")] + public string MyString { get; set; } + public Guid MyGuid { get; set; } + public DateTime MyDateTime { get; set; } + public DateTime? MyDateTimeNullable { get; set; } + public int? MyIntNullable { get; set; } + public MyEnum MyEnumProp { get; set; } + public char MyChar { get; set; } + public byte MyByte { get; set; } + + [BsonIndex(ignoreCase: true)] + public decimal MyDecimal { get; set; } + public decimal? MyDecimalNullable { get; set; } + + [BsonIndex(true)] + public Uri MyUri { get; set; } + + // serialize this properties + [BsonField] + internal string MyProperty { get; set; } + + // do not serialize this properties + [BsonIgnore] + public string MyIgnore { get; set; } + public string MyReadOnly { get; private set; } + public string MyWriteOnly { set; private get; } + public string MyField = "DoNotSerializeThis"; + internal string MyInternalProperty { get; set; } + + // special types + public NameValueCollection MyNameValueCollection { get; set; } + + // lists + public string[] MyStringArray { get; set; } + public List MyStringList { get; set; } + public Dictionary MyDict { get; set; } + + // interfaces + public IMyInterface MyInterface { get; set; } + public List MyListInterface { get; set; } + public IList MyIListInterface { get; set; } + + // objects + public object MyObjectString { get; set; } + public object MyObjectInt { get; set; } + public object MyObjectImpl { get; set; } + public List MyObjectList { get; set; } + } + + + + public interface IMyInterface + { + string Name { get; set; } + } + + public class MyImpl : IMyInterface + { + public string Name { get; set; } + } + + [TestClass] + public class MapperTest + { + private MyClass CreateModel() + { + var c = new MyClass + { + MyId = 123, + MyString = "John", + MyGuid = Guid.NewGuid(), + MyDateTime = DateTime.Now, + MyProperty = "SerializeTHIS", + MyIgnore = "IgnoreTHIS", + MyIntNullable = 999, + MyStringList = new List() { "String-1", "String-2" }, + MyWriteOnly = "write-only", + MyInternalProperty = "internal-field", + MyNameValueCollection = new NameValueCollection(), + MyDict = new Dictionary() { { 1, "Row1" }, { 2, "Row2" } }, + MyStringArray = new string[] { "One", "Two" }, + MyEnumProp = MyEnum.Second, + MyChar = 'Y', + MyUri = new Uri("http://www.numeria.com.br"), + MyByte = 255, + MyDecimal = 19.9m, + MyDecimalNullable = 25.5m, + + MyInterface = new MyImpl { Name = "John" }, + MyListInterface = new List() { new MyImpl { Name = "John" } }, + MyIListInterface = new List() { new MyImpl { Name = "John" } }, + + MyObjectString = "MyString", + MyObjectInt = 123, + MyObjectImpl = new MyImpl { Name = "John" }, + MyObjectList = new List() { 1, "ola", new MyImpl { Name = "John" }, new Uri("http://www.cnn.com") } + }; + + c.MyNameValueCollection["key-1"] = "value-1"; + c.MyNameValueCollection["KeyNumber2"] = "value-2"; + + return c; + } + + + [TestMethod] + public void Mapper_Test() + { + var mapper = new BsonMapper(); + mapper.UseLowerCaseDelimiter('_'); + + var obj = CreateModel(); + var doc = mapper.ToDocument(obj); + + var json = JsonSerializer.Serialize(doc, true); + + var nobj = mapper.ToObject(doc); + + // compare object to document + Assert.AreEqual(doc["_id"].AsInt32, obj.MyId); + Assert.AreEqual(doc["MY-STRING"].AsString, obj.MyString); + Assert.AreEqual(doc["my_guid"].AsGuid, obj.MyGuid); + + // compare 2 objects + Assert.AreEqual(obj.MyId, nobj.MyId); + Assert.AreEqual(obj.MyString, nobj.MyString); + Assert.AreEqual(obj.MyProperty, nobj.MyProperty); + Assert.AreEqual(obj.MyGuid, nobj.MyGuid); + Assert.AreEqual(obj.MyDateTime, nobj.MyDateTime); + Assert.AreEqual(obj.MyDateTimeNullable, nobj.MyDateTimeNullable); + Assert.AreEqual(obj.MyIntNullable, nobj.MyIntNullable); + Assert.AreEqual(obj.MyEnumProp, nobj.MyEnumProp); + Assert.AreEqual(obj.MyChar, nobj.MyChar); + Assert.AreEqual(obj.MyByte, nobj.MyByte); + Assert.AreEqual(obj.MyDecimal, nobj.MyDecimal); + Assert.AreEqual(obj.MyUri, nobj.MyUri); + Assert.AreEqual(obj.MyNameValueCollection["key-1"], nobj.MyNameValueCollection["key-1"]); + Assert.AreEqual(obj.MyNameValueCollection["KeyNumber2"], nobj.MyNameValueCollection["KeyNumber2"]); + + + // list + Assert.AreEqual(obj.MyStringArray[0], nobj.MyStringArray[0]); + Assert.AreEqual(obj.MyStringArray[1], nobj.MyStringArray[1]); + Assert.AreEqual(obj.MyDict[2], nobj.MyDict[2]); + + // interfaces + Assert.AreEqual(obj.MyInterface.Name, nobj.MyInterface.Name); + Assert.AreEqual(obj.MyListInterface[0].Name, nobj.MyListInterface[0].Name); + Assert.AreEqual(obj.MyIListInterface[0].Name, nobj.MyIListInterface[0].Name); + + // objects + Assert.AreEqual(obj.MyObjectString, nobj.MyObjectString); + Assert.AreEqual(obj.MyObjectInt, nobj.MyObjectInt); + Assert.AreEqual((obj.MyObjectImpl as MyImpl).Name, (nobj.MyObjectImpl as MyImpl).Name); + Assert.AreEqual(obj.MyObjectList[0], obj.MyObjectList[0]); + Assert.AreEqual(obj.MyObjectList[1], obj.MyObjectList[1]); + Assert.AreEqual(obj.MyObjectList[3], obj.MyObjectList[3]); + + Assert.AreEqual(nobj.MyInternalProperty, null); + } + + } +} diff --git a/UnitTest/ObjectIdTest.cs b/LiteDB.Tests/ObjectIdTest.cs similarity index 100% rename from UnitTest/ObjectIdTest.cs rename to LiteDB.Tests/ObjectIdTest.cs diff --git a/UnitTest/PerformanceTest.cs b/LiteDB.Tests/PerformanceTest.cs similarity index 100% rename from UnitTest/PerformanceTest.cs rename to LiteDB.Tests/PerformanceTest.cs diff --git a/UnitTest/Properties/AssemblyInfo.cs b/LiteDB.Tests/Properties/AssemblyInfo.cs similarity index 84% rename from UnitTest/Properties/AssemblyInfo.cs rename to LiteDB.Tests/Properties/AssemblyInfo.cs index d12364eaf..be8541cd1 100644 --- a/UnitTest/Properties/AssemblyInfo.cs +++ b/LiteDB.Tests/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("UnitTest")] +[assembly: AssemblyTitle("LiteDB.Tests")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("UnitTest")] -[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyProduct("LiteDB.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/UnitTest/Utils/DB.cs b/LiteDB.Tests/Utils/DB.cs similarity index 100% rename from UnitTest/Utils/DB.cs rename to LiteDB.Tests/Utils/DB.cs diff --git a/LiteDB.sln b/LiteDB.sln index 746d0c3f5..87b0f4736 100644 --- a/LiteDB.sln +++ b/LiteDB.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB", "LiteDB\LiteDB.csproj", "{E808051A-83B7-4FA9-B004-D064EA162B60}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "UnitTest\UnitTest.csproj", "{BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Shell", "LiteDB.Shell\LiteDB.Shell.csproj", "{5C46B331-F4DC-469D-B370-532E67CBA808}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Tests", "LiteDB.Tests\LiteDB.Tests.csproj", "{BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,14 +19,14 @@ Global {E808051A-83B7-4FA9-B004-D064EA162B60}.Debug|Any CPU.Build.0 = Debug|Any CPU {E808051A-83B7-4FA9-B004-D064EA162B60}.Release|Any CPU.ActiveCfg = Release|Any CPU {E808051A-83B7-4FA9-B004-D064EA162B60}.Release|Any CPU.Build.0 = Release|Any CPU - {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.Build.0 = Release|Any CPU {5C46B331-F4DC-469D-B370-532E67CBA808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C46B331-F4DC-469D-B370-532E67CBA808}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C46B331-F4DC-469D-B370-532E67CBA808}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C46B331-F4DC-469D-B370-532E67CBA808}.Release|Any CPU.Build.0 = Release|Any CPU + {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LiteDB/Database/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs similarity index 100% rename from LiteDB/Database/Collections/Delete.cs rename to LiteDB/Core/Collections/Delete.cs diff --git a/LiteDB/Database/Collections/Drop.cs b/LiteDB/Core/Collections/Drop.cs similarity index 100% rename from LiteDB/Database/Collections/Drop.cs rename to LiteDB/Core/Collections/Drop.cs diff --git a/LiteDB/Database/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs similarity index 100% rename from LiteDB/Database/Collections/Find.cs rename to LiteDB/Core/Collections/Find.cs diff --git a/LiteDB/Database/Collections/FindIndex.cs b/LiteDB/Core/Collections/FindIndex.cs similarity index 100% rename from LiteDB/Database/Collections/FindIndex.cs rename to LiteDB/Core/Collections/FindIndex.cs diff --git a/LiteDB/Database/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs similarity index 100% rename from LiteDB/Database/Collections/Include.cs rename to LiteDB/Core/Collections/Include.cs diff --git a/LiteDB/Database/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs similarity index 100% rename from LiteDB/Database/Collections/Index.cs rename to LiteDB/Core/Collections/Index.cs diff --git a/LiteDB/Database/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs similarity index 100% rename from LiteDB/Database/Collections/Insert.cs rename to LiteDB/Core/Collections/Insert.cs diff --git a/LiteDB/Database/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs similarity index 96% rename from LiteDB/Database/Collections/InsertBulk.cs rename to LiteDB/Core/Collections/InsertBulk.cs index d36f63991..abf6e3ed1 100644 --- a/LiteDB/Database/Collections/InsertBulk.cs +++ b/LiteDB/Core/Collections/InsertBulk.cs @@ -38,7 +38,7 @@ public int InsertBulk(IEnumerable docs, int buffer = 32768) } this.Database.Transaction.Commit(); - this.Database.Cache.Clear(null); + this.Database.Cache.Clear(); if (more == false) { diff --git a/LiteDB/Database/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs similarity index 100% rename from LiteDB/Database/Collections/LiteCollection.cs rename to LiteDB/Core/Collections/LiteCollection.cs diff --git a/LiteDB/Database/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs similarity index 100% rename from LiteDB/Database/Collections/Update.cs rename to LiteDB/Core/Collections/Update.cs diff --git a/LiteDB/Database/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs similarity index 80% rename from LiteDB/Database/LiteDatabase.cs rename to LiteDB/Core/LiteDatabase.cs index b246d7f7b..0b5bcd742 100644 --- a/LiteDB/Database/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -1,4 +1,5 @@ -using System; +using LiteDB.Shell; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,18 +14,12 @@ public partial class LiteDatabase : IDisposable { #region Properties + Ctor - public ConnectionString ConnectionString { get; private set; } - - internal RecoveryService Recovery { get; private set; } - internal CacheService Cache { get; private set; } - internal DiskService Disk { get; private set; } + internal IDiskService Disk { get; private set; } internal PageService Pager { get; private set; } - internal JournalService Journal { get; private set; } - internal TransactionService Transaction { get; private set; } internal IndexService Indexer { get; private set; } @@ -38,39 +33,25 @@ public partial class LiteDatabase : IDisposable /// /// Starts LiteDB database. Open database file or create a new one if not exits /// - /// Full filename or connection string - public LiteDatabase(string connectionString) + public LiteDatabase(string filename) { - this.ConnectionString = new ConnectionString(connectionString); + this.Disk = new FileDiskService(filename); - if (!File.Exists(this.ConnectionString.Filename)) - { - DiskService.CreateNewDatafile(this.ConnectionString); - } + this.Disk.Initialize(); this.Mapper = BsonMapper.Global; - this.Recovery = new RecoveryService(this.ConnectionString); - - this.Recovery.TryRecovery(); - - this.Disk = new DiskService(this.ConnectionString); - - this.Cache = new CacheService(this.Disk); + this.Cache = new CacheService(); this.Pager = new PageService(this.Disk, this.Cache); - this.Journal = new JournalService(this.ConnectionString, this.Cache); - - this.Indexer = new IndexService(this.Cache, this.Pager); + this.Indexer = new IndexService(this.Pager); - this.Transaction = new TransactionService(this.Disk, this.Cache, this.Journal); + this.Data = new DataService(this.Pager); - this.Data = new DataService(this.Disk, this.Cache, this.Pager); + this.Collections = new CollectionService(this.Pager, this.Indexer, this.Data); - this.Collections = new CollectionService(this.Cache, this.Pager, this.Indexer, this.Data); - - this.UpdateDatabaseVersion(); + this.Transaction = new TransactionService(this.Disk, this.Cache); } #endregion @@ -201,6 +182,24 @@ public void Rollback() #endregion + #region Shell + + private LiteShell _shell = null; + + /// + /// Run a shell command in current database. Returns a BsonValue as result + /// + public BsonValue RunCommand(string command) + { + if (_shell == null) + { + _shell = new LiteShell(this); + } + return _shell.Run(command); + } + + #endregion + public void Dispose() { this.Disk.Dispose(); diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs new file mode 100644 index 000000000..0f5b23cb5 --- /dev/null +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace LiteDB +{ + internal class FileDiskService : IDiskService + { + private const int LOCK_POSITION = 0; + private FileStream _stream; + private BinaryReader _reader; + private BinaryWriter _writer; + private string _filename; + + public FileDiskService(string filename) + { + _filename = filename; + } + + public void Initialize() + { + // open file as readOnly - if we need use Write, re-open in Write Mode + _stream = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite, BasePage.PAGE_SIZE); + + _reader = new BinaryReader(_stream); + + if (_stream.Length == 0) + { + this.WritePages(new HeaderPage[] { new HeaderPage() }); + } + } + + public void Lock() + { + TryExec(() => _stream.Lock(LOCK_POSITION, 1)); + } + + public void Unlock() + { + _stream.Unlock(LOCK_POSITION, 1); + } + + public T ReadPage(uint pageID) + where T : BasePage, new() + { + var page = new T(); + var posStart = (long)pageID * (long)BasePage.PAGE_SIZE; + var posEnd = posStart + BasePage.PAGE_SIZE; + + TryExec(() => + { + // read page header + page.ReadHeader(_reader); + + // if T is base and PageType has a defined type, convert page + var isBase = page.GetType() == typeof(BasePage); + + if (isBase) + { + if (page.PageType == PageType.Index) page = (T)(object)page.CopyTo(); + else if (page.PageType == PageType.Data) page = (T)(object)page.CopyTo(); + else if (page.PageType == PageType.Extend) page = (T)(object)page.CopyTo(); + else if (page.PageType == PageType.Collection) page = (T)(object)page.CopyTo(); + } + + // read page content if page is not empty + if (page.PageType != PageType.Empty) + { + page.ReadContent(_reader); + } + + // position cursor at starts next page + _reader.ReadBytes((int)(posEnd - _stream.Position)); + }); + + return page; + } + + public void WritePages(IEnumerable pages) + { + if(_writer == null) + { + _reader.Dispose(); + _stream.Dispose(); + _stream = new FileStream(_filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); + _reader = new BinaryReader(_stream); + _writer = new BinaryWriter(_stream); + } + + foreach(var page in pages) + { + var posStart = (long)page.PageID * (long)BasePage.PAGE_SIZE; + var posEnd = posStart + BasePage.PAGE_SIZE; + + // position cursor + if (_stream.Position != posStart) + { + _stream.Seek(posStart, SeekOrigin.Begin); + } + + // write page header + page.WriteHeader(_writer); + + // write content except for empty pages + if (page.PageType != PageType.Empty) + { + page.WriteContent(_writer); + } + + // write with zero non-used page + _writer.Write(new byte[posEnd - _stream.Position]); + + page.IsDirty = false; + } + } + + public void Dispose() + { + if(_stream != null) + { + _stream.Dispose(); + _reader.Close(); + if(_writer != null) _writer.Dispose(); + } + } + + public static void TryExec(Action action) + { + var timeout = new TimeSpan(0, 1, 0); + var timer = DateTime.Now.Add(timeout); + + while (DateTime.Now < timer) + { + try + { + action(); + return; + } + catch (UnauthorizedAccessException) + { + Thread.Sleep(250); + } + catch (IOException ex) + { + ex.WaitIfLocked(250); + } + } + + throw LiteException.LockTimeout(timeout); + } + } +} diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs new file mode 100644 index 000000000..0e5eeb897 --- /dev/null +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace LiteDB +{ + internal interface IDiskService : IDisposable + { + void Initialize(); + void Lock(); + void Unlock(); + T ReadPage(uint pageID) where T : BasePage, new(); + void WritePages(IEnumerable pages); + } +} diff --git a/LiteDB/Storage/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs similarity index 98% rename from LiteDB/Storage/Pages/BasePage.cs rename to LiteDB/DataStructure/Pages/BasePage.cs index d33ee1ee5..0c4ad9cfb 100644 --- a/LiteDB/Storage/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -116,7 +116,7 @@ public T CopyTo() return page; } - #region Page Header + #region Read/Write page public virtual void ReadHeader(BinaryReader reader) { @@ -138,10 +138,6 @@ public virtual void WriteHeader(BinaryWriter writer) writer.Write((UInt16)this.FreeBytes); } - #endregion - - #region Page Content - public virtual void ReadContent(BinaryReader reader) { } diff --git a/LiteDB/Storage/Pages/CollectionPage.cs b/LiteDB/DataStructure/Pages/CollectionPage.cs similarity index 98% rename from LiteDB/Storage/Pages/CollectionPage.cs rename to LiteDB/DataStructure/Pages/CollectionPage.cs index 66cb5ebb8..1062cfe2d 100644 --- a/LiteDB/Storage/Pages/CollectionPage.cs +++ b/LiteDB/DataStructure/Pages/CollectionPage.cs @@ -53,6 +53,8 @@ public CollectionPage() } } + #region Read/Write pages + public override void ReadContent(BinaryReader reader) { this.CollectionName = reader.ReadString(); @@ -93,6 +95,8 @@ public override void WriteContent(BinaryWriter writer) } } + #endregion + #region Methods to work with index array /// diff --git a/LiteDB/Storage/Pages/DataPage.cs b/LiteDB/DataStructure/Pages/DataPage.cs similarity index 94% rename from LiteDB/Storage/Pages/DataPage.cs rename to LiteDB/DataStructure/Pages/DataPage.cs index 65345d091..8f86bfd27 100644 --- a/LiteDB/Storage/Pages/DataPage.cs +++ b/LiteDB/DataStructure/Pages/DataPage.cs @@ -47,20 +47,7 @@ public override void UpdateItemCount() this.FreeBytes = PAGE_AVAILABLE_BYTES - this.DataBlocks.Sum(x => x.Value.Length); } - public override void WriteContent(BinaryWriter writer) - { - foreach (var block in this.DataBlocks.Values) - { - writer.Write(block.Position.Index); - writer.Write(block.ExtendPageID); - foreach (var idx in block.IndexRef) - { - writer.Write(idx); - } - writer.Write((ushort)block.Data.Length); - writer.Write(block.Data); - } - } + #region Read/Write pages public override void ReadContent(BinaryReader reader) { @@ -74,7 +61,7 @@ public override void ReadContent(BinaryReader reader) block.Position = new PageAddress(this.PageID, reader.ReadUInt16()); block.ExtendPageID = reader.ReadUInt32(); - for(var j = 0; j < CollectionIndex.INDEX_PER_COLLECTION; j++) + for (var j = 0; j < CollectionIndex.INDEX_PER_COLLECTION; j++) { block.IndexRef[j] = reader.ReadPageAddress(); } @@ -83,7 +70,24 @@ public override void ReadContent(BinaryReader reader) block.Data = reader.ReadBytes(size); this.DataBlocks.Add(block.Position.Index, block); - } + } } + + public override void WriteContent(BinaryWriter writer) + { + foreach (var block in this.DataBlocks.Values) + { + writer.Write(block.Position.Index); + writer.Write(block.ExtendPageID); + foreach (var idx in block.IndexRef) + { + writer.Write(idx); + } + writer.Write((ushort)block.Data.Length); + writer.Write(block.Data); + } + } + + #endregion } } diff --git a/LiteDB/Storage/Pages/ExtendPage.cs b/LiteDB/DataStructure/Pages/ExtendPage.cs similarity index 96% rename from LiteDB/Storage/Pages/ExtendPage.cs rename to LiteDB/DataStructure/Pages/ExtendPage.cs index 88d92efb4..ff819c50b 100644 --- a/LiteDB/Storage/Pages/ExtendPage.cs +++ b/LiteDB/DataStructure/Pages/ExtendPage.cs @@ -42,6 +42,8 @@ public override void UpdateItemCount() this.FreeBytes = PAGE_AVAILABLE_BYTES - this.Data.Length; // not used on ExtendPage } + #region Read/Write pages + public override void ReadContent(BinaryReader reader) { this.Data = reader.ReadBytes(this.ItemCount); @@ -51,5 +53,7 @@ public override void WriteContent(BinaryWriter writer) { writer.Write(this.Data); } + + #endregion } } diff --git a/LiteDB/Storage/Pages/HeaderPage.cs b/LiteDB/DataStructure/Pages/HeaderPage.cs similarity index 77% rename from LiteDB/Storage/Pages/HeaderPage.cs rename to LiteDB/DataStructure/Pages/HeaderPage.cs index 02a889816..a7efd80ee 100644 --- a/LiteDB/Storage/Pages/HeaderPage.cs +++ b/LiteDB/DataStructure/Pages/HeaderPage.cs @@ -16,7 +16,7 @@ internal class HeaderPage : BasePage /// /// Datafile specification version /// - private const byte FILE_VERSION = 4; + private const byte FILE_VERSION = 5; /// /// Get/Set the changeID of data. When a client read pages, all pages are in the same version. But when OpenTransaction, we need validade that current changeID is the sabe that we have in cache @@ -47,53 +47,42 @@ public HeaderPage() : base() { this.PageID = 0; - this.PageType = LiteDB.PageType.Header; + this.PageType = PageType.Header; this.FreeEmptyPageID = uint.MaxValue; this.FirstCollectionPageID = uint.MaxValue; this.ChangeID = 0; this.LastPageID = 0; - this.UserVersion = 0; this.ItemCount = 1; // fixed for header this.FreeBytes = 0; // no free bytes on header } - public override void ReadHeader(BinaryReader reader) - { - reader.BaseStream.Seek(4, SeekOrigin.Current); // skip byte 0 - it's loked in a transaction - //this.PageID = reader.ReadUInt32(); - this.PrevPageID = reader.ReadUInt32(); - this.NextPageID = reader.ReadUInt32(); - this.PageType = (PageType)reader.ReadByte(); - this.ItemCount = reader.ReadUInt16(); - this.FreeBytes = reader.ReadUInt16(); - } + #region Read/Write pages - public override void ReadContent(BinaryReader reader) + public override void ReadHeader(BinaryReader reader) { - var info = reader.ReadString(); - - if (info != HEADER_INFO) throw LiteException.InvalidDatabase(reader.BaseStream); - - var ver = reader.ReadByte(); - - if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(reader.BaseStream, ver); - this.ChangeID = reader.ReadUInt16(); + var info = reader.ReadString(HEADER_INFO.Length); + var ver = reader.ReadByte(); this.FreeEmptyPageID = reader.ReadUInt32(); this.FirstCollectionPageID = reader.ReadUInt32(); this.LastPageID = reader.ReadUInt32(); this.UserVersion = reader.ReadInt32(); + + if (info != HEADER_INFO) throw LiteException.InvalidDatabase(reader.BaseStream); + if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(reader.BaseStream, ver); } - public override void WriteContent(BinaryWriter writer) + public override void WriteHeader(BinaryWriter writer) { - writer.Write(HEADER_INFO); - writer.Write(FILE_VERSION); writer.Write(this.ChangeID); + writer.Write(HEADER_INFO, HEADER_INFO.Length); + writer.Write(FILE_VERSION); writer.Write(this.FreeEmptyPageID); writer.Write(this.FirstCollectionPageID); writer.Write(this.LastPageID); writer.Write(this.UserVersion); } + + #endregion } } diff --git a/LiteDB/Storage/Pages/IndexPage.cs b/LiteDB/DataStructure/Pages/IndexPage.cs similarity index 98% rename from LiteDB/Storage/Pages/IndexPage.cs rename to LiteDB/DataStructure/Pages/IndexPage.cs index 73141ea4c..76afb2d04 100644 --- a/LiteDB/Storage/Pages/IndexPage.cs +++ b/LiteDB/DataStructure/Pages/IndexPage.cs @@ -40,6 +40,8 @@ public override void UpdateItemCount() this.FreeBytes = PAGE_AVAILABLE_BYTES - this.Nodes.Sum(x => x.Value.Length); } + #region Read/Write pages + public override void ReadContent(BinaryReader reader) { this.Nodes = new Dictionary(this.ItemCount); @@ -84,5 +86,7 @@ public override void WriteContent(BinaryWriter writer) } } } + + #endregion } } diff --git a/LiteDB/Storage/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs similarity index 52% rename from LiteDB/Storage/Services/CacheService.cs rename to LiteDB/DataStructure/Services/CacheService.cs index 4e3b3e5cf..2701b214a 100644 --- a/LiteDB/Storage/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -16,35 +16,11 @@ internal class CacheService : IDisposable // a very simple dictionary for pages cache and track private SortedDictionary _cache; - private DiskService _disk; - - private HeaderPage _header; - - public CacheService(DiskService disk) + public CacheService() { - _disk = disk; - _cache = new SortedDictionary(); } - /// - /// Gets total pages in cache for database info - /// - public int PagesInCache { get { return _cache.Count; } } - - /// - /// Get header page in cache or request for a new instance if not existis yet - /// - public HeaderPage Header - { - get - { - if (_header == null) - _header = _disk.ReadPage(0); - return _header; - } - } - /// /// Get a page inside cache system. Returns null if page not existis. /// If T is more specific than page that I have in cache, returns null (eg. Page 2 is BasePage in cache and this method call for IndexPage PageId 2) @@ -74,57 +50,18 @@ public void AddPage(BasePage page) /// /// Empty cache and header page /// - public void Clear(HeaderPage newHeaderPage) + public void Clear() { - _header = newHeaderPage; _cache.Clear(); } - /// - /// Remove from cache only extend pages - useful for FileStorage - /// - public void RemoveExtendPages() - { - var keys = _cache.Values.Where(x => x.PageType == PageType.Extend && x.IsDirty == false).Select(x => x.PageID).ToList(); - - foreach (var key in keys) - { - _cache.Remove(key); - } - } - - /// - /// Persist all dirty pages - /// - public void PersistDirtyPages() - { - // alocate datafile file first (only when file need to grow) - _disk.AllocateDiskSpace((this.Header.LastPageID + 1) * BasePage.PAGE_SIZE); - - foreach (var page in this.GetDirtyPages()) - { - _disk.WritePage(page); - } - } - - /// - /// Checks if cache has dirty pages - /// - public bool HasDirtyPages() - { - return this.GetDirtyPages().FirstOrDefault() != null; - } + public bool HasDirtyPages { get { return this.GetDirtyPages().FirstOrDefault() != null; } } /// /// Returns all dirty pages including header page (for better write performance, get all pages in PageID increase order) /// public IEnumerable GetDirtyPages() { - if (this.Header.IsDirty) - { - yield return _header; - } - // now returns all pages in sequence foreach (var page in _cache.Values.Where(x => x.IsDirty)) { @@ -134,7 +71,7 @@ public IEnumerable GetDirtyPages() public void Dispose() { - this.Clear(null); + this.Clear(); } } } diff --git a/LiteDB/Storage/Services/CollectionService.cs b/LiteDB/DataStructure/Services/CollectionService.cs similarity index 83% rename from LiteDB/Storage/Services/CollectionService.cs rename to LiteDB/DataStructure/Services/CollectionService.cs index 1633c441f..9409fe642 100644 --- a/LiteDB/Storage/Services/CollectionService.cs +++ b/LiteDB/DataStructure/Services/CollectionService.cs @@ -9,14 +9,12 @@ namespace LiteDB { internal class CollectionService { - private CacheService _cache; private PageService _pager; private IndexService _indexer; private DataService _data; - public CollectionService(CacheService cache, PageService pager, IndexService indexer, DataService data) + public CollectionService(PageService pager, IndexService indexer, DataService data) { - _cache = cache; _pager = pager; _indexer = indexer; _data = data; @@ -29,9 +27,9 @@ public CollectionPage Get(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); - if (_cache.Header.FirstCollectionPageID == uint.MaxValue) return null; + if (_pager.Header.FirstCollectionPageID == uint.MaxValue) return null; - var pages = _pager.GetSeqPages(_cache.Header.FirstCollectionPageID); + var pages = _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); var col = pages.FirstOrDefault(x => x.CollectionName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); @@ -47,7 +45,7 @@ public CollectionPage Add(string name) if(!CollectionPage.NamePattern.IsMatch(name)) throw LiteException.InvalidFormat("CollectionName", name); // test collection limit - var pages = _pager.GetSeqPages(_cache.Header.FirstCollectionPageID); + var pages = _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); if (pages.Count() >= CollectionPage.MAX_COLLECTIONS) { @@ -57,7 +55,7 @@ public CollectionPage Add(string name) var col = _pager.NewPage(); // add page in collection list - _pager.AddOrRemoveToFreeList(true, col, _cache.Header, ref _cache.Header.FirstCollectionPageID); + _pager.AddOrRemoveToFreeList(true, col, _pager.Header, ref _pager.Header.FirstCollectionPageID); col.CollectionName = name; col.IsDirty = true; @@ -76,7 +74,7 @@ public CollectionPage Add(string name) /// public IEnumerable GetAll() { - return _pager.GetSeqPages(_cache.Header.FirstCollectionPageID); + return _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); } /// @@ -121,7 +119,7 @@ public void Drop(CollectionPage col) } // remove page from collection list - _pager.AddOrRemoveToFreeList(false, col, _cache.Header, ref _cache.Header.FirstCollectionPageID); + _pager.AddOrRemoveToFreeList(false, col, _pager.Header, ref _pager.Header.FirstCollectionPageID); _pager.DeletePage(col.PageID, false); } diff --git a/LiteDB/Storage/Services/DataService.cs b/LiteDB/DataStructure/Services/DataService.cs similarity index 97% rename from LiteDB/Storage/Services/DataService.cs rename to LiteDB/DataStructure/Services/DataService.cs index 59e2e262f..eaa22bb4c 100644 --- a/LiteDB/Storage/Services/DataService.cs +++ b/LiteDB/DataStructure/Services/DataService.cs @@ -8,14 +8,10 @@ namespace LiteDB { internal class DataService { - private DiskService _disk; private PageService _pager; - private CacheService _cache; - public DataService(DiskService disk, CacheService cache, PageService pager) + public DataService(PageService pager) { - _disk = disk; - _cache = cache; _pager = pager; } diff --git a/LiteDB/Storage/Services/IndexService.cs b/LiteDB/DataStructure/Services/IndexService.cs similarity index 98% rename from LiteDB/Storage/Services/IndexService.cs rename to LiteDB/DataStructure/Services/IndexService.cs index 0b8762e3f..0d7e39fe7 100644 --- a/LiteDB/Storage/Services/IndexService.cs +++ b/LiteDB/DataStructure/Services/IndexService.cs @@ -18,13 +18,11 @@ internal class IndexService public const int MAX_INDEX_LENGTH = 512; private PageService _pager; - private CacheService _cache; private Random _rand = new Random(); - public IndexService(CacheService cache, PageService pager) + public IndexService(PageService pager) { - _cache = cache; _pager = pager; } diff --git a/LiteDB/Storage/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs similarity index 93% rename from LiteDB/Storage/Services/PageService.cs rename to LiteDB/DataStructure/Services/PageService.cs index 1badc0d28..1e1ab60b8 100644 --- a/LiteDB/Storage/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -9,10 +9,10 @@ namespace LiteDB { internal class PageService { - private DiskService _disk; + private IDiskService _disk; private CacheService _cache; - public PageService(DiskService disk, CacheService cache) + public PageService(IDiskService disk, CacheService cache) { _disk = disk; _cache = cache; @@ -36,6 +36,8 @@ public T GetPage(uint pageID) return page; } + public HeaderPage Header { get { return this.GetPage(0); } } + /// /// Read all sequences pages from a start pageID (using NextPageID) /// @@ -63,18 +65,18 @@ public T NewPage(BasePage prevPage = null) var page = new T(); // try get page from Empty free list - if(_cache.Header.FreeEmptyPageID != uint.MaxValue) + if(this.Header.FreeEmptyPageID != uint.MaxValue) { - var free = this.GetPage(_cache.Header.FreeEmptyPageID); + var free = this.GetPage(this.Header.FreeEmptyPageID); // remove page from empty list - this.AddOrRemoveToFreeList(false, free, _cache.Header, ref _cache.Header.FreeEmptyPageID); + this.AddOrRemoveToFreeList(false, free, this.Header, ref this.Header.FreeEmptyPageID); page.PageID = free.PageID; } else { - page.PageID = ++_cache.Header.LastPageID; + page.PageID = ++this.Header.LastPageID; } // if there a page before, just fix NextPageID pointer @@ -87,7 +89,7 @@ public T NewPage(BasePage prevPage = null) // mark header and this new page as dirty, and then add to cache page.IsDirty = true; - _cache.Header.IsDirty = true; + this.Header.IsDirty = true; _cache.AddPage(page); @@ -109,7 +111,7 @@ public void DeletePage(uint pageID, bool addSequence = false) page.IsDirty = true; // add to empty free list - this.AddOrRemoveToFreeList(true, page, _cache.Header, ref _cache.Header.FreeEmptyPageID); + this.AddOrRemoveToFreeList(true, page, this.Header, ref this.Header.FreeEmptyPageID); } } diff --git a/LiteDB/Storage/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs similarity index 53% rename from LiteDB/Storage/Services/TransactionService.cs rename to LiteDB/DataStructure/Services/TransactionService.cs index f31f288bc..6540ecece 100644 --- a/LiteDB/Storage/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -11,18 +11,15 @@ namespace LiteDB /// internal class TransactionService { - private DiskService _disk; + private IDiskService _disk; private CacheService _cache; - private JournalService _journal; private int _level = 0; - private DateTime _lastFileCheck = DateTime.Now; - internal TransactionService(DiskService disk, CacheService cache, JournalService journal) + internal TransactionService(IDiskService disk, CacheService cache) { _disk = disk; _cache = cache; - _journal = journal; } public bool IsInTransaction { get { return _level > 0; } } @@ -34,17 +31,10 @@ public void Begin() { if (_level == 0) { + this.AvoidDirtyRead(); + // lock (or try to) datafile _disk.Lock(); - - // get header page from DISK to check changeID - var header = _disk.ReadPage(0); - - // if changeID was changed, file was changed by another process - if (header.ChangeID != _cache.Header.ChangeID) - { - _cache.Clear(header); - } } _level++; @@ -59,7 +49,7 @@ public void Abort() if (_level == 1) { - _disk.UnLock(); + _disk.Unlock(); _level = 0; } @@ -78,31 +68,22 @@ public void Commit() if (_level == 1) { - if (_cache.HasDirtyPages()) + if (_cache.HasDirtyPages) { + var header = _cache.GetPage(0); + // increase file changeID (back to 0 when overflow) - _cache.Header.ChangeID = _cache.Header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(_cache.Header.ChangeID + (ushort)1); - - _cache.Header.IsDirty = true; - - // first i will save all dirty pages to a recovery file - _journal.CreateJournalFile(() => - { - // [transaction are now acepted] (all dirty pages are in journal file) - // journal file still open in exlcusive mode, let's persist pages - // if occurs a failure during "PersistDirtyPages" database will be recovered on next open - - // inside this file will be all locked for avoid inconsistent reads - _disk.ProtectWriteFile(() => - { - // persist all dirty pages - _cache.PersistDirtyPages(); - }); - }); + header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); + + header.IsDirty = true; + + _disk.WritePages(_cache.GetDirtyPages()); + + _cache.Clear(); } // unlock datafile - _disk.UnLock(); + _disk.Unlock(); _level = 0; } @@ -116,11 +97,11 @@ public void Rollback() { if (_level == 0) return; - // Clear all pages from memory - _cache.Clear(null); + // clear all pages from memory + _cache.Clear(); - // Unlock datafile - _disk.UnLock(); + // unlock datafile + _disk.Unlock(); _level = 0; } @@ -134,18 +115,18 @@ public void AvoidDirtyRead() // if is in transaction pages are not dirty (begin trans was checked) if(this.IsInTransaction == true) return; - // check if file changed only after 1 second from last check - if (DateTime.Now.Subtract(_lastFileCheck).TotalMilliseconds < 1000) return; + var cache = _cache.GetPage(0); - _lastFileCheck = DateTime.Now; + if (cache == null) return; // get header page from DISK to check changeID var header = _disk.ReadPage(0); // if changeID was changed, file was changed by another process - if (header.ChangeID != _cache.Header.ChangeID) + if (cache.ChangeID != header.ChangeID) { - _cache.Clear(header); + _cache.Clear(); + _cache.AddPage(header); } } } diff --git a/LiteDB/Storage/Structures/CollectionIndex.cs b/LiteDB/DataStructure/Structures/CollectionIndex.cs similarity index 100% rename from LiteDB/Storage/Structures/CollectionIndex.cs rename to LiteDB/DataStructure/Structures/CollectionIndex.cs diff --git a/LiteDB/Storage/Structures/DataBlock.cs b/LiteDB/DataStructure/Structures/DataBlock.cs similarity index 100% rename from LiteDB/Storage/Structures/DataBlock.cs rename to LiteDB/DataStructure/Structures/DataBlock.cs diff --git a/LiteDB/Storage/Structures/IndexNode.cs b/LiteDB/DataStructure/Structures/IndexNode.cs similarity index 100% rename from LiteDB/Storage/Structures/IndexNode.cs rename to LiteDB/DataStructure/Structures/IndexNode.cs diff --git a/LiteDB/Storage/Structures/IndexOptions.cs b/LiteDB/DataStructure/Structures/IndexOptions.cs similarity index 100% rename from LiteDB/Storage/Structures/IndexOptions.cs rename to LiteDB/DataStructure/Structures/IndexOptions.cs diff --git a/LiteDB/Storage/Structures/PageAddress.cs b/LiteDB/DataStructure/Structures/PageAddress.cs similarity index 100% rename from LiteDB/Storage/Structures/PageAddress.cs rename to LiteDB/DataStructure/Structures/PageAddress.cs diff --git a/LiteDB/Database/Tools/GetDatabaseInfo.cs b/LiteDB/Database/Tools/GetDatabaseInfo.cs deleted file mode 100644 index 747cb4596..000000000 --- a/LiteDB/Database/Tools/GetDatabaseInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteDatabase - { - /// - /// Get all database information - /// - public BsonDocument GetDatabaseInfo() - { - this.Transaction.AvoidDirtyRead(); - - var info = new BsonDocument(); - - info["filename"] = this.ConnectionString.Filename; - info["journal"] = this.ConnectionString.JournalEnabled; - info["timeout"] = this.ConnectionString.Timeout.TotalSeconds; - info["version"] = this.Cache.Header.UserVersion; - info["changeID"] = this.Cache.Header.ChangeID; - info["fileLength"] = (this.Cache.Header.LastPageID + 1) * BasePage.PAGE_SIZE; - info["lastPageID"] = this.Cache.Header.LastPageID; - info["pagesInCache"] = this.Cache.PagesInCache; - info["dirtyPages"] = this.Cache.GetDirtyPages().Count(); - - //TODO: Add collections info - // Add indexes info - // Add storage used/free info - - return info; - } - } -} diff --git a/LiteDB/Database/Tools/RunCommand.cs b/LiteDB/Database/Tools/RunCommand.cs deleted file mode 100644 index fa2c1293d..000000000 --- a/LiteDB/Database/Tools/RunCommand.cs +++ /dev/null @@ -1,26 +0,0 @@ -using LiteDB.Shell; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteDatabase - { - private LiteShell _shell = null; - - /// - /// Run a shell command in current database. Returns a BsonValue as result - /// - public BsonValue RunCommand(string command) - { - if (_shell == null) - { - _shell = new LiteShell(this); - } - return _shell.Run(command); - } - } -} diff --git a/LiteDB/Database/Tools/UserVersion.cs b/LiteDB/Database/Tools/UserVersion.cs deleted file mode 100644 index 064c29234..000000000 --- a/LiteDB/Database/Tools/UserVersion.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteDatabase - { - /// - /// Virtual method for update database when a new version (from coneection string) was setted - /// - /// The new database version - protected virtual void OnVersionUpdate(int newVersion) - { - } - - /// - /// Update database version, when necessary - /// - private void UpdateDatabaseVersion() - { - // not necessary "AvoidDirtyRead" because its calls from ctor - var current = this.Cache.Header.UserVersion; - var recent = this.ConnectionString.UserVersion; - - // there is no updates - if (current == recent) return; - - // start a transaction - this.Transaction.Begin(); - - try - { - for (var newVersion = current + 1; newVersion <= recent; newVersion++) - { - OnVersionUpdate(newVersion); - - this.Cache.Header.UserVersion = newVersion; - } - - this.Cache.Header.IsDirty = true; - this.Transaction.Commit(); - - } - catch - { - this.Transaction.Rollback(); - throw; - } - } - } -} diff --git a/LiteDB/Database/FileStorage/LiteFileInfo.cs b/LiteDB/FileStorage/LiteFileInfo.cs similarity index 100% rename from LiteDB/Database/FileStorage/LiteFileInfo.cs rename to LiteDB/FileStorage/LiteFileInfo.cs diff --git a/LiteDB/Database/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs similarity index 96% rename from LiteDB/Database/FileStorage/LiteFileStorage.cs rename to LiteDB/FileStorage/LiteFileStorage.cs index c8e29ef2a..b7c3a7274 100644 --- a/LiteDB/Database/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -44,9 +44,6 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) foreach (var chunk in file.CreateChunks(stream)) { this.Chunks.Insert(chunk); - - // clear extend pages in cache to avoid too many use of memory in big files - this.Database.Cache.RemoveExtendPages(); } // update fileLength to confirm full file length stored in disk @@ -207,8 +204,6 @@ public bool Delete(string id) { var del = Chunks.Delete(LiteFileInfo.GetChunckId(id, index++)); - this.Database.Cache.RemoveExtendPages(); - if (del == false) break; } diff --git a/LiteDB/Database/FileStorage/LiteFileStream.cs b/LiteDB/FileStorage/LiteFileStream.cs similarity index 97% rename from LiteDB/Database/FileStorage/LiteFileStream.cs rename to LiteDB/FileStorage/LiteFileStream.cs index 566abd333..edbe0e63a 100644 --- a/LiteDB/Database/FileStorage/LiteFileStream.cs +++ b/LiteDB/FileStorage/LiteFileStream.cs @@ -77,9 +77,6 @@ public override int Read(byte[] buffer, int offset, int count) private byte[] GetChunkData(int index) { - // avoid too many extend pages on memory - _db.Cache.RemoveExtendPages(); - // check if there is no more chunks in this file var chunks = _db.GetCollection("_chunks"); diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 3c73d2105..217c0264d 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -50,14 +50,12 @@ - - - - - - - - + + + + + + @@ -75,15 +73,15 @@ - - - - - - - - - + + + + + + + + + @@ -125,36 +123,32 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + diff --git a/LiteDB/Properties/AssemblyInfo.cs b/LiteDB/Properties/AssemblyInfo.cs index 938207cba..8475cee99 100644 --- a/LiteDB/Properties/AssemblyInfo.cs +++ b/LiteDB/Properties/AssemblyInfo.cs @@ -13,7 +13,7 @@ [assembly: ComVisible(false)] [assembly: Guid("54989b5c-4bcf-4d58-b8ba-9b014a324f76")] -[assembly: AssemblyVersion("1.0.4.0")] -[assembly: AssemblyFileVersion("1.0.4.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] // [assembly: InternalsVisibleTo("UnitTest")] \ No newline at end of file diff --git a/LiteDB/Database/Tools/DbRef.cs b/LiteDB/Serializer/Mapper/DbRef.cs similarity index 100% rename from LiteDB/Database/Tools/DbRef.cs rename to LiteDB/Serializer/Mapper/DbRef.cs diff --git a/LiteDB/Shell/Commands/Others/Info.cs b/LiteDB/Shell/Commands/Others/Info.cs deleted file mode 100644 index 80a2dc00c..000000000 --- a/LiteDB/Shell/Commands/Others/Info.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; - -namespace LiteDB.Shell.Commands -{ - internal class Info : ILiteCommand - { - public bool IsCommand(StringScanner s) - { - return s.Match(@"db\.info$"); - } - - public BsonValue Execute(LiteDatabase db, StringScanner s) - { - return db.GetDatabaseInfo(); - } - } -} diff --git a/LiteDB/Storage/Services/DiskService.cs b/LiteDB/Storage/Services/DiskService.cs deleted file mode 100644 index 60462be7b..000000000 --- a/LiteDB/Storage/Services/DiskService.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace LiteDB -{ - internal class DiskService : IDisposable - { - private const int LOCK_POSITION = 0; - - private ConnectionString _connectionString; - - private BinaryReader _reader; - private BinaryWriter _writer; - - public DiskService(ConnectionString connectionString) - { - _connectionString = connectionString; - - // Open file as ReadOnly - if we need use Write, re-open in Write Mode - var stream = new FileStream(_connectionString.Filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, BasePage.PAGE_SIZE); - - _reader = new BinaryReader(stream); - } - - /// - /// Create a empty database ready to be used using connectionString as parameters - /// - public static void CreateNewDatafile(ConnectionString connectionString) - { - using (var stream = File.Create(connectionString.Filename)) - { - using (var writer = new BinaryWriter(stream)) - { - DiskService.WritePage(writer, new HeaderPage()); - } - } - } - - /// - /// Create a new Page instance and read data from disk - /// - public T ReadPage(uint pageID) - where T : BasePage, new() - { - // create page instance and read from disk (read page header + content page) - var page = new T(); - var stream = _reader.BaseStream; - var posStart = (long)pageID * (long)BasePage.PAGE_SIZE; - var posEnd = posStart + BasePage.PAGE_SIZE; - - TryExec(_connectionString.Timeout, () => - { - // position cursor - if (stream.Position != posStart) - { - stream.Seek(posStart, SeekOrigin.Begin); - } - - // read page header - page.ReadHeader(_reader); - - // if T is base and PageType has a defined type, convert page - var isBase = page.GetType() == typeof(BasePage); - - if (isBase) - { - if (page.PageType == PageType.Index) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Data) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Extend) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Collection) page = (T)(object)page.CopyTo(); - } - - // read page content if page is not empty - if (page.PageType != PageType.Empty) - { - page.ReadContent(_reader); - } - - // position cursor at starts next page - _reader.ReadBytes((int)(posEnd - stream.Position)); - - }); - - return page; - } - - /// - /// Write a page from memory to disk - /// - public void WritePage(BasePage page) - { - DiskService.WritePage(GetWriter(), page); - } - - /// - /// Static method for write a page using a diferent writer - used when create empty datafile - /// - public static void WritePage(BinaryWriter writer, BasePage page) - { - var stream = writer.BaseStream; - var posStart = (long)page.PageID * (long)BasePage.PAGE_SIZE; - var posEnd = posStart + BasePage.PAGE_SIZE; - - // position cursor - if (stream.Position != posStart) - { - stream.Seek(posStart, SeekOrigin.Begin); - } - - // write page header - page.WriteHeader(writer); - - // write content except for empty pages - if (page.PageType != PageType.Empty) - { - page.WriteContent(writer); - } - - // write with zero non-used page - writer.Write(new byte[posEnd - stream.Position]); - - // if page is dirty, clean up - page.IsDirty = false; - } - - /// - /// Pre-allocate more disk space to fast write new pages on disk - /// - public void AllocateDiskSpace(long length) - { - var writer = this.GetWriter(); - var stream = writer.BaseStream as FileStream; - - if (length > stream.Length) - { - stream.SetLength(length); - } - } - - /// - /// Get BinaryWriter - /// - private BinaryWriter GetWriter() - { - // If no writer - re-open file in Write Mode - if (_writer == null) - { - _reader.Close(); // Close reader - - var stream = new FileStream(_connectionString.Filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); - - _reader = new BinaryReader(stream); - _writer = new BinaryWriter(stream); - } - - return _writer; - } - - #region Lock/Unlock functions - - /// - /// Lock the datafile when start a begin transaction - /// - public void Lock() - { - var stream = this.GetWriter().BaseStream as FileStream; - - TryExec(_connectionString.Timeout, () => - { - // try to lock - if is in use, a exception will be throwed - stream.Lock(LOCK_POSITION, 1); - - }); - } - - /// - /// Unlock the datafile - /// - public void UnLock() - { - var stream = this.GetWriter().BaseStream as FileStream; - - stream.Unlock(LOCK_POSITION, 1); - } - - public void Flush() - { - this.GetWriter().BaseStream.Flush(); - } - - /// - /// Lock all file during write operations - avoid reads during inconsistence data - /// - public void ProtectWriteFile(Action action) - { - var stream = this.GetWriter().BaseStream as FileStream; - var fileLength = stream.Length; - - stream.Lock(LOCK_POSITION + 1, fileLength); - - action(); - - stream.Unlock(LOCK_POSITION + 1, fileLength); - } - - /// - /// Try execute a block of code until timeout when IO lock exception occurs OR access denind - /// - public static void TryExec(TimeSpan timeout, Action action) - { - var timer = DateTime.Now.Add(timeout); - - while (DateTime.Now < timer) - { - try - { - action(); - return; - } - catch (UnauthorizedAccessException) - { - Thread.Sleep(250); - } - catch (IOException ex) - { - ex.WaitIfLocked(250); - } - } - - throw LiteException.LockTimeout(timeout); - } - - #endregion - - public void Dispose() - { - _reader.Close(); - - if (_writer != null) - { - _writer.Close(); - } - } - } -} diff --git a/LiteDB/Storage/Services/JournalService.cs b/LiteDB/Storage/Services/JournalService.cs deleted file mode 100644 index 949f9d1eb..000000000 --- a/LiteDB/Storage/Services/JournalService.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace LiteDB -{ - /// - /// Service to create a journal file to garantee write operations will be atomic - /// - internal class JournalService - { - public const long FINISH_POSITION = BasePage.PAGE_SIZE - 1; // Last byte from header - - private CacheService _cache; - private ConnectionString _connectionString; - - public JournalService(ConnectionString connectionString, CacheService cache) - { - _connectionString = connectionString; - _cache = cache; - } - - public void CreateJournalFile(Action action) - { - if (!_connectionString.JournalEnabled) - { - action(); - return; - } - - FileStream journal = null; - - // try create journal file in EXCLUSIVE mode - DiskService.TryExec(_connectionString.Timeout, () => - { - journal = new FileStream(_connectionString.JournalFilename, - FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); - }); - - try - { - using (var writer = new BinaryWriter(journal)) - { - // first, allocate all journal file - var total = (uint)_cache.GetDirtyPages().Count(); - journal.SetLength(total * BasePage.PAGE_SIZE); - - // write all dirty pages in sequence on journal file - foreach (var page in _cache.GetDirtyPages()) - { - this.WritePageInJournal(writer, page); - } - - // flush all data - writer.Flush(); - - // mark header as finish - journal.Seek(FINISH_POSITION, SeekOrigin.Begin); - - writer.Write(true); // mark as TRUE - - // flush last finish mark - writer.Flush(); - - action(); - } - - journal.Dispose(); - - File.Delete(_connectionString.JournalFilename); - } - catch - { - journal.Dispose(); - } - } - - /// - /// Write a page in sequence, not in absolute position - /// - private void WritePageInJournal(BinaryWriter writer, BasePage page) - { - // no need position cursor - journal writes in sequence - var stream = writer.BaseStream; - var posStart = stream.Position; - var posEnd = posStart + BasePage.PAGE_SIZE; - - // Write page header - page.WriteHeader(writer); - - // write content except for empty pages - if (page.PageType != PageType.Empty) - { - page.WriteContent(writer); - } - - // write with zero non-used page - writer.Write(new byte[posEnd - stream.Position]); - } - } -} diff --git a/LiteDB/Storage/Services/RecoveryService.cs b/LiteDB/Storage/Services/RecoveryService.cs deleted file mode 100644 index d78fbe13d..000000000 --- a/LiteDB/Storage/Services/RecoveryService.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace LiteDB -{ - /// - /// Service for restore datafile with there a problem when save on disk - /// - internal class RecoveryService - { - private ConnectionString _connectionString; - - public RecoveryService(ConnectionString connectionString) - { - _connectionString = connectionString; - } - - public void TryRecovery() - { - // if I can open journal file, test FINISH_POSITION. If no journal, do not call action() - this.OpenExclusiveFile(_connectionString.JournalFilename, (stream) => - { - // check if FINISH_POSITON is true - using (var reader = new BinaryReader(stream)) - { - stream.Seek(JournalService.FINISH_POSITION, SeekOrigin.Begin); - - // if file is finish, datafile needs to be recovery. if not, - // the failure ocurrs during write journal file but not finish - just discard it - if (reader.ReadBoolean() == true) - { - this.DoRecovery(reader); - } - } - - // close stream for delete file - stream.Close(); - - File.Delete(_connectionString.JournalFilename); - }); - - // if I can't open, it's in use (and it's ok, there is a transaction executing in another process) - } - - private void DoRecovery(BinaryReader reader) - { - // open disk service - using (var disk = new DiskService(_connectionString)) - { - disk.Lock(); - - // while pages, read from redo, write on disk - while (reader.BaseStream.Position != reader.BaseStream.Length) - { - var page = this.ReadPageJournal(reader); - - disk.WritePage(page); - } - - reader.Close(); - - disk.UnLock(); - } - } - - private BasePage ReadPageJournal(BinaryReader reader) - { - var stream = reader.BaseStream; - var posStart = stream.Position * BasePage.PAGE_SIZE; - var posEnd = posStart + BasePage.PAGE_SIZE; - - // Create page instance and read from disk (read page header + content page) - var page = new BasePage(); - - // read page header - page.ReadHeader(reader); - - // Convert BasePage to correct Page Type - if (page.PageType == PageType.Header) page = page.CopyTo(); - else if (page.PageType == PageType.Collection) page = page.CopyTo(); - else if (page.PageType == PageType.Index) page = page.CopyTo(); - else if (page.PageType == PageType.Data) page = page.CopyTo(); - else if (page.PageType == PageType.Extend) page = page.CopyTo(); - - // read page content if page is not empty - if (page.PageType != PageType.Empty) - { - // read page content - page.ReadContent(reader); - } - - // read non-used bytes on page and position cursor to next page - reader.ReadBytes((int)(posEnd - stream.Position)); - - return page; - } - - private void OpenExclusiveFile(string filename, Action success) - { - // check if is using by another process, if not, call fn passing stream opened - try - { - using (var stream = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - { - success(stream); - } - } - catch (FileNotFoundException) - { - // do nothing - no journal, no recovery - } - catch (IOException ex) - { - ex.WaitIfLocked(0); - } - } - } -} diff --git a/LiteDB/Utils/ConnectionString.cs b/LiteDB/Utils/ConnectionString.cs deleted file mode 100644 index 463f47b78..000000000 --- a/LiteDB/Utils/ConnectionString.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.Configuration; -using System.Text.RegularExpressions; - -namespace LiteDB -{ - /// - /// Manage ConnectionString to connect and create databases. Can be used as: - /// * If only a word - get from App.Config - /// * If is a path - use all parameters as default - /// * Otherwise, is name=value collection - /// - public class ConnectionString - { - /// - /// Path of filename (no default - required key) - /// - public string Filename { get; private set; } - - /// - /// Default Timeout connection to wait for unlock (default: 1 minute) - /// - public TimeSpan Timeout { get; private set; } - - /// - /// Supports recovery mode if a fail during write pages to disk - /// - public bool JournalEnabled { get; private set; } - - /// - /// Jounal filename with full path - /// - internal string JournalFilename { get; private set; } - - /// - /// Define, in connection string, the user database version. When you increse this value - /// LiteDatabase will run OnUpdate method for each new version. If defined, must be >= 1. Default: 1 - /// - public int UserVersion { get; private set; } - - public ConnectionString(string connectionString) - { - if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString"); - - // If is only a name, get connectionString from App.config - if (Regex.IsMatch(connectionString, @"^[\w-]+$")) - connectionString = ConfigurationManager.ConnectionStrings[connectionString].ConnectionString; - - // Create a dictionary from string name=value collection - var values = new Dictionary(); - - if(connectionString.Contains("=")) - { - values = connectionString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) - .Select(t => t.Split(new char[] { '=' }, 2)) - .ToDictionary(t => t[0].Trim().ToLower(), t => t.Length == 1 ? "" : t[1].Trim(), StringComparer.InvariantCultureIgnoreCase); - } - else - { - // If connectionstring is only a filename, set filename - values["filename"] = Path.GetFullPath(connectionString); - } - - // Read connection string parameters with default value - this.Timeout = this.GetValue(values, "timeout", new TimeSpan(0, 1, 0)); - this.Filename = Path.GetFullPath(this.GetValue(values, "filename", "")); - this.JournalEnabled = this.GetValue(values, "journal", true); - this.UserVersion = this.GetValue(values, "version", 1); - this.JournalFilename = Path.Combine(Path.GetDirectoryName(this.Filename), - Path.GetFileNameWithoutExtension(this.Filename) + "-journal" + - Path.GetExtension(this.Filename)); - - // validade parameter values - if (string.IsNullOrEmpty(Filename)) throw new ArgumentException("Missing FileName in ConnectionString"); - if (this.UserVersion <= 0) throw new ArgumentException("Connection String version must be greater or equals to 1"); - } - - private T GetValue(Dictionary values, string key, T defaultValue) - { - return values.ContainsKey(key) ? - (T)Convert.ChangeType(values[key], typeof(T)) : - defaultValue; - } - } -} diff --git a/LiteDB/Utils/DumpDatabase.cs b/LiteDB/Utils/DumpDatabase.cs index 551a8359d..24a08c6aa 100644 --- a/LiteDB/Utils/DumpDatabase.cs +++ b/LiteDB/Utils/DumpDatabase.cs @@ -18,7 +18,7 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) sb.AppendLine("Dump - " + (mem ? "Cache/Disk" : "Only Disk")); - for (uint i = 0; i <= db.Cache.Header.LastPageID; i++) + for (uint i = 0; i <= db.Pager.GetPage(0).LastPageID; i++) { var p = ReadPage(db, i, mem); @@ -46,7 +46,7 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) private static T ReadPage(LiteDatabase db, uint pageID, bool mem) where T : BasePage, new() { - if (mem && pageID == 0) return (T)(object)db.Cache.Header; + if (mem && pageID == 0) return (T)(object)db.Cache.GetPage(0); return mem ? db.Pager.GetPage(pageID) : db.Disk.ReadPage(pageID); } diff --git a/UnitTest/VersionTest.cs b/UnitTest/VersionTest.cs deleted file mode 100644 index 05d005dbd..000000000 --- a/UnitTest/VersionTest.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; - -namespace UnitTest -{ - public class VersionDB : LiteDatabase - { - public VersionDB(string connectionString) - : base(connectionString) - { - } - - protected override void OnVersionUpdate(int newVersion) - { - if (newVersion == 1) - { - var col = this.GetCollection("customer"); - - // add 1 - col.Insert(new BsonDocument()); - } - else if (newVersion == 2) - { - var col = this.GetCollection("customer"); - - // add more 3 - col.Insert(new BsonDocument()); - col.Insert(new BsonDocument()); - col.Insert(new BsonDocument()); - } - } - } - - [TestClass] - public class VersionTest - { - [TestMethod] - public void Version_Test() - { - var dbf = DB.Path(); - var cs1 = "version=1; filename=" + dbf; - var cs2 = "version=2; filename=" + dbf; - - using (var db = new VersionDB(cs1)) - { - var col = db.GetCollection("customer"); - - // On initialize db, i insert a first row - Assert.AreEqual(1, col.Count()); - } - - using (var db = new VersionDB(cs2)) - { - var col = db.GetCollection("customer"); - - // And when update database to version 2, insert another - Assert.AreEqual(4, col.Count()); - } - } - } -} From 7b1bc4901a716a07db9a3171da17c0b9bbb5ab07 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 1 Nov 2015 20:25:02 -0200 Subject: [PATCH 02/91] Fix page start read --- LiteDB/DataStructure/Disks/FileDiskService.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 0f5b23cb5..3a53f4dcf 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -53,6 +53,12 @@ public T ReadPage(uint pageID) TryExec(() => { + // position cursor + if (_stream.Position != posStart) + { + _stream.Seek(posStart, SeekOrigin.Begin); + } + // read page header page.ReadHeader(_reader); From 81ebdc7abba50d174068230674257d949fef763a Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 1 Nov 2015 20:31:42 -0200 Subject: [PATCH 03/91] Update my todo --- LiteDB-TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 58b73a963..fbb87bf3a 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,7 +1,12 @@ # LiteDB v.2 - TODO +- Implementer SortDictionaryMRU - Implementar tamanho do BsonDocument - Implementar ByteReader/Writer + +- Remover transacao +- Implementar cache de 2 repos + - Converter page read/write para ByteR/W - Converter BsonSerializer para Byte R/W - Converter on-demand a serializacao no insert/update \ No newline at end of file From 9ef3fe98d0f7cda4d0ed55704fc24377d74fcd05 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 10:09:24 -0200 Subject: [PATCH 04/91] Removing user version --- LiteDB/DataStructure/Pages/HeaderPage.cs | 7 ------- LiteDB/Utils/DumpDatabase.cs | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/LiteDB/DataStructure/Pages/HeaderPage.cs b/LiteDB/DataStructure/Pages/HeaderPage.cs index a7efd80ee..d98f519b3 100644 --- a/LiteDB/DataStructure/Pages/HeaderPage.cs +++ b/LiteDB/DataStructure/Pages/HeaderPage.cs @@ -33,11 +33,6 @@ internal class HeaderPage : BasePage /// public uint LastPageID { get; set; } - /// - /// Get/Set a user version of database file - /// - public int UserVersion { get; set; } - /// /// Get/Set the first collection pageID (used as Field to be passed as reference) /// @@ -66,7 +61,6 @@ public override void ReadHeader(BinaryReader reader) this.FreeEmptyPageID = reader.ReadUInt32(); this.FirstCollectionPageID = reader.ReadUInt32(); this.LastPageID = reader.ReadUInt32(); - this.UserVersion = reader.ReadInt32(); if (info != HEADER_INFO) throw LiteException.InvalidDatabase(reader.BaseStream); if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(reader.BaseStream, ver); @@ -80,7 +74,6 @@ public override void WriteHeader(BinaryWriter writer) writer.Write(this.FreeEmptyPageID); writer.Write(this.FirstCollectionPageID); writer.Write(this.LastPageID); - writer.Write(this.UserVersion); } #endregion diff --git a/LiteDB/Utils/DumpDatabase.cs b/LiteDB/Utils/DumpDatabase.cs index 24a08c6aa..6c17db5c4 100644 --- a/LiteDB/Utils/DumpDatabase.cs +++ b/LiteDB/Utils/DumpDatabase.cs @@ -20,7 +20,9 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) for (uint i = 0; i <= db.Pager.GetPage(0).LastPageID; i++) { - var p = ReadPage(db, i, mem); + var p = i == 0 ? + ReadPage(db, i, mem) : + ReadPage(db, i, mem); sb.AppendFormat("{0} <{1},{2}> [{3}] {4}{5} | ", p.PageID.Dump(), @@ -30,7 +32,6 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) p.FreeBytes.ToString("0000"), p.IsDirty ? "d" : " "); - if (p.PageType == PageType.Header) p = ReadPage(db, i, mem); if (p.PageType == PageType.Collection) p = ReadPage(db, i, mem); if (p.PageType == PageType.Data) p = ReadPage(db, i, mem); if (p.PageType == PageType.Extend) p = ReadPage(db, i, mem); @@ -156,9 +157,8 @@ public static void Dump(this BasePage page, StringBuilder sb) public static void Dump(this HeaderPage page, StringBuilder sb) { - sb.AppendFormat("Change: {0}, Version: {1}, FreeEmptyPageID: {2}, LastPageID: {3}, ColID: {4}", + sb.AppendFormat("Change: {0}, FreeEmptyPageID: {1}, LastPageID: {2}, ColID: {3}", page.ChangeID, - page.UserVersion, page.FreeEmptyPageID.Dump(), page.LastPageID.Dump(), page.FirstCollectionPageID.Dump()); From 2c7ea5a936b7b583b0b4cccc62f38ddeeb92f215 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 18:24:20 -0200 Subject: [PATCH 05/91] ByteWriter and BsonWriter2 --- LiteDB-TODO.txt | 8 +- LiteDB.Tests/BsonPerfTest.cs | 44 +++++ LiteDB.Tests/LiteDB.Tests.csproj | 1 + LiteDB/DataStructure/Disks/FileDiskService.cs | 1 + LiteDB/DataStructure/Services/IndexService.cs | 4 +- LiteDB/Document/BsonValue.cs | 78 +++++---- LiteDB/LiteDB.csproj | 6 +- LiteDB/Serializer/Bson/BsonWriter.cs | 4 +- LiteDB/Serializer/Bson2/BsonReader2.cs | 154 ++++++++++++++++++ LiteDB/Serializer/Bson2/BsonSerializer2.cs | 43 +++++ LiteDB/Serializer/Bson2/BsonWriter2.cs | 139 ++++++++++++++++ LiteDB/Utils/ByteReader.cs | 145 +++++++++++++++++ LiteDB/Utils/ByteWriter.cs | 153 +++++++++++++++++ 13 files changed, 740 insertions(+), 40 deletions(-) create mode 100644 LiteDB.Tests/BsonPerfTest.cs create mode 100644 LiteDB/Serializer/Bson2/BsonReader2.cs create mode 100644 LiteDB/Serializer/Bson2/BsonSerializer2.cs create mode 100644 LiteDB/Serializer/Bson2/BsonWriter2.cs create mode 100644 LiteDB/Utils/ByteReader.cs create mode 100644 LiteDB/Utils/ByteWriter.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index fbb87bf3a..fa2779a5e 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -9,4 +9,10 @@ - Converter page read/write para ByteR/W - Converter BsonSerializer para Byte R/W -- Converter on-demand a serializacao no insert/update \ No newline at end of file +- Converter on-demand a serializacao no insert/update + +## A pensar: + +- Voltar user/version +- Voltar string connection +- A implementacaos dos multiplos discos ser apenas interna \ No newline at end of file diff --git a/LiteDB.Tests/BsonPerfTest.cs b/LiteDB.Tests/BsonPerfTest.cs new file mode 100644 index 000000000..1c679b338 --- /dev/null +++ b/LiteDB.Tests/BsonPerfTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; + +namespace UnitTest +{ + [TestClass] + public class BsonPerfTest + { + [TestMethod] + public void BsonWritePerf_Test() + { + // Usando a versão inicial, pra 20k + // time to convert - 1979732 + // Time to copy - 77853 + + var json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; + var o = JsonSerializer.Deserialize(json).AsDocument; + + var w1 = new Stopwatch(); + var w2 = new Stopwatch(); + + for(var i = 0; i < 20000; i++) + { + w1.Start(); + var bson1 = BsonSerializer.Serialize(o); + w1.Stop(); + + w2.Start(); + var bson2 = BsonSerializer2.Serialize(o); + w2.Stop(); + + } + + Debug.WriteLine("Time to convert 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to convert 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + + } + } +} diff --git a/LiteDB.Tests/LiteDB.Tests.csproj b/LiteDB.Tests/LiteDB.Tests.csproj index 495ff3795..7dde2ec46 100644 --- a/LiteDB.Tests/LiteDB.Tests.csproj +++ b/LiteDB.Tests/LiteDB.Tests.csproj @@ -56,6 +56,7 @@ + diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 3a53f4dcf..5ca3baf11 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -11,6 +11,7 @@ namespace LiteDB internal class FileDiskService : IDiskService { private const int LOCK_POSITION = 0; + private FileStream _stream; private BinaryReader _reader; private BinaryWriter _writer; diff --git a/LiteDB/DataStructure/Services/IndexService.cs b/LiteDB/DataStructure/Services/IndexService.cs index 0d7e39fe7..4cdfb4f90 100644 --- a/LiteDB/DataStructure/Services/IndexService.cs +++ b/LiteDB/DataStructure/Services/IndexService.cs @@ -41,7 +41,7 @@ public CollectionIndex CreateIndex(CollectionPage col) var head = new IndexNode(IndexNode.MAX_LEVEL_LENGTH) { Key = BsonValue.MinValue, - KeyLength = BsonValue.MinValue.GetBytesCount(), + KeyLength = (ushort)BsonValue.MinValue.GetBytesCount(false), Page = page, Position = new PageAddress(page.PageID, 0) }; @@ -87,7 +87,7 @@ private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) var node = new IndexNode(level) { Key = key, - KeyLength = key.GetBytesCount() + KeyLength = (ushort)key.GetBytesCount(false) }; if (node.KeyLength > MAX_INDEX_LENGTH) diff --git a/LiteDB/Document/BsonValue.cs b/LiteDB/Document/BsonValue.cs index 931e438cb..d7b4315f7 100644 --- a/LiteDB/Document/BsonValue.cs +++ b/LiteDB/Document/BsonValue.cs @@ -164,7 +164,10 @@ public BsonArray AsArray { if (this.IsArray) { - return new BsonArray((List)this.RawValue); + var array = new BsonArray((List)this.RawValue); + array.Length = this.Length; + + return array; } else { @@ -179,7 +182,10 @@ public BsonDocument AsDocument { if (this.IsDocument) { - return new BsonDocument((Dictionary)this.RawValue); + var doc = new BsonDocument((Dictionary)this.RawValue); + doc.Length = this.Length; + + return doc; } else { @@ -559,64 +565,68 @@ public override int GetHashCode() #endregion - #region GetBytesLength, Normalize + #region GetBytesCount, Normalize + + internal int? Length = null; /// /// Returns how many bytes this BsonValue will use to persist in index writes /// - internal ushort GetBytesCount() + public int GetBytesCount(bool recalc) { - var length = 0; + if (recalc == false && this.Length.HasValue) return this.Length.Value; switch (this.Type) { case BsonType.Null: case BsonType.MinValue: case BsonType.MaxValue: - length = 0; break; + this.Length = 0; break; - case BsonType.Int32: length = 4; break; - case BsonType.Int64: length = 8; break; - case BsonType.Double: length = 8; break; + case BsonType.Int32: this.Length = 4; break; + case BsonType.Int64: this.Length = 8; break; + case BsonType.Double: this.Length = 8; break; - case BsonType.String: length = Encoding.UTF8.GetByteCount((string)this.RawValue); break; + case BsonType.String: this.Length = Encoding.UTF8.GetByteCount((string)this.RawValue); break; - case BsonType.Binary: length = ((Byte[])this.RawValue).Length; break; - case BsonType.ObjectId: length = 12; break; - case BsonType.Guid: length = 16; break; + case BsonType.Binary: this.Length = ((Byte[])this.RawValue).Length; break; + case BsonType.ObjectId: this.Length = 12; break; + case BsonType.Guid: this.Length = 16; break; - case BsonType.Boolean: length = 1; break; - case BsonType.DateTime: length = 8; break; + case BsonType.Boolean: this.Length = 1; break; + case BsonType.DateTime: this.Length = 8; break; - // for Array/Document use BsonWriter + // for Array/Document calculate from elements case BsonType.Array: - using(var ma = new MemoryStream()) + var array = (List)this.RawValue; + this.Length = 5; // header + footer + for (var i = 0; i < array.Count; i++) { - using (var w = new BinaryWriter(ma)) - { - new BsonWriter() - .WriteArray(w, this.AsArray); - - length = (int)ma.Position; - } + this.Length += this.GetBytesCountElement(i.ToString(), array[i] ?? BsonValue.Null, recalc); } break; case BsonType.Document: - using(var md = new MemoryStream()) + var doc = (Dictionary)this.RawValue; + this.Length = 5; // header + footer + foreach (var key in doc.Keys) { - using (var w = new BinaryWriter(md)) - { - new BsonWriter() - .WriteDocument(w, this.AsDocument); - - length = (int)md.Position; - } + this.Length += this.GetBytesCountElement(key, doc[key] ?? BsonValue.Null, recalc); } break; } - // limits in ushort.MaxValue (store in 2 bytes only) - return (ushort)Math.Min(length, ushort.MaxValue); + return this.Length.Value; + } + + private int GetBytesCountElement(string key, BsonValue value, bool recalc) + { + return + 1 + // element type + Encoding.UTF8.GetByteCount(key) + // CString + 1 + // CString 0x00 + value.GetBytesCount(recalc) + + (value.Type == BsonType.String || value.Type == BsonType.Binary || value.Type == BsonType.Guid ? 5 : 0) // bytes.Length + 0x?? + ; } /// diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 217c0264d..3cdf03e05 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -21,7 +21,7 @@ TRACE;DEBUG prompt 4 - false + true bin\Debug\LiteDB.xml 1591 false @@ -52,6 +52,9 @@ + + + @@ -144,6 +147,7 @@ + diff --git a/LiteDB/Serializer/Bson/BsonWriter.cs b/LiteDB/Serializer/Bson/BsonWriter.cs index d4b6b1159..a59e42db7 100644 --- a/LiteDB/Serializer/Bson/BsonWriter.cs +++ b/LiteDB/Serializer/Bson/BsonWriter.cs @@ -115,7 +115,7 @@ internal void WriteDocument(BinaryWriter writer, BsonDocument doc) this.WriteElement(w, key, doc[key] ?? BsonValue.Null); } - writer.Write((Int32)mem.Position); + writer.Write((Int32)mem.Position + 4); writer.Write(mem.GetBuffer(), 0, (int)mem.Position); writer.Write((byte)0x00); } @@ -132,7 +132,7 @@ internal void WriteArray(BinaryWriter writer, BsonArray arr) this.WriteElement(w, i.ToString(), arr[i] ?? BsonValue.Null); } - writer.Write((Int32)mem.Position); + writer.Write((Int32)mem.Position + 4); writer.Write(mem.GetBuffer(), 0, (int)mem.Position); writer.Write((byte)0x00); } diff --git a/LiteDB/Serializer/Bson2/BsonReader2.cs b/LiteDB/Serializer/Bson2/BsonReader2.cs new file mode 100644 index 000000000..dffdf5bf4 --- /dev/null +++ b/LiteDB/Serializer/Bson2/BsonReader2.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace LiteDB +{ + internal class BsonReader2 + { + public BsonDocument Deserialize(Stream stream) + { + var reader = new BinaryReader(stream); + + return this.ReadDocument(reader); + } + + private BsonValue ReadElement(BinaryReader reader, out string name) + { + var type = reader.ReadByte(); + name = this.ReadCString(reader); + + if (type == 0x01) // Double + { + return reader.ReadDouble(); + } + else if (type == 0x02) // String + { + return this.ReadString(reader); + } + else if (type == 0x03) // Document + { + return this.ReadDocument(reader); + } + else if (type == 0x04) // Array + { + return this.ReadArray(reader); + } + else if (type == 0x05) // Binary + { + var length = reader.ReadInt32(); + var subType = reader.ReadByte(); + var bytes = reader.ReadBytes(length); + + switch (subType) + { + case 0x00: return bytes; + case 0x04: return new Guid(bytes); + } + } + else if (type == 0x07) // ObjectId + { + return new ObjectId(reader.ReadBytes(12)); + } + else if (type == 0x08) // Boolean + { + return reader.ReadBoolean(); + } + else if (type == 0x09) // DateTime + { + var ts = reader.ReadInt64(); + + // catch specific values for MaxValue / MinValue #19 + if (ts == 253402300800000) return DateTime.MaxValue; + if (ts == -62135596800000) return DateTime.MinValue; + + return BsonValue.UnixEpoch.AddMilliseconds(ts).ToLocalTime(); + } + else if (type == 0x0A) // Null + { + return BsonValue.Null; + } + else if (type == 0x10) // Int32 + { + return reader.ReadInt32(); + } + else if (type == 0x12) // Int64 + { + return reader.ReadInt64(); + } + else if (type == 0xFF) // MinKey + { + return BsonValue.MinValue; + } + else if (type == 0x7F) // MaxKey + { + return BsonValue.MaxValue; + } + + throw new NotSupportedException("BSON type not supported"); + } + + public BsonDocument ReadDocument(BinaryReader reader) + { + var length = reader.ReadInt32(); + var end = (int)reader.BaseStream.Position + length - 1; + var obj = new BsonDocument(); + + while (reader.BaseStream.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + obj.RawValue[name] = value; + } + + reader.ReadByte(); // zero + + return obj; + } + + public BsonArray ReadArray(BinaryReader reader) + { + var length = reader.ReadInt32(); + var end = (int)reader.BaseStream.Position + length - 1; + var arr = new BsonArray(); + + while (reader.BaseStream.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + arr.Add(value); + } + + reader.ReadByte(); // zero + + return arr; + } + + private string ReadString(BinaryReader reader) + { + var length = reader.ReadInt32(); + var bytes = reader.ReadBytes(length - 1); + reader.ReadByte(); // discard \x00 + return Encoding.UTF8.GetString(bytes); + } + + private string ReadCString(BinaryReader reader) + { + using (var ms = new MemoryStream()) + { + while (true) + { + byte buf = reader.ReadByte(); + if (buf == 0x00) break; + ms.WriteByte(buf); + } + + return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Position); + } + } + } +} diff --git a/LiteDB/Serializer/Bson2/BsonSerializer2.cs b/LiteDB/Serializer/Bson2/BsonSerializer2.cs new file mode 100644 index 000000000..4e7e3bd63 --- /dev/null +++ b/LiteDB/Serializer/Bson2/BsonSerializer2.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Text.RegularExpressions; + +namespace LiteDB +{ + /// + /// Class to call method for convert BsonDocument to/from byte[] - based on http://bsonspec.org/spec.html + /// + public class BsonSerializer2 + { + public static byte[] Serialize(BsonDocument value) + { + if (value == null) throw new ArgumentNullException("value"); + + var count = value.GetBytesCount(true); + var writer = new ByteWriter(count); + var bson = new BsonWriter2(); + + bson.WriteDocument(writer, value); + + return writer.Buffer; + } + + public static BsonDocument Deserialize(byte[] bson) + { + if (bson == null || bson.Length == 0) throw new ArgumentNullException("bson"); + + using (var mem = new MemoryStream(bson)) + { + var reader = new BsonReader(); + + return reader.Deserialize(mem); + } + } + } +} diff --git a/LiteDB/Serializer/Bson2/BsonWriter2.cs b/LiteDB/Serializer/Bson2/BsonWriter2.cs new file mode 100644 index 000000000..e1d73f54b --- /dev/null +++ b/LiteDB/Serializer/Bson2/BsonWriter2.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal class BsonWriter2 + { + /// + /// Write a bson document + /// + public void WriteDocument(ByteWriter writer, BsonDocument doc) + { + writer.Write(doc.GetBytesCount(false)); + + foreach (var key in doc.Keys) + { + this.WriteElement(writer, key, doc[key] ?? BsonValue.Null); + } + + writer.Write((byte)0x00); + } + + private void WriteElement(ByteWriter writer, string key, BsonValue value) + { + // cast RawValue to avoid one if on As + switch (value.Type) + { + case BsonType.Double: + writer.Write((byte)0x01); + this.WriteCString(writer, key); + writer.Write((Double)value.RawValue); + break; + case BsonType.String: + writer.Write((byte)0x02); + this.WriteCString(writer, key); + this.WriteString(writer, (String)value.RawValue); + break; + case BsonType.Document: + writer.Write((byte)0x03); + this.WriteCString(writer, key); + this.WriteDocument(writer, new BsonDocument((Dictionary)value.RawValue)); + break; + case BsonType.Array: + writer.Write((byte)0x04); + this.WriteCString(writer, key); + this.WriteArray(writer, new BsonArray((List)value.RawValue)); + break; + case BsonType.Binary: + writer.Write((byte)0x05); + this.WriteCString(writer, key); + var bytes = (byte[])value.RawValue; + writer.Write(bytes.Length); + writer.Write((byte)0x00); // subtype 00 - Generic binary subtype + writer.Write(bytes); + break; + case BsonType.Guid: + writer.Write((byte)0x05); + this.WriteCString(writer, key); + var guid = ((Guid)value.RawValue).ToByteArray(); + writer.Write(guid.Length); + writer.Write((byte)0x04); // UUID + writer.Write(guid); + break; + case BsonType.ObjectId: + writer.Write((byte)0x07); + this.WriteCString(writer, key); + writer.Write(((ObjectId)value.RawValue).ToByteArray()); + break; + case BsonType.Boolean: + writer.Write((byte)0x08); + this.WriteCString(writer, key); + writer.Write((byte)(((Boolean)value.RawValue) ? 0x01 : 0x00)); + break; + case BsonType.DateTime: + writer.Write((byte)0x09); + this.WriteCString(writer, key); + var date = (DateTime)value.RawValue; + // do not convert to UTC min/max date values - #19 + var utc = (date == DateTime.MinValue || date == DateTime.MaxValue) ? date : date.ToUniversalTime(); + var ts = utc - BsonValue.UnixEpoch; + writer.Write(Convert.ToInt64(ts.TotalMilliseconds)); + break; + case BsonType.Null: + writer.Write((byte)0x0A); + this.WriteCString(writer, key); + break; + case BsonType.Int32: + writer.Write((byte)0x10); + this.WriteCString(writer, key); + writer.Write((Int32)value.RawValue); + break; + case BsonType.Int64: + writer.Write((byte)0x12); + this.WriteCString(writer, key); + writer.Write((Int64)value.RawValue); + break; + case BsonType.MinValue: + writer.Write((byte)0xFF); + this.WriteCString(writer, key); + break; + case BsonType.MaxValue: + writer.Write((byte)0x7F); + this.WriteCString(writer, key); + break; + } + } + + private void WriteArray(ByteWriter writer, BsonArray array) + { + writer.Write(array.GetBytesCount(false)); + + for (var i = 0; i < array.Count; i++) + { + this.WriteElement(writer, i.ToString(), array[i] ?? BsonValue.Null); + } + + writer.Write((byte)0x00); + } + + private void WriteString(ByteWriter writer, string s) + { + var bytes = Encoding.UTF8.GetBytes(s); + writer.Write(bytes.Length + 1); + writer.Write(bytes); + writer.Write((byte)0x00); + } + + private void WriteCString(ByteWriter writer, string s) + { + var bytes = Encoding.UTF8.GetBytes(s); + writer.Write(bytes); + writer.Write((byte)0x00); + } + } +} diff --git a/LiteDB/Utils/ByteReader.cs b/LiteDB/Utils/ByteReader.cs new file mode 100644 index 000000000..5757e0264 --- /dev/null +++ b/LiteDB/Utils/ByteReader.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace LiteDB +{ + public unsafe class ByteReader + { + private byte[] _buffer; + private int _index; + + public ByteReader(byte[] buffer) + { + _buffer = buffer; + _index = 0; + } + + public void Write(byte value) + { + _buffer[_index] = value; + + _index++; + } + + public void Write(bool value) + { + _buffer[_index] = value ? (byte)1 : (byte)1; + + _index++; + } + + public void Write(ushort value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + + _index += 2; + } + + public void Write(uint value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(ulong value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(short value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + + _index += 2; + } + + public void Write(int value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(long value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(float value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(double value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(byte[] value) + { + System.Buffer.BlockCopy(value, 0, _buffer, _index, value.Length); + + _index += value.Length; + } + } +} \ No newline at end of file diff --git a/LiteDB/Utils/ByteWriter.cs b/LiteDB/Utils/ByteWriter.cs new file mode 100644 index 000000000..d29119063 --- /dev/null +++ b/LiteDB/Utils/ByteWriter.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace LiteDB +{ + public unsafe class ByteWriter + { + private byte[] _buffer; + private int _index; + + public byte[] Buffer { get { return _buffer; } } + + public ByteWriter(int length) + { + _buffer = new byte[length]; + _index = 0; + } + + public ByteWriter(byte[] buffer) + { + _buffer = buffer; + _index = 0; + } + + public void Write(byte value) + { + _buffer[_index] = value; + + _index++; + } + + public void Write(bool value) + { + _buffer[_index] = value ? (byte)1 : (byte)1; + + _index++; + } + + public void Write(ushort value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + + _index += 2; + } + + public void Write(uint value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(ulong value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(short value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + + _index += 2; + } + + public void Write(int value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(long value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(float value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + + _index += 4; + } + + public void Write(double value) + { + byte* pi = (byte*)&value; + + _buffer[_index + 0] = pi[0]; + _buffer[_index + 1] = pi[1]; + _buffer[_index + 2] = pi[2]; + _buffer[_index + 3] = pi[3]; + _buffer[_index + 4] = pi[4]; + _buffer[_index + 5] = pi[5]; + _buffer[_index + 6] = pi[6]; + _buffer[_index + 7] = pi[7]; + + _index += 8; + } + + public void Write(byte[] value) + { + System.Buffer.BlockCopy(value, 0, _buffer, _index, value.Length); + + _index += value.Length; + } + } +} \ No newline at end of file From c12c60cb941c791cc84201028c613676997d71f0 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 19:45:37 -0200 Subject: [PATCH 06/91] BsonReader2 and ByteReader --- LiteDB-TODO.txt | 11 +- LiteDB.Tests/BsonPerfTest.cs | 43 +++++- LiteDB/LiteDB.csproj | 1 + LiteDB/Serializer/Bson/BsonWriter.cs | 4 +- LiteDB/Serializer/Bson2/BsonReader2.cs | 117 ++++++++------- LiteDB/Serializer/Bson2/BsonSerializer2.cs | 19 +-- LiteDB/Serializer/Bson2/BsonWriter2.cs | 40 +++-- LiteDB/Utils/ByteReader.cs | 165 +++++++++------------ LiteDB/Utils/ByteWriter.cs | 143 +++++++++--------- 9 files changed, 297 insertions(+), 246 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index fa2779a5e..15723375c 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -15,4 +15,13 @@ - Voltar user/version - Voltar string connection -- A implementacaos dos multiplos discos ser apenas interna \ No newline at end of file +- A implementacaos dos multiplos discos ser apenas interna + +var w = new ByteWriter(); + +w.OnGetBytes((pos) => new byte[4000]) + + +Time to convert 1975687 +Time to copy 77554 + diff --git a/LiteDB.Tests/BsonPerfTest.cs b/LiteDB.Tests/BsonPerfTest.cs index 1c679b338..a695244bc 100644 --- a/LiteDB.Tests/BsonPerfTest.cs +++ b/LiteDB.Tests/BsonPerfTest.cs @@ -11,14 +11,11 @@ namespace UnitTest [TestClass] public class BsonPerfTest { + private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; + [TestMethod] public void BsonWritePerf_Test() { - // Usando a versão inicial, pra 20k - // time to convert - 1979732 - // Time to copy - 77853 - - var json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; var o = JsonSerializer.Deserialize(json).AsDocument; var w1 = new Stopwatch(); @@ -36,9 +33,39 @@ public void BsonWritePerf_Test() } - Debug.WriteLine("Time to convert 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); - Debug.WriteLine("Time to convert 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to write 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to write 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + + } + + [TestMethod] + public void BsonReaderPerf_Test() + { + var bytes1 = BsonSerializer.Serialize(JsonSerializer.Deserialize(json).AsDocument); + var bytes2 = BsonSerializer2.Serialize(JsonSerializer.Deserialize(json).AsDocument); + + var w1 = new Stopwatch(); + var w2 = new Stopwatch(); + + for (var i = 0; i < 1000; i++) + { + w1.Start(); + var doc1 = BsonSerializer.Deserialize(bytes1); + w1.Stop(); + + w2.Start(); + var doc2 = BsonSerializer2.Deserialize(bytes2); + w2.Stop(); + + Assert.AreEqual(doc1.ToString(), doc2.ToString()); + } + + + + Debug.WriteLine("Time to read 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to read 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + } } -} +} \ No newline at end of file diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 3cdf03e05..6021800db 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -147,6 +147,7 @@ + diff --git a/LiteDB/Serializer/Bson/BsonWriter.cs b/LiteDB/Serializer/Bson/BsonWriter.cs index a59e42db7..d4b6b1159 100644 --- a/LiteDB/Serializer/Bson/BsonWriter.cs +++ b/LiteDB/Serializer/Bson/BsonWriter.cs @@ -115,7 +115,7 @@ internal void WriteDocument(BinaryWriter writer, BsonDocument doc) this.WriteElement(w, key, doc[key] ?? BsonValue.Null); } - writer.Write((Int32)mem.Position + 4); + writer.Write((Int32)mem.Position); writer.Write(mem.GetBuffer(), 0, (int)mem.Position); writer.Write((byte)0x00); } @@ -132,7 +132,7 @@ internal void WriteArray(BinaryWriter writer, BsonArray arr) this.WriteElement(w, i.ToString(), arr[i] ?? BsonValue.Null); } - writer.Write((Int32)mem.Position + 4); + writer.Write((Int32)mem.Position); writer.Write(mem.GetBuffer(), 0, (int)mem.Position); writer.Write((byte)0x00); } diff --git a/LiteDB/Serializer/Bson2/BsonReader2.cs b/LiteDB/Serializer/Bson2/BsonReader2.cs index dffdf5bf4..3de8b0e3b 100644 --- a/LiteDB/Serializer/Bson2/BsonReader2.cs +++ b/LiteDB/Serializer/Bson2/BsonReader2.cs @@ -8,16 +8,65 @@ namespace LiteDB { + /// + /// Internal class to deserialize a byte[] into a BsonDocument using BSON data format + /// internal class BsonReader2 { - public BsonDocument Deserialize(Stream stream) + /// + /// Main method - deserialize using ByteReader helper + /// + public BsonDocument Deserialize(byte[] bson) { - var reader = new BinaryReader(stream); + return this.ReadDocument(new ByteReader(bson)); + } + + /// + /// Read a BsonDocument from reader + /// + private BsonDocument ReadDocument(ByteReader reader) + { + var length = reader.ReadInt32(); + var end = reader.Position + length - 5; + var obj = new BsonDocument(); - return this.ReadDocument(reader); + while (reader.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + obj.RawValue[name] = value; + } + + reader.ReadByte(); // zero + + return obj; } - private BsonValue ReadElement(BinaryReader reader, out string name) + /// + /// Read an BsonArray from reader + /// + private BsonArray ReadArray(ByteReader reader) + { + var length = reader.ReadInt32(); + var end = reader.Position + length - 5; + var arr = new BsonArray(); + + while (reader.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + arr.Add(value); + } + + reader.ReadByte(); // zero + + return arr; + } + + /// + /// Reads an element (key-value) from an reader + /// + private BsonValue ReadElement(ByteReader reader, out string name) { var type = reader.ReadByte(); name = this.ReadCString(reader); @@ -92,43 +141,7 @@ private BsonValue ReadElement(BinaryReader reader, out string name) throw new NotSupportedException("BSON type not supported"); } - public BsonDocument ReadDocument(BinaryReader reader) - { - var length = reader.ReadInt32(); - var end = (int)reader.BaseStream.Position + length - 1; - var obj = new BsonDocument(); - - while (reader.BaseStream.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - obj.RawValue[name] = value; - } - - reader.ReadByte(); // zero - - return obj; - } - - public BsonArray ReadArray(BinaryReader reader) - { - var length = reader.ReadInt32(); - var end = (int)reader.BaseStream.Position + length - 1; - var arr = new BsonArray(); - - while (reader.BaseStream.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - arr.Add(value); - } - - reader.ReadByte(); // zero - - return arr; - } - - private string ReadString(BinaryReader reader) + private string ReadString(ByteReader reader) { var length = reader.ReadInt32(); var bytes = reader.ReadBytes(length - 1); @@ -136,19 +149,21 @@ private string ReadString(BinaryReader reader) return Encoding.UTF8.GetString(bytes); } - private string ReadCString(BinaryReader reader) + // use byte array buffer for CString (key-only) + private byte[] _strBuffer = new byte[1000]; + + private string ReadCString(ByteReader reader) { - using (var ms = new MemoryStream()) - { - while (true) - { - byte buf = reader.ReadByte(); - if (buf == 0x00) break; - ms.WriteByte(buf); - } + var pos = 0; - return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Position); + while (true) + { + byte buf = reader.ReadByte(); + if (buf == 0x00) break; + _strBuffer[pos++] = buf; } + + return Encoding.UTF8.GetString(_strBuffer, 0, pos); } } } diff --git a/LiteDB/Serializer/Bson2/BsonSerializer2.cs b/LiteDB/Serializer/Bson2/BsonSerializer2.cs index 4e7e3bd63..930a43974 100644 --- a/LiteDB/Serializer/Bson2/BsonSerializer2.cs +++ b/LiteDB/Serializer/Bson2/BsonSerializer2.cs @@ -15,29 +15,22 @@ namespace LiteDB /// public class BsonSerializer2 { - public static byte[] Serialize(BsonDocument value) + public static byte[] Serialize(BsonDocument doc) { - if (value == null) throw new ArgumentNullException("value"); + if (doc == null) throw new ArgumentNullException("doc"); - var count = value.GetBytesCount(true); - var writer = new ByteWriter(count); - var bson = new BsonWriter2(); + var writer = new BsonWriter2(); - bson.WriteDocument(writer, value); - - return writer.Buffer; + return writer.Serialize(doc); } public static BsonDocument Deserialize(byte[] bson) { if (bson == null || bson.Length == 0) throw new ArgumentNullException("bson"); - using (var mem = new MemoryStream(bson)) - { - var reader = new BsonReader(); + var reader = new BsonReader2(); - return reader.Deserialize(mem); - } + return reader.Deserialize(bson); } } } diff --git a/LiteDB/Serializer/Bson2/BsonWriter2.cs b/LiteDB/Serializer/Bson2/BsonWriter2.cs index e1d73f54b..b46076389 100644 --- a/LiteDB/Serializer/Bson2/BsonWriter2.cs +++ b/LiteDB/Serializer/Bson2/BsonWriter2.cs @@ -7,8 +7,24 @@ namespace LiteDB { + /// + /// Internal class to serialize a BsonDocument to BSON data format (byte[]) + /// internal class BsonWriter2 { + /// + /// Main method - serialize document. Uses ByteWriter + /// + public byte[] Serialize(BsonDocument doc) + { + var count = doc.GetBytesCount(true); + var writer = new ByteWriter(count); + + this.WriteDocument(writer, doc); + + return writer.Buffer; + } + /// /// Write a bson document /// @@ -24,6 +40,18 @@ public void WriteDocument(ByteWriter writer, BsonDocument doc) writer.Write((byte)0x00); } + private void WriteArray(ByteWriter writer, BsonArray array) + { + writer.Write(array.GetBytesCount(false)); + + for (var i = 0; i < array.Count; i++) + { + this.WriteElement(writer, i.ToString(), array[i] ?? BsonValue.Null); + } + + writer.Write((byte)0x00); + } + private void WriteElement(ByteWriter writer, string key, BsonValue value) { // cast RawValue to avoid one if on As @@ -109,18 +137,6 @@ private void WriteElement(ByteWriter writer, string key, BsonValue value) } } - private void WriteArray(ByteWriter writer, BsonArray array) - { - writer.Write(array.GetBytesCount(false)); - - for (var i = 0; i < array.Count; i++) - { - this.WriteElement(writer, i.ToString(), array[i] ?? BsonValue.Null); - } - - writer.Write((byte)0x00); - } - private void WriteString(ByteWriter writer, string s) { var bytes = Encoding.UTF8.GetBytes(s); diff --git a/LiteDB/Utils/ByteReader.cs b/LiteDB/Utils/ByteReader.cs index 5757e0264..dbcc37cbf 100644 --- a/LiteDB/Utils/ByteReader.cs +++ b/LiteDB/Utils/ByteReader.cs @@ -9,137 +9,120 @@ namespace LiteDB public unsafe class ByteReader { private byte[] _buffer; - private int _index; + private int _pos; + + public int Position { get { return _pos; } } public ByteReader(byte[] buffer) { _buffer = buffer; - _index = 0; + _pos = 0; } - public void Write(byte value) - { - _buffer[_index] = value; + #region Native data types - _index++; - } - - public void Write(bool value) + public Byte ReadByte() { - _buffer[_index] = value ? (byte)1 : (byte)1; + var value = _buffer[_pos]; + + _pos++; - _index++; + return value; } - public void Write(ushort value) + public Boolean ReadBoolean() { - byte* pi = (byte*)&value; + var value = _buffer[_pos]; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; + _pos++; - _index += 2; + return value == 0 ? false : true; } - public void Write(uint value) + public UInt16 ReadUInt16() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - - _index += 4; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 2; + return *(((UInt16*)numRef)); + } } - public void Write(ulong value) + public UInt32 ReadUInt32() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; - - _index += 8; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 4; + return *(((UInt32*)numRef)); + } } - public void Write(short value) + public UInt64 ReadUInt64() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - - _index += 2; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 8; + return *(((UInt64*)numRef)); + } } - public void Write(int value) + public Int16 ReadInt16() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - - _index += 4; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 2; + return *(((Int16*)numRef)); + } } - public void Write(long value) + public Int32 ReadInt32() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; - - _index += 8; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 4; + return *(((Int32*)numRef)); + } } - public void Write(float value) + public Int64 ReadInt64() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 8; + return *(((Int64*)numRef)); + } + } - _index += 4; + public Single ReadSingle() + { + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 4; + return *(((Single*)numRef)); + } } - public void Write(double value) + public Double ReadDouble() { - byte* pi = (byte*)&value; - - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; - - _index += 8; + fixed (byte* numRef = &(_buffer[_pos])) + { + _pos += 8; + return *(((Double*)numRef)); + } } - public void Write(byte[] value) + public Byte[] ReadBytes(int count) { - System.Buffer.BlockCopy(value, 0, _buffer, _index, value.Length); + var buffer = new byte[count]; - _index += value.Length; + System.Buffer.BlockCopy(_buffer, _pos, buffer, 0, count); + + _pos += count; + + return buffer; } + + #endregion + } } \ No newline at end of file diff --git a/LiteDB/Utils/ByteWriter.cs b/LiteDB/Utils/ByteWriter.cs index d29119063..06521b8a8 100644 --- a/LiteDB/Utils/ByteWriter.cs +++ b/LiteDB/Utils/ByteWriter.cs @@ -9,145 +9,152 @@ namespace LiteDB public unsafe class ByteWriter { private byte[] _buffer; - private int _index; + private int _pos; public byte[] Buffer { get { return _buffer; } } + public int Position { get { return _pos; } } + public ByteWriter(int length) { _buffer = new byte[length]; - _index = 0; + _pos = 0; } public ByteWriter(byte[] buffer) { _buffer = buffer; - _index = 0; + _pos = 0; } - public void Write(byte value) + #region Native data types + + public void Write(Byte value) { - _buffer[_index] = value; + _buffer[_pos] = value; - _index++; + _pos++; } - public void Write(bool value) + public void Write(Boolean value) { - _buffer[_index] = value ? (byte)1 : (byte)1; + _buffer[_pos] = value ? (byte)1 : (byte)1; - _index++; + _pos++; } - public void Write(ushort value) + public void Write(UInt16 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; - _index += 2; + _pos += 2; } - public void Write(uint value) + public void Write(UInt32 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; - _index += 4; + _pos += 4; } - public void Write(ulong value) + public void Write(UInt64 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; + _buffer[_pos + 4] = pi[4]; + _buffer[_pos + 5] = pi[5]; + _buffer[_pos + 6] = pi[6]; + _buffer[_pos + 7] = pi[7]; - _index += 8; + _pos += 8; } - public void Write(short value) + public void Write(Int16 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; - _index += 2; + _pos += 2; } - public void Write(int value) + public void Write(Int32 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; - _index += 4; + _pos += 4; } - public void Write(long value) + public void Write(Int64 value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; + _buffer[_pos + 4] = pi[4]; + _buffer[_pos + 5] = pi[5]; + _buffer[_pos + 6] = pi[6]; + _buffer[_pos + 7] = pi[7]; - _index += 8; + _pos += 8; } - public void Write(float value) + public void Write(Single value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; - _index += 4; + _pos += 4; } - public void Write(double value) + public void Write(Double value) { byte* pi = (byte*)&value; - _buffer[_index + 0] = pi[0]; - _buffer[_index + 1] = pi[1]; - _buffer[_index + 2] = pi[2]; - _buffer[_index + 3] = pi[3]; - _buffer[_index + 4] = pi[4]; - _buffer[_index + 5] = pi[5]; - _buffer[_index + 6] = pi[6]; - _buffer[_index + 7] = pi[7]; + _buffer[_pos + 0] = pi[0]; + _buffer[_pos + 1] = pi[1]; + _buffer[_pos + 2] = pi[2]; + _buffer[_pos + 3] = pi[3]; + _buffer[_pos + 4] = pi[4]; + _buffer[_pos + 5] = pi[5]; + _buffer[_pos + 6] = pi[6]; + _buffer[_pos + 7] = pi[7]; - _index += 8; + _pos += 8; } - public void Write(byte[] value) + public void Write(Byte[] value) { - System.Buffer.BlockCopy(value, 0, _buffer, _index, value.Length); + System.Buffer.BlockCopy(value, 0, _buffer, _pos, value.Length); - _index += value.Length; + _pos += value.Length; } + + #endregion + } } \ No newline at end of file From 6d96033a198a75892657dfdae12f0ef16e8609e2 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 21:05:41 -0200 Subject: [PATCH 07/91] Pages using ByteReader/Writer --- LiteDB/DataStructure/Disks/FileDiskService.cs | 103 +++++++----------- LiteDB/DataStructure/Disks/IDiskService.cs | 4 +- LiteDB/DataStructure/Pages/BasePage.cs | 34 +++++- LiteDB/DataStructure/Pages/CollectionPage.cs | 4 +- LiteDB/DataStructure/Pages/DataPage.cs | 4 +- LiteDB/DataStructure/Pages/ExtendPage.cs | 4 +- LiteDB/DataStructure/Pages/HeaderPage.cs | 12 +- LiteDB/DataStructure/Pages/IndexPage.cs | 4 +- LiteDB/DataStructure/Services/PageService.cs | 4 +- .../Services/TransactionService.cs | 12 +- LiteDB/Utils/ByteReader.cs | 70 +++++++++++- LiteDB/Utils/ByteWriter.cs | 72 +++++++++++- LiteDB/Utils/DumpDatabase.cs | 11 +- LiteDB/Utils/LiteException.cs | 8 +- 14 files changed, 249 insertions(+), 97 deletions(-) diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 5ca3baf11..838d0bc66 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -13,116 +13,88 @@ internal class FileDiskService : IDiskService private const int LOCK_POSITION = 0; private FileStream _stream; - private BinaryReader _reader; - private BinaryWriter _writer; private string _filename; + private byte[] _buffer = new byte[BasePage.PAGE_SIZE]; + public FileDiskService(string filename) { _filename = filename; } + /// + /// Open datafile - if not exits, create a new one + /// public void Initialize() { // open file as readOnly - if we need use Write, re-open in Write Mode _stream = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite, BasePage.PAGE_SIZE); - _reader = new BinaryReader(_stream); - if (_stream.Length == 0) { - this.WritePages(new HeaderPage[] { new HeaderPage() }); + this.WritePage(0, new HeaderPage().WritePage()); } } + /// + /// Lock datafile agains other process read/write + /// public void Lock() { TryExec(() => _stream.Lock(LOCK_POSITION, 1)); } + /// + /// Release lock + /// public void Unlock() { _stream.Unlock(LOCK_POSITION, 1); } - public T ReadPage(uint pageID) - where T : BasePage, new() + /// + /// Read page bytes from disk + /// + public byte[] ReadPage(uint pageID) { - var page = new T(); - var posStart = (long)pageID * (long)BasePage.PAGE_SIZE; - var posEnd = posStart + BasePage.PAGE_SIZE; + var position = (long)pageID * (long)BasePage.PAGE_SIZE; TryExec(() => { // position cursor - if (_stream.Position != posStart) + if (_stream.Position != position) { - _stream.Seek(posStart, SeekOrigin.Begin); + _stream.Seek(position, SeekOrigin.Begin); } - // read page header - page.ReadHeader(_reader); - - // if T is base and PageType has a defined type, convert page - var isBase = page.GetType() == typeof(BasePage); - - if (isBase) - { - if (page.PageType == PageType.Index) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Data) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Extend) page = (T)(object)page.CopyTo(); - else if (page.PageType == PageType.Collection) page = (T)(object)page.CopyTo(); - } + // read bytes from data file + _stream.Read(_buffer, 0, BasePage.PAGE_SIZE); - // read page content if page is not empty - if (page.PageType != PageType.Empty) - { - page.ReadContent(_reader); - } - - // position cursor at starts next page - _reader.ReadBytes((int)(posEnd - _stream.Position)); }); - return page; + return _buffer; } - public void WritePages(IEnumerable pages) + /// + /// Persist single page bytes to disk + /// + public void WritePage(uint pageID, byte[] buffer) { - if(_writer == null) + if(_stream.CanWrite == false) { - _reader.Dispose(); _stream.Dispose(); _stream = new FileStream(_filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); - _reader = new BinaryReader(_stream); - _writer = new BinaryWriter(_stream); } - foreach(var page in pages) - { - var posStart = (long)page.PageID * (long)BasePage.PAGE_SIZE; - var posEnd = posStart + BasePage.PAGE_SIZE; + var position = (long)pageID * (long)BasePage.PAGE_SIZE; - // position cursor - if (_stream.Position != posStart) - { - _stream.Seek(posStart, SeekOrigin.Begin); - } - - // write page header - page.WriteHeader(_writer); - - // write content except for empty pages - if (page.PageType != PageType.Empty) - { - page.WriteContent(_writer); - } - - // write with zero non-used page - _writer.Write(new byte[posEnd - _stream.Position]); - - page.IsDirty = false; + // position cursor + if (_stream.Position != position) + { + _stream.Seek(position, SeekOrigin.Begin); } + + _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } public void Dispose() @@ -130,12 +102,13 @@ public void Dispose() if(_stream != null) { _stream.Dispose(); - _reader.Close(); - if(_writer != null) _writer.Dispose(); } } - public static void TryExec(Action action) + /// + /// Try run an operation over datafile - keep tring if locked + /// + private static void TryExec(Action action) { var timeout = new TimeSpan(0, 1, 0); var timer = DateTime.Now.Add(timeout); diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs index 0e5eeb897..0a2982469 100644 --- a/LiteDB/DataStructure/Disks/IDiskService.cs +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -13,7 +13,7 @@ internal interface IDiskService : IDisposable void Initialize(); void Lock(); void Unlock(); - T ReadPage(uint pageID) where T : BasePage, new(); - void WritePages(IEnumerable pages); + byte[] ReadPage(uint pageID); + void WritePage(uint pageID, byte[] buffer); } } diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index 0c4ad9cfb..7ecb63dfa 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -118,7 +118,7 @@ public T CopyTo() #region Read/Write page - public virtual void ReadHeader(BinaryReader reader) + public virtual void ReadHeader(ByteReader reader) { this.PageID = reader.ReadUInt32(); this.PrevPageID = reader.ReadUInt32(); @@ -128,7 +128,7 @@ public virtual void ReadHeader(BinaryReader reader) this.FreeBytes = reader.ReadUInt16(); } - public virtual void WriteHeader(BinaryWriter writer) + public virtual void WriteHeader(ByteWriter writer) { writer.Write(this.PageID); writer.Write(this.PrevPageID); @@ -138,14 +138,40 @@ public virtual void WriteHeader(BinaryWriter writer) writer.Write((UInt16)this.FreeBytes); } - public virtual void ReadContent(BinaryReader reader) + public virtual void ReadContent(ByteReader reader) { } - public virtual void WriteContent(BinaryWriter writer) + public virtual void WriteContent(ByteWriter writer) { } + public void ReadPage(byte[] buffer) + { + var reader = new ByteReader(buffer); + + this.ReadHeader(reader); + + if (this.PageType != LiteDB.PageType.Empty) + { + this.ReadContent(reader); + } + } + + public byte[] WritePage() + { + var writer = new ByteWriter(BasePage.PAGE_SIZE); + + WriteHeader(writer); + + if (this.PageType != LiteDB.PageType.Empty) + { + WriteContent(writer); + } + + return writer.Buffer; + } + #endregion } } diff --git a/LiteDB/DataStructure/Pages/CollectionPage.cs b/LiteDB/DataStructure/Pages/CollectionPage.cs index 1062cfe2d..5a6831842 100644 --- a/LiteDB/DataStructure/Pages/CollectionPage.cs +++ b/LiteDB/DataStructure/Pages/CollectionPage.cs @@ -55,7 +55,7 @@ public CollectionPage() #region Read/Write pages - public override void ReadContent(BinaryReader reader) + public override void ReadContent(ByteReader reader) { this.CollectionName = reader.ReadString(); this.FreeDataPageID = reader.ReadUInt32(); @@ -75,7 +75,7 @@ public override void ReadContent(BinaryReader reader) } } - public override void WriteContent(BinaryWriter writer) + public override void WriteContent(ByteWriter writer) { writer.Write(this.CollectionName); writer.Write(this.FreeDataPageID); diff --git a/LiteDB/DataStructure/Pages/DataPage.cs b/LiteDB/DataStructure/Pages/DataPage.cs index 8f86bfd27..6b986e2de 100644 --- a/LiteDB/DataStructure/Pages/DataPage.cs +++ b/LiteDB/DataStructure/Pages/DataPage.cs @@ -49,7 +49,7 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(BinaryReader reader) + public override void ReadContent(ByteReader reader) { this.DataBlocks = new Dictionary(ItemCount); @@ -73,7 +73,7 @@ public override void ReadContent(BinaryReader reader) } } - public override void WriteContent(BinaryWriter writer) + public override void WriteContent(ByteWriter writer) { foreach (var block in this.DataBlocks.Values) { diff --git a/LiteDB/DataStructure/Pages/ExtendPage.cs b/LiteDB/DataStructure/Pages/ExtendPage.cs index ff819c50b..192d995a9 100644 --- a/LiteDB/DataStructure/Pages/ExtendPage.cs +++ b/LiteDB/DataStructure/Pages/ExtendPage.cs @@ -44,12 +44,12 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(BinaryReader reader) + public override void ReadContent(ByteReader reader) { this.Data = reader.ReadBytes(this.ItemCount); } - public override void WriteContent(BinaryWriter writer) + public override void WriteContent(ByteWriter writer) { writer.Write(this.Data); } diff --git a/LiteDB/DataStructure/Pages/HeaderPage.cs b/LiteDB/DataStructure/Pages/HeaderPage.cs index d98f519b3..32dddc929 100644 --- a/LiteDB/DataStructure/Pages/HeaderPage.cs +++ b/LiteDB/DataStructure/Pages/HeaderPage.cs @@ -53,23 +53,23 @@ public HeaderPage() #region Read/Write pages - public override void ReadHeader(BinaryReader reader) + public override void ReadHeader(ByteReader reader) { this.ChangeID = reader.ReadUInt16(); - var info = reader.ReadString(HEADER_INFO.Length); + var info = reader.ReadString(); var ver = reader.ReadByte(); this.FreeEmptyPageID = reader.ReadUInt32(); this.FirstCollectionPageID = reader.ReadUInt32(); this.LastPageID = reader.ReadUInt32(); - if (info != HEADER_INFO) throw LiteException.InvalidDatabase(reader.BaseStream); - if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(reader.BaseStream, ver); + if (info != HEADER_INFO) throw LiteException.InvalidDatabase(); + if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(ver); } - public override void WriteHeader(BinaryWriter writer) + public override void WriteHeader(ByteWriter writer) { writer.Write(this.ChangeID); - writer.Write(HEADER_INFO, HEADER_INFO.Length); + writer.Write(HEADER_INFO); writer.Write(FILE_VERSION); writer.Write(this.FreeEmptyPageID); writer.Write(this.FirstCollectionPageID); diff --git a/LiteDB/DataStructure/Pages/IndexPage.cs b/LiteDB/DataStructure/Pages/IndexPage.cs index 76afb2d04..db8b28832 100644 --- a/LiteDB/DataStructure/Pages/IndexPage.cs +++ b/LiteDB/DataStructure/Pages/IndexPage.cs @@ -42,7 +42,7 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(BinaryReader reader) + public override void ReadContent(ByteReader reader) { this.Nodes = new Dictionary(this.ItemCount); @@ -69,7 +69,7 @@ public override void ReadContent(BinaryReader reader) } } - public override void WriteContent(BinaryWriter writer) + public override void WriteContent(ByteWriter writer) { foreach (var node in this.Nodes.Values) { diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs index 1e1ab60b8..4eaadf867 100644 --- a/LiteDB/DataStructure/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -28,8 +28,10 @@ public T GetPage(uint pageID) if (page == null) { - page = _disk.ReadPage(pageID); + page = new T(); + page.ReadPage(_disk.ReadPage(pageID)); + _cache.AddPage(page); } diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 6540ecece..837da1054 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -77,9 +77,12 @@ public void Commit() header.IsDirty = true; - _disk.WritePages(_cache.GetDirtyPages()); + foreach(var page in _cache.GetDirtyPages()) + { + _disk.WritePage(page.PageID, page.WritePage()); - _cache.Clear(); + page.IsDirty = false; + } } // unlock datafile @@ -120,7 +123,10 @@ public void AvoidDirtyRead() if (cache == null) return; // get header page from DISK to check changeID - var header = _disk.ReadPage(0); + var header = new HeaderPage(); + + //TODO: better approch - get only first bytes instaned all (IDisk.GetChangeID) + header.ReadPage(_disk.ReadPage(0)); // if changeID was changed, file was changed by another process if (cache.ChangeID != header.ChangeID) diff --git a/LiteDB/Utils/ByteReader.cs b/LiteDB/Utils/ByteReader.cs index dbcc37cbf..767c815ea 100644 --- a/LiteDB/Utils/ByteReader.cs +++ b/LiteDB/Utils/ByteReader.cs @@ -6,7 +6,7 @@ namespace LiteDB { - public unsafe class ByteReader + internal unsafe class ByteReader { private byte[] _buffer; private int _pos; @@ -124,5 +124,73 @@ public Byte[] ReadBytes(int count) #endregion + #region Extended types + + public string ReadString() + { + var length = this.ReadInt32(); + var bytes = this.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } + + public string ReadString(int length) + { + var bytes = this.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } + + public DateTime ReadDateTime() + { + return new DateTime(this.ReadInt64()); + } + + public Guid ReadGuid() + { + return new Guid(this.ReadBytes(16)); + } + + public ObjectId ReadObjectId() + { + return new ObjectId(this.ReadBytes(12)); + } + + public PageAddress ReadPageAddress() + { + return new PageAddress(this.ReadUInt32(), this.ReadUInt16()); + } + + public BsonValue ReadBsonValue(ushort length) + { + var type = (BsonType)this.ReadByte(); + + switch (type) + { + case BsonType.Null: return BsonValue.Null; + + case BsonType.Int32: return this.ReadInt32(); + case BsonType.Int64: return this.ReadInt64(); + case BsonType.Double: return this.ReadDouble(); + + case BsonType.String: return this.ReadString(length); + + //case BsonType.Document: return new BsonReader().ReadDocument(reader); + //case BsonType.Array: return new BsonReader().ReadArray(reader); + + case BsonType.Binary: return this.ReadBytes(length); + case BsonType.ObjectId: return this.ReadObjectId(); + case BsonType.Guid: return this.ReadGuid(); + + case BsonType.Boolean: return this.ReadBoolean(); + case BsonType.DateTime: return this.ReadDateTime(); + + case BsonType.MinValue: return BsonValue.MinValue; + case BsonType.MaxValue: return BsonValue.MaxValue; + } + + throw new NotImplementedException(); + } + + #endregion + } } \ No newline at end of file diff --git a/LiteDB/Utils/ByteWriter.cs b/LiteDB/Utils/ByteWriter.cs index 06521b8a8..1f4c863c2 100644 --- a/LiteDB/Utils/ByteWriter.cs +++ b/LiteDB/Utils/ByteWriter.cs @@ -6,7 +6,7 @@ namespace LiteDB { - public unsafe class ByteWriter + internal unsafe class ByteWriter { private byte[] _buffer; private int _pos; @@ -156,5 +156,75 @@ public void Write(Byte[] value) #endregion + #region Extended types + + public void Write(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + this.Write(bytes.Length); + this.Write(bytes); + } + + public void Write(string value, int length) + { + var bytes = Encoding.UTF8.GetBytes(value); + if (bytes.Length != length) throw new ArgumentException("Invalid string length"); + this.Write(bytes); + } + + public void Write(DateTime value) + { + this.Write(value.Ticks); + } + + public void Write(Guid value) + { + this.Write(value.ToByteArray()); + } + + public void Write(ObjectId value) + { + this.Write(value.ToByteArray()); + } + + public void Write(PageAddress value) + { + this.Write(value.PageID); + this.Write(value.Index); + } + + public void WriteBsonValue(BsonValue value, ushort length) + { + this.Write((byte)value.Type); + + switch (value.Type) + { + case BsonType.Null: + case BsonType.MinValue: + case BsonType.MaxValue: + break; + + case BsonType.Int32: this.Write((Int32)value.RawValue); break; + case BsonType.Int64: this.Write((Int64)value.RawValue); break; + case BsonType.Double: this.Write((Double)value.RawValue); break; + + case BsonType.String: this.Write((String)value.RawValue, length); break; + + //case BsonType.Document: new BsonWriter().WriteDocument(writer, value.AsDocument); break; + //case BsonType.Array: new BsonWriter().WriteArray(writer, value.AsArray); break; + + case BsonType.Binary: this.Write((Byte[])value.RawValue); break; + case BsonType.ObjectId: this.Write((ObjectId)value.RawValue); break; + case BsonType.Guid: this.Write((Guid)value.RawValue); break; + + case BsonType.Boolean: this.Write((Boolean)value.RawValue); break; + case BsonType.DateTime: this.Write((DateTime)value.RawValue); break; + + default: throw new NotImplementedException(); + } + } + + #endregion + } } \ No newline at end of file diff --git a/LiteDB/Utils/DumpDatabase.cs b/LiteDB/Utils/DumpDatabase.cs index 6c17db5c4..ed933be96 100644 --- a/LiteDB/Utils/DumpDatabase.cs +++ b/LiteDB/Utils/DumpDatabase.cs @@ -49,7 +49,16 @@ private static T ReadPage(LiteDatabase db, uint pageID, bool mem) { if (mem && pageID == 0) return (T)(object)db.Cache.GetPage(0); - return mem ? db.Pager.GetPage(pageID) : db.Disk.ReadPage(pageID); + if(mem) + { + return db.Pager.GetPage(pageID); + } + else + { + var page = new T(); + page.ReadPage(db.Disk.ReadPage(pageID)); + return page; + } } public static StringBuilder Index(LiteDatabase db, string collection, string field, int size = 5) diff --git a/LiteDB/Utils/LiteException.cs b/LiteDB/Utils/LiteException.cs index b37e58521..04ca197c5 100644 --- a/LiteDB/Utils/LiteException.cs +++ b/LiteDB/Utils/LiteException.cs @@ -47,14 +47,12 @@ public static LiteException FileCorrupted(LiteFileInfo file) return new LiteException(103, "File '{0}' has no content or is corrupted", file.Id); } - public static LiteException InvalidDatabase(Stream stream) + public static LiteException InvalidDatabase() { - var filename = stream is FileStream ? ((FileStream)stream).Name : "Stream"; - - return new LiteException(104, "'{0}' is not a LiteDB database", filename); + return new LiteException(104, "Datafile is not a LiteDB database"); } - public static LiteException InvalidDatabaseVersion(Stream stream, int version) + public static LiteException InvalidDatabaseVersion(int version) { return new LiteException(105, "Invalid database version: {0}", version); } From 8162a59bc9194f6f2a321384462b9017820342e3 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 21:15:53 -0200 Subject: [PATCH 08/91] Update my tasks --- LiteDB-TODO.txt | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 15723375c..d2b767ddb 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,27 +1,19 @@ # LiteDB v.2 - TODO - Implementer SortDictionaryMRU -- Implementar tamanho do BsonDocument -- Implementar ByteReader/Writer +- Revisar BsonArray.Length não faz cache -- Remover transacao +- Remover/Revisar transacao - Locks - Implementar cache de 2 repos +- Substituir BsonSerializer pela versao 2 +- Remover BinaryReader/WriterExtentensions +- Implementar StreamDiskService() +- Otimizar GetChangeID() no IDiskService -- Converter page read/write para ByteR/W -- Converter BsonSerializer para Byte R/W -- Converter on-demand a serializacao no insert/update ## A pensar: -- Voltar user/version -- Voltar string connection -- A implementacaos dos multiplos discos ser apenas interna - -var w = new ByteWriter(); - -w.OnGetBytes((pos) => new byte[4000]) - - -Time to convert 1975687 -Time to copy 77554 - +- Voltar user/version?? +- Voltar string connection?? +- A implementacaos dos multiplos IDiskService ser apenas interna?? +- Sistema de Disk mais complexo (suporte a Journal/Recovery no FileDisk) \ No newline at end of file From 5293f40048272eceaa3967ccaccc5d1b93c866bf Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 2 Nov 2015 23:17:25 -0200 Subject: [PATCH 09/91] Update my todo --- LiteDB-TODO.txt | 50 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index d2b767ddb..56ed2b32b 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -9,6 +9,7 @@ - Remover BinaryReader/WriterExtentensions - Implementar StreamDiskService() - Otimizar GetChangeID() no IDiskService +- Thread Safe ## A pensar: @@ -16,4 +17,51 @@ - Voltar user/version?? - Voltar string connection?? - A implementacaos dos multiplos IDiskService ser apenas interna?? -- Sistema de Disk mais complexo (suporte a Journal/Recovery no FileDisk) \ No newline at end of file +- Sistema de Disk mais complexo (suporte a Journal/Recovery no FileDisk) +- Folder: "src", "test", "nuget" +- Como implementar pro DNX + + +BasePage { + long MRU; +} + +class MruCache { + + SortedDictionary _cache; + SortedDictionary _dirty; + int limit = 100; + int rem = 10; + + void Add() { + page.MRU = DateTime.Now.Tricks; // ou lock(inc) + _dirty[page.PageID] = page; + RemoveOlds(); + } + + BasePage Get(pageID) { + var page = _cache[pageID]; + page.MRU = DateTime.Now.Ticks; // ou lock(inc) + } + + void AddDirtyPage(page) { + page.IsDirty; + _dirty[page.PageID] = page; + } + + void ClearDirty() { + // _copy _dirty -> _cache + } + + void Clear() { + // _cache & _dirty + } + + void RemoveOlds() { + if(_cache.Count() > limit) { + // linq retornas IDs + _cache.remove(arrayIDs) + } + } + +} \ No newline at end of file From c6f57c9f334fcaf18e3d9648d0bd69fde65eb3d4 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 06:55:04 -0200 Subject: [PATCH 10/91] Update my todos --- LiteDB-TODO.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 56ed2b32b..bd23fe822 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,5 +1,10 @@ # LiteDB v.2 - TODO +- BasePage.OriginalBuffer +- BasePage.MRU +- LiteDatabase.Timeout +- pager.SetDirty(page) antes de alterar + - Implementer SortDictionaryMRU - Revisar BsonArray.Length não faz cache From 468cb6f440790517e4c922412fd400f04aae9b25 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 10:28:11 -0200 Subject: [PATCH 11/91] Add connection string again --- LiteDB/Core/LiteDatabase.cs | 43 +++++++++++++--- LiteDB/DataStructure/Disks/FileDiskService.cs | 37 ++++++++++++-- LiteDB/DataStructure/Disks/IDiskService.cs | 6 ++- .../Services/TransactionService.cs | 14 ++--- LiteDB/LiteDB.csproj | 1 + LiteDB/Utils/ConnectionString.cs | 51 +++++++++++++++++++ 6 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 LiteDB/Utils/ConnectionString.cs diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 0b5bcd742..498e4e08c 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -28,18 +28,49 @@ public partial class LiteDatabase : IDisposable internal CollectionService Collections { get; private set; } - public BsonMapper Mapper { get; set; } + public BsonMapper Mapper { get; private set; } + + public TimeSpan Timeout { get; private set; } + + private LiteDatabase() + { + this.Timeout = new TimeSpan(0, 1, 0); + this.Mapper = BsonMapper.Global; + } /// - /// Starts LiteDB database. Open database file or create a new one if not exits + /// Starts LiteDB database using a connectionString /// - public LiteDatabase(string filename) + public LiteDatabase(string connectionString) + : this() { - this.Disk = new FileDiskService(filename); + var connStr = new ConnectionString(connectionString); - this.Disk.Initialize(); + this.Timeout = connStr.GetValue("timeout", this.Timeout); - this.Mapper = BsonMapper.Global; + var filename = connStr.GetValue("filename", ""); + var journal = connStr.GetValue("journal", true); + + this.Disk = new FileDiskService(filename, journal, this.Timeout); + + this.Initialize(); + } + + /// + /// Starts LiteDB database using full parameters + /// + public LiteDatabase(IDiskService diskService, BsonMapper mapper) + { + this.Disk = diskService; + this.Mapper = mapper; + } + + /// + /// Initialize database engine - starts all services and open datafile + /// + private void Initialize() + { + this.Disk.Initialize(); this.Cache = new CacheService(); diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 838d0bc66..099b33f7d 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -14,12 +14,16 @@ internal class FileDiskService : IDiskService private FileStream _stream; private string _filename; + private TimeSpan _timeout; + private bool _journal; private byte[] _buffer = new byte[BasePage.PAGE_SIZE]; - public FileDiskService(string filename) + public FileDiskService(string filename, bool journal, TimeSpan timeout) { _filename = filename; + _journal = journal; + _timeout = timeout; } /// @@ -52,6 +56,18 @@ public void Unlock() _stream.Unlock(LOCK_POSITION, 1); } + + /// + /// Read first 2 bytes from datafile - contains changeID (avoid to read all header page) + /// + public ushort GetChangeID() + { + var bytes = new byte[2]; + _stream.Seek(0, SeekOrigin.End); + _stream.Read(bytes, 0, 2); + return BitConverter.ToUInt16(bytes, 0); + } + /// /// Read page bytes from disk /// @@ -97,6 +113,18 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } + public void ChangePage(uint pageID, byte[] original) + { + } + + public void StartWrite() + { + } + + public void EndWrite() + { + } + public void Dispose() { if(_stream != null) @@ -108,10 +136,9 @@ public void Dispose() /// /// Try run an operation over datafile - keep tring if locked /// - private static void TryExec(Action action) + private void TryExec(Action action) { - var timeout = new TimeSpan(0, 1, 0); - var timer = DateTime.Now.Add(timeout); + var timer = DateTime.Now.Add(_timeout); while (DateTime.Now < timer) { @@ -130,7 +157,7 @@ private static void TryExec(Action action) } } - throw LiteException.LockTimeout(timeout); + throw LiteException.LockTimeout(_timeout); } } } diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs index 0a2982469..31a0d961c 100644 --- a/LiteDB/DataStructure/Disks/IDiskService.cs +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -8,12 +8,16 @@ namespace LiteDB { - internal interface IDiskService : IDisposable + public interface IDiskService : IDisposable { void Initialize(); void Lock(); void Unlock(); byte[] ReadPage(uint pageID); + void ChangePage(uint pageID, byte[] original); + ushort GetChangeID(); + void StartWrite(); void WritePage(uint pageID, byte[] buffer); + void EndWrite(); } } diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 837da1054..883bf0385 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -110,7 +110,7 @@ public void Rollback() } /// - /// This method must be called before read operation to avoid dirty reads. + /// This method must be called before read/write operation to avoid dirty reads. /// It's occurs when my cache contains pages that was changed in another process /// public void AvoidDirtyRead() @@ -122,17 +122,13 @@ public void AvoidDirtyRead() if (cache == null) return; - // get header page from DISK to check changeID - var header = new HeaderPage(); + // read change direct from disk + var change = _disk.GetChangeID(); - //TODO: better approch - get only first bytes instaned all (IDisk.GetChangeID) - header.ReadPage(_disk.ReadPage(0)); - - // if changeID was changed, file was changed by another process - if (cache.ChangeID != header.ChangeID) + // if changeID was changed, file was changed by another process - clear all cache + if (cache.ChangeID != change) { _cache.Clear(); - _cache.AddPage(header); } } } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 6021800db..a545f59df 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -156,6 +156,7 @@ + diff --git a/LiteDB/Utils/ConnectionString.cs b/LiteDB/Utils/ConnectionString.cs new file mode 100644 index 000000000..693c82687 --- /dev/null +++ b/LiteDB/Utils/ConnectionString.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Configuration; +using System.Text.RegularExpressions; + +namespace LiteDB +{ + /// + /// Manage ConnectionString to connect and create databases. Connection string are NameValue using Name1=Value1; Name2=Value2 + /// + internal class ConnectionString + { + private Dictionary _values; + + public ConnectionString(string connectionString) + { + if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException("connectionString"); + + // Create a dictionary from string name=value collection + if(connectionString.Contains("=")) + { + _values = connectionString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Split(new char[] { '=' }, 2)) + .ToDictionary(t => t[0].Trim().ToLower(), t => t.Length == 1 ? "" : t[1].Trim(), StringComparer.InvariantCultureIgnoreCase); + } + else + { + // If connectionstring is only a filename, set filename + _values = new Dictionary(); + _values["filename"] = Path.GetFullPath(connectionString); + } + } + + public T GetValue(string key, T defaultValue) + { + try + { + return _values.ContainsKey(key) ? + (T)Convert.ChangeType(_values[key], typeof(T)) : + defaultValue; + } + catch(Exception) + { + throw new LiteException("Invalid connection string value type for " + key); + } + } + } +} From afcce072fc9e430ec9c7b7c0315009e471111156 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 10:33:21 -0200 Subject: [PATCH 12/91] Removing BinaryRead/Write extensions --- LiteDB/LiteDB.csproj | 2 - LiteDB/Utils/BinaryReaderExtensions.cs | 68 ----------------------- LiteDB/Utils/BinaryWriterExtensions.cs | 76 -------------------------- 3 files changed, 146 deletions(-) delete mode 100644 LiteDB/Utils/BinaryReaderExtensions.cs delete mode 100644 LiteDB/Utils/BinaryWriterExtensions.cs diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index a545f59df..70babd767 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -149,8 +149,6 @@ - - diff --git a/LiteDB/Utils/BinaryReaderExtensions.cs b/LiteDB/Utils/BinaryReaderExtensions.cs deleted file mode 100644 index c52bda2cf..000000000 --- a/LiteDB/Utils/BinaryReaderExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; - -namespace LiteDB -{ - internal static class BinaryReaderExtensions - { - public static string ReadString(this BinaryReader reader, int size) - { - var bytes = reader.ReadBytes(size); - return Encoding.UTF8.GetString(bytes); - } - - public static Guid ReadGuid(this BinaryReader reader) - { - return new Guid(reader.ReadBytes(16)); - } - - public static ObjectId ReadObjectId(this BinaryReader reader) - { - return new ObjectId(reader.ReadBytes(12)); - } - - public static DateTime ReadDateTime(this BinaryReader reader) - { - return new DateTime(reader.ReadInt64()); - } - - public static PageAddress ReadPageAddress(this BinaryReader reader) - { - return new PageAddress(reader.ReadUInt32(), reader.ReadUInt16()); - } - - public static BsonValue ReadBsonValue(this BinaryReader reader, ushort length) - { - var type = (BsonType)reader.ReadByte(); - - switch (type) - { - case BsonType.Null: return BsonValue.Null; - - case BsonType.Int32: return reader.ReadInt32(); - case BsonType.Int64: return reader.ReadInt64(); - case BsonType.Double: return reader.ReadDouble(); - - case BsonType.String: return reader.ReadString(length); - - case BsonType.Document: return new BsonReader().ReadDocument(reader); - case BsonType.Array: return new BsonReader().ReadArray(reader); - - case BsonType.Binary: return reader.ReadBytes(length); - case BsonType.ObjectId: return reader.ReadObjectId(); - case BsonType.Guid: return reader.ReadGuid(); - - case BsonType.Boolean: return reader.ReadBoolean(); - case BsonType.DateTime: return reader.ReadDateTime(); - - case BsonType.MinValue: return BsonValue.MinValue; - case BsonType.MaxValue: return BsonValue.MaxValue; - } - - throw new NotImplementedException(); - } - } -} diff --git a/LiteDB/Utils/BinaryWriterExtensions.cs b/LiteDB/Utils/BinaryWriterExtensions.cs deleted file mode 100644 index 3a02a6df3..000000000 --- a/LiteDB/Utils/BinaryWriterExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.Threading; - -namespace LiteDB -{ - internal static class BinaryWriterExtensions - { - public static void Write(this BinaryWriter writer, string text, int length) - { - var bytes = Encoding.UTF8.GetBytes(text); - - if (bytes.Length != length) - { - throw new ArgumentException("Invalid string length"); - } - - writer.Write(bytes); - } - - public static void Write(this BinaryWriter writer, ObjectId oid) - { - writer.Write(oid.ToByteArray()); - } - - public static void Write(this BinaryWriter writer, Guid guid) - { - writer.Write(guid.ToByteArray()); - } - - public static void Write(this BinaryWriter writer, DateTime dateTime) - { - writer.Write(dateTime.Ticks); - } - - public static void Write(this BinaryWriter writer, PageAddress address) - { - writer.Write(address.PageID); - writer.Write(address.Index); - } - - public static void WriteBsonValue(this BinaryWriter writer, BsonValue value, ushort length) - { - writer.Write((byte)value.Type); - - switch(value.Type) - { - case BsonType.Null: - case BsonType.MinValue: - case BsonType.MaxValue: - break; - - case BsonType.Int32: writer.Write((Int32)value.RawValue); break; - case BsonType.Int64: writer.Write((Int64)value.RawValue); break; - case BsonType.Double: writer.Write((Double)value.RawValue); break; - - case BsonType.String: writer.Write((String)value.RawValue, length); break; - - case BsonType.Document: new BsonWriter().WriteDocument(writer, value.AsDocument); break; - case BsonType.Array: new BsonWriter().WriteArray(writer, value.AsArray); break; - - case BsonType.Binary: writer.Write((Byte[])value.RawValue); break; - case BsonType.ObjectId: writer.Write((ObjectId)value.RawValue); break; - case BsonType.Guid: writer.Write((Guid)value.RawValue); break; - - case BsonType.Boolean: writer.Write((Boolean)value.RawValue); break; - case BsonType.DateTime: writer.Write((DateTime)value.RawValue); break; - - default: throw new NotImplementedException(); - } - } - } -} From 181a3c72dde675715afe33bc99275146108970fa Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 13:12:33 -0200 Subject: [PATCH 13/91] Add circular cache first commit --- LiteDB-TODO.txt | 11 ++- LiteDB/DataStructure/Pages/BasePage.cs | 10 +++ LiteDB/DataStructure/Services/CacheService.cs | 74 ++++++++++++++++++- .../Services/TransactionService.cs | 4 + 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index bd23fe822..125dc5d1f 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,8 +1,7 @@ -# LiteDB v.2 - TODO +# LiteDB v.2 - TODO - BasePage.OriginalBuffer - BasePage.MRU -- LiteDatabase.Timeout - pager.SetDirty(page) antes de alterar - Implementer SortDictionaryMRU @@ -16,6 +15,14 @@ - Otimizar GetChangeID() no IDiskService - Thread Safe +## Cache + +Otimizar a cache: + +- Ter uma cache exclusiva para Header (só 1) +- Ter uma cache exclusiva para as Collecitons (são poucas e muito usadas) +- As demais (Indice, Data, Extend) pode ser por ordem circular +- Cache exclusiva, não circular, para paginas dirty (indice/data/extend) ## A pensar: diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index 7ecb63dfa..d5a998706 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -67,6 +67,16 @@ internal class BasePage /// public bool IsDirty { get; set; } + /// + /// Used in cache system to manager circular cache (CacheService only will use) [not-persistable] + /// + public uint MRU { get; set; } + + /// + /// This is the data when read first from disk - used to journal operations (IDiskService only will use) + /// + //TODO public byte[] OriginalBuffer { get; private set; } + public BasePage() { this.PrevPageID = uint.MaxValue; diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 2701b214a..8723c20ae 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -13,7 +14,14 @@ namespace LiteDB /// internal class CacheService : IDisposable { - // a very simple dictionary for pages cache and track + // cache use a circular structure to avoid consume too many memory + private const int CACHE_LIMIT = 200; // ~200MB + private const int CACHE_CLEAR = 20; + + private uint _mru = 0; + private object _mru_lock = new object(); + + // single cache structure private SortedDictionary _cache; public CacheService() @@ -36,6 +44,8 @@ public T GetPage(uint pageID) return null; } + this.SetMRU(page); + return (T)page; } @@ -44,7 +54,11 @@ public T GetPage(uint pageID) /// public void AddPage(BasePage page) { + this.SetMRU(page); + _cache[page.PageID] = page; + + this.RemoveLowerMRU(); } /// @@ -52,6 +66,7 @@ public void AddPage(BasePage page) /// public void Clear() { + _mru = 0; _cache.Clear(); } @@ -69,8 +84,65 @@ public IEnumerable GetDirtyPages() } } + /// + /// Set a new MRU to page + /// + private void SetMRU(BasePage page) + { + if(page == null) return; + + // if header or collection set most higher value to never clear + if(page.PageType == PageType.Header || page.PageType == PageType.Collection) + { + page.MRU = uint.MaxValue - page.PageID; + } + else + { + // get next mru value + lock(_mru_lock) + { + page.MRU = ++_mru; + } + } + } + + private Stopwatch _select = new Stopwatch(); + private Stopwatch _count = new Stopwatch(); + + private void RemoveLowerMRU() + { + _count.Start(); + var count = _cache.Count(); + _count.Stop(); + + Console.WriteLine("CACHE_COUNT: " + count); + + // check if I have too many pages in cache + if(count > CACHE_LIMIT) + { + // lets clear some non-dirty pages + //TODO: test performance + _select.Start(); + var delete = _cache + .Where(x => x.Value.IsDirty == false) + .OrderBy(x => x.Value.MRU) + .Take(CACHE_CLEAR) + .Select(x => x.Key) + .ToArray(); + _select.Stop(); + + foreach(var pageID in delete) + { + _cache.Remove(pageID); + } + } + } + public void Dispose() { + Console.WriteLine("Total em cache : " + _cache.Count()); + Console.WriteLine("Tempo total para contar : " + _count.ElapsedMilliseconds); + Console.WriteLine("Tempo total para selecionar: " + _select.ElapsedMilliseconds); this.Clear(); } } diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 883bf0385..8976db41f 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -77,12 +77,16 @@ public void Commit() header.IsDirty = true; + _disk.StartWrite(); + foreach(var page in _cache.GetDirtyPages()) { _disk.WritePage(page.PageID, page.WritePage()); page.IsDirty = false; } + + _disk.EndWrite(); } // unlock datafile From faf4c18b2d5d4c78220234da1383f521884bf018 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 13:58:45 -0200 Subject: [PATCH 14/91] Try new cache system --- LiteDB-TODO.txt | 1 + LiteDB/DataStructure/Services/CacheService.cs | 16 +++++++++++----- .../DataStructure/Services/TransactionService.cs | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 125dc5d1f..c498a3aa5 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,5 +1,6 @@ # LiteDB v.2 - TODO +- Remover escrita ta header page no filedisk - BasePage.OriginalBuffer - BasePage.MRU - pager.SetDirty(page) antes de alterar diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 8723c20ae..ce5e7f845 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -15,12 +15,15 @@ namespace LiteDB internal class CacheService : IDisposable { // cache use a circular structure to avoid consume too many memory - private const int CACHE_LIMIT = 200; // ~200MB - private const int CACHE_CLEAR = 20; + private const int CACHE_LIMIT = 10000; // ~80MB + private const int CACHE_CLEAR = 1000; private uint _mru = 0; private object _mru_lock = new object(); + private int _hit = 0; + private int _get = 0; + // single cache structure private SortedDictionary _cache; @@ -46,6 +49,9 @@ public T GetPage(uint pageID) this.SetMRU(page); + if(page != null) _hit++; + _get++; + return (T)page; } @@ -109,14 +115,12 @@ private void SetMRU(BasePage page) private Stopwatch _select = new Stopwatch(); private Stopwatch _count = new Stopwatch(); - private void RemoveLowerMRU() + public void RemoveLowerMRU() { _count.Start(); var count = _cache.Count(); _count.Stop(); - Console.WriteLine("CACHE_COUNT: " + count); - // check if I have too many pages in cache if(count > CACHE_LIMIT) { @@ -140,6 +144,8 @@ private void RemoveLowerMRU() public void Dispose() { + Console.WriteLine("Total em GET : " + _get); + Console.WriteLine("Total em HIT : " + _hit); Console.WriteLine("Total em cache : " + _cache.Count()); Console.WriteLine("Tempo total para contar : " + _count.ElapsedMilliseconds); Console.WriteLine("Tempo total para selecionar: " + _select.ElapsedMilliseconds); diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 8976db41f..75335f1ac 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -87,6 +87,8 @@ public void Commit() } _disk.EndWrite(); + + _cache.RemoveLowerMRU(); } // unlock datafile From d0ec9b275f34cffad1ce09a72296ad29d0c69351 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 16:27:18 -0200 Subject: [PATCH 15/91] Update my todos --- LiteDB-TODO.txt | 93 +++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index c498a3aa5..6c7b6f327 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,80 +1,49 @@ # LiteDB v.2 - TODO -- Remover escrita ta header page no filedisk - BasePage.OriginalBuffer -- BasePage.MRU -- pager.SetDirty(page) antes de alterar - -- Implementer SortDictionaryMRU - Revisar BsonArray.Length não faz cache - - Remover/Revisar transacao - Locks -- Implementar cache de 2 repos -- Substituir BsonSerializer pela versao 2 -- Remover BinaryReader/WriterExtentensions - Implementar StreamDiskService() -- Otimizar GetChangeID() no IDiskService - Thread Safe +- InitialSize para FileDiskService +- Remove auto-register do shell + ## Cache -Otimizar a cache: +Para saber qual caminho seguir em relação a cache, acho que precisa fazer branchs para testar: -- Ter uma cache exclusiva para Header (só 1) -- Ter uma cache exclusiva para as Collecitons (são poucas e muito usadas) -- As demais (Indice, Data, Extend) pode ser por ordem circular -- Cache exclusiva, não circular, para paginas dirty (indice/data/extend) +1) A "dev" como está: usa cache sem limites +2) Não usa cache nenhuma para leitura, apenas para dirty pages +3) Usa cache para salvar com limitador de tamanho. Vai gravando em disco conforme vai ficando dirty (grava a cada X paginas sujas) -## A pensar: +Preciso criar uma banchmark para testar os mais diversos tipos de situação: -- Voltar user/version?? -- Voltar string connection?? -- A implementacaos dos multiplos IDiskService ser apenas interna?? -- Sistema de Disk mais complexo (suporte a Journal/Recovery no FileDisk) -- Folder: "src", "test", "nuget" -- Como implementar pro DNX +a) Criação de um banco grande de 1 unica coleção +b) Criação de um banco grande de 100 coleções diferentes +c) Criação de indice +d) Leitura de todos os registros +e) Leitura conforme indice (retorno poucos registros) + +> Para cada teste, deve usar documento grande/complexo e documento pequeno/simples +> Avaliar tempo/consumo total de memória +> Aproveitar os testes para fazer PAGE_SIZE de 8K tb +?? Remover escrita ta header page no filedisk +?? pager.SetDirty(page) antes de alterar +?? Implementer SortDictionaryMRU +?? Implementar cache de 2 repos -BasePage { - long MRU; -} +## A pensar: -class MruCache { +- Voltar UserVersion?? +- Solução de Migrations (protected?) + db.Migrations.Add(new Migration1()); +- DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? +- Folder root: "src", "test", "nuget" +- Encrypt datafile (EncryptDiskService : FileDiskService) - SortedDictionary _cache; - SortedDictionary _dirty; - int limit = 100; - int rem = 10; - - void Add() { - page.MRU = DateTime.Now.Tricks; // ou lock(inc) - _dirty[page.PageID] = page; - RemoveOlds(); - } - - BasePage Get(pageID) { - var page = _cache[pageID]; - page.MRU = DateTime.Now.Ticks; // ou lock(inc) - } - - void AddDirtyPage(page) { - page.IsDirty; - _dirty[page.PageID] = page; - } - - void ClearDirty() { - // _copy _dirty -> _cache - } - - void Clear() { - // _cache & _dirty - } - - void RemoveOlds() { - if(_cache.Count() > limit) { - // linq retornas IDs - _cache.remove(arrayIDs) - } - } +- Como implementar pro DNX +- Mapeador/Configuration estilo EF + db.Configuration.Add(new CredorConfiguration()) -} \ No newline at end of file From 5aee6dd34b1270e9f5ec022e7ec87951cb25a8b5 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 3 Nov 2015 22:04:09 -0200 Subject: [PATCH 16/91] New cache system, need tests --- LiteDB-TODO.txt | 1 + LiteDB/Core/Collections/Index.cs | 4 +- LiteDB/Core/Collections/Update.cs | 2 +- LiteDB/Core/LiteDatabase.cs | 9 +- LiteDB/DataStructure/Disks/FileDiskService.cs | 39 ++++--- LiteDB/DataStructure/Disks/IDiskService.cs | 14 ++- LiteDB/DataStructure/Pages/BasePage.cs | 10 +- LiteDB/DataStructure/Pages/HeaderPage.cs | 13 ++- LiteDB/DataStructure/Services/CacheService.cs | 109 +++++------------- .../Services/CollectionService.cs | 2 +- LiteDB/DataStructure/Services/DataService.cs | 12 +- LiteDB/DataStructure/Services/IndexService.cs | 16 +-- LiteDB/DataStructure/Services/PageService.cs | 35 +++--- .../Services/TransactionService.cs | 14 ++- 14 files changed, 130 insertions(+), 150 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 6c7b6f327..2f0ed48db 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,5 +1,6 @@ # LiteDB v.2 - TODO +- Journal - BasePage.OriginalBuffer - Revisar BsonArray.Length não faz cache - Remover/Revisar transacao - Locks diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index a4c31df1f..f20ac56d8 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -84,7 +84,7 @@ public virtual bool EnsureIndex(string field, IndexOptions options) newNode.DataBlock = dataBlock.Position; // mark datablock page as dirty - dataBlock.Page.IsDirty = true; + this.Database.Pager.SetDirty(dataBlock.Page); } this.Database.Transaction.Commit(); @@ -190,7 +190,7 @@ public bool DropIndex(string field) index.Clear(); // save collection page - col.IsDirty = true; + this.Database.Pager.SetDirty(col); this.Database.Transaction.Commit(); diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index d6e80b39b..6b0d4b13a 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -96,7 +96,7 @@ private bool UpdateDoc(BsonValue id, BsonDocument doc) // point my dataBlock dataBlock.IndexRef[index.Slot] = newNode.Position; - dataBlock.Page.IsDirty = true; + this.Database.Pager.SetDirty(dataBlock.Page); } } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 498e4e08c..fcf150119 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -70,7 +70,12 @@ public LiteDatabase(IDiskService diskService, BsonMapper mapper) /// private void Initialize() { - this.Disk.Initialize(); + var isNew = this.Disk.Initialize(); + + if(isNew) + { + this.Disk.WritePage(0, new HeaderPage().WritePage()); + } this.Cache = new CacheService(); @@ -154,7 +159,7 @@ public bool RenameCollection(string oldName, string newName) } col.CollectionName = newName; - col.IsDirty = true; + this.Pager.SetDirty(col); this.Transaction.Commit(); } diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 099b33f7d..c05baf0df 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -10,12 +10,16 @@ namespace LiteDB { internal class FileDiskService : IDiskService { + /// + /// Lock data file position + /// private const int LOCK_POSITION = 0; private FileStream _stream; private string _filename; private TimeSpan _timeout; private bool _journal; + //private bool _readonly; private byte[] _buffer = new byte[BasePage.PAGE_SIZE]; @@ -27,17 +31,15 @@ public FileDiskService(string filename, bool journal, TimeSpan timeout) } /// - /// Open datafile - if not exits, create a new one + /// Open datafile - returns true if new /// - public void Initialize() + public bool Initialize() { - // open file as readOnly - if we need use Write, re-open in Write Mode - _stream = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite, BasePage.PAGE_SIZE); + // open data file + _stream = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); - if (_stream.Length == 0) - { - this.WritePage(0, new HeaderPage().WritePage()); - } + // returns true if new file + return _stream.Length == 0; } /// @@ -45,7 +47,9 @@ public void Initialize() /// public void Lock() { - TryExec(() => _stream.Lock(LOCK_POSITION, 1)); + TryExec(() => + _stream.Lock(LOCK_POSITION, 1) + ); } /// @@ -56,14 +60,13 @@ public void Unlock() _stream.Unlock(LOCK_POSITION, 1); } - /// /// Read first 2 bytes from datafile - contains changeID (avoid to read all header page) /// public ushort GetChangeID() { var bytes = new byte[2]; - _stream.Seek(0, SeekOrigin.End); + _stream.Seek(HeaderPage.CHANGE_ID_POSITION, SeekOrigin.Begin); _stream.Read(bytes, 0, 2); return BitConverter.ToUInt16(bytes, 0); } @@ -96,12 +99,6 @@ public byte[] ReadPage(uint pageID) /// public void WritePage(uint pageID, byte[] buffer) { - if(_stream.CanWrite == false) - { - _stream.Dispose(); - _stream = new FileStream(_filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); - } - var position = (long)pageID * (long)BasePage.PAGE_SIZE; // position cursor @@ -113,16 +110,24 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } + private void TryRecovery() + { + // se tiver journal, faz recovery + } + public void ChangePage(uint pageID, byte[] original) { + // grava o journal-file } public void StartWrite() { + // comita o journal file } public void EndWrite() { + // remove journal file } public void Dispose() diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs index 31a0d961c..f5e9282fc 100644 --- a/LiteDB/DataStructure/Disks/IDiskService.cs +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -10,14 +10,18 @@ namespace LiteDB { public interface IDiskService : IDisposable { - void Initialize(); + bool Initialize(); + void Lock(); void Unlock(); + + void ChangePage(uint pageID, byte[] original); // WriteJournal() + void StartWrite(); // EndJournal + void EndWrite(); // DeleteJournal() + byte[] ReadPage(uint pageID); - void ChangePage(uint pageID, byte[] original); - ushort GetChangeID(); - void StartWrite(); void WritePage(uint pageID, byte[] buffer); - void EndWrite(); + + ushort GetChangeID(); } } diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index d5a998706..0daa60ecf 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -67,15 +67,10 @@ internal class BasePage /// public bool IsDirty { get; set; } - /// - /// Used in cache system to manager circular cache (CacheService only will use) [not-persistable] - /// - public uint MRU { get; set; } - /// /// This is the data when read first from disk - used to journal operations (IDiskService only will use) /// - //TODO public byte[] OriginalBuffer { get; private set; } + public byte[] DiskData { get; private set; } public BasePage() { @@ -106,6 +101,7 @@ public virtual void Clear() this.PageType = PageType.Empty; this.FreeBytes = PAGE_AVAILABLE_BYTES; this.ItemCount = 0; + this.DiskData = new byte[0]; } /// @@ -166,6 +162,8 @@ public void ReadPage(byte[] buffer) { this.ReadContent(reader); } + + //this.DiskData = buffer; } public byte[] WritePage() diff --git a/LiteDB/DataStructure/Pages/HeaderPage.cs b/LiteDB/DataStructure/Pages/HeaderPage.cs index 32dddc929..528c72891 100644 --- a/LiteDB/DataStructure/Pages/HeaderPage.cs +++ b/LiteDB/DataStructure/Pages/HeaderPage.cs @@ -8,6 +8,11 @@ namespace LiteDB { internal class HeaderPage : BasePage { + /// + /// ChangeID in file position + /// + public const int CHANGE_ID_POSITION = 49; + /// /// Header info the validate that datafile is a LiteDB file /// @@ -53,11 +58,11 @@ public HeaderPage() #region Read/Write pages - public override void ReadHeader(ByteReader reader) + public override void ReadContent(ByteReader reader) { - this.ChangeID = reader.ReadUInt16(); var info = reader.ReadString(); var ver = reader.ReadByte(); + this.ChangeID = reader.ReadUInt16(); this.FreeEmptyPageID = reader.ReadUInt32(); this.FirstCollectionPageID = reader.ReadUInt32(); this.LastPageID = reader.ReadUInt32(); @@ -66,11 +71,11 @@ public override void ReadHeader(ByteReader reader) if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(ver); } - public override void WriteHeader(ByteWriter writer) + public override void WriteContent(ByteWriter writer) { - writer.Write(this.ChangeID); writer.Write(HEADER_INFO); writer.Write(FILE_VERSION); + writer.Write(this.ChangeID); writer.Write(this.FreeEmptyPageID); writer.Write(this.FirstCollectionPageID); writer.Write(this.LastPageID); diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index ce5e7f845..3c6aa793e 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -14,22 +14,14 @@ namespace LiteDB /// internal class CacheService : IDisposable { - // cache use a circular structure to avoid consume too many memory - private const int CACHE_LIMIT = 10000; // ~80MB - private const int CACHE_CLEAR = 1000; - - private uint _mru = 0; - private object _mru_lock = new object(); - - private int _hit = 0; - private int _get = 0; - // single cache structure private SortedDictionary _cache; + private SortedDictionary _dirty; public CacheService() { _cache = new SortedDictionary(); + _dirty = new SortedDictionary(); } /// @@ -47,24 +39,28 @@ public T GetPage(uint pageID) return null; } - this.SetMRU(page); - - if(page != null) _hit++; - _get++; - return (T)page; } /// /// Add a page to cache. if this page is in cache, override (except if is basePage - in this case, copy header) + /// If set is dirty, add in a second list /// - public void AddPage(BasePage page) + public void AddPage(BasePage page, bool dirty = false) { - this.SetMRU(page); - - _cache[page.PageID] = page; + // do not cache extend page - never will be reused + if (page.PageType != PageType.Extend) + { + _cache[page.PageID] = page; + } - this.RemoveLowerMRU(); + // page is dirty? add in a special list too and mark as dirty (all type of pages) + if (dirty && !page.IsDirty) + { + page.IsDirty = true; + _dirty[page.PageID] = page; + //TODO: dispara metodo de pagina alterada + } } /// @@ -72,83 +68,40 @@ public void AddPage(BasePage page) /// public void Clear() { - _mru = 0; + _dirty.Clear(); _cache.Clear(); } - public bool HasDirtyPages { get { return this.GetDirtyPages().FirstOrDefault() != null; } } - /// - /// Returns all dirty pages including header page (for better write performance, get all pages in PageID increase order) + /// Clear dirty cache only /// - public IEnumerable GetDirtyPages() + public void ClearDirty() { - // now returns all pages in sequence - foreach (var page in _cache.Values.Where(x => x.IsDirty)) + // clear page and clear special list (will keep in _cache list) + foreach(var page in _dirty.Values) { - yield return page; + page.IsDirty = false; } - } - /// - /// Set a new MRU to page - /// - private void SetMRU(BasePage page) - { - if(page == null) return; - - // if header or collection set most higher value to never clear - if(page.PageType == PageType.Header || page.PageType == PageType.Collection) - { - page.MRU = uint.MaxValue - page.PageID; - } - else - { - // get next mru value - lock(_mru_lock) - { - page.MRU = ++_mru; - } - } + _dirty.Clear(); } - private Stopwatch _select = new Stopwatch(); - private Stopwatch _count = new Stopwatch(); + public bool HasDirtyPages { get { return _dirty.Count() > 0; } } - public void RemoveLowerMRU() + /// + /// Returns all dirty pages including header page (for better write performance, get all pages in PageID increase order) + /// + public IEnumerable GetDirtyPages() { - _count.Start(); - var count = _cache.Count(); - _count.Stop(); - - // check if I have too many pages in cache - if(count > CACHE_LIMIT) + // now returns all pages in sequence + foreach (var page in _dirty.Values) { - // lets clear some non-dirty pages - //TODO: test performance - _select.Start(); - var delete = _cache - .Where(x => x.Value.IsDirty == false) - .OrderBy(x => x.Value.MRU) - .Take(CACHE_CLEAR) - .Select(x => x.Key) - .ToArray(); - _select.Stop(); - - foreach(var pageID in delete) - { - _cache.Remove(pageID); - } + yield return page; } } public void Dispose() { - Console.WriteLine("Total em GET : " + _get); - Console.WriteLine("Total em HIT : " + _hit); - Console.WriteLine("Total em cache : " + _cache.Count()); - Console.WriteLine("Tempo total para contar : " + _count.ElapsedMilliseconds); - Console.WriteLine("Tempo total para selecionar: " + _select.ElapsedMilliseconds); this.Clear(); } } diff --git a/LiteDB/DataStructure/Services/CollectionService.cs b/LiteDB/DataStructure/Services/CollectionService.cs index 9409fe642..b2a08bf5e 100644 --- a/LiteDB/DataStructure/Services/CollectionService.cs +++ b/LiteDB/DataStructure/Services/CollectionService.cs @@ -58,7 +58,7 @@ public CollectionPage Add(string name) _pager.AddOrRemoveToFreeList(true, col, _pager.Header, ref _pager.Header.FirstCollectionPageID); col.CollectionName = name; - col.IsDirty = true; + _pager.SetDirty(col); // create PK index var pk = _indexer.CreateIndex(col); diff --git a/LiteDB/DataStructure/Services/DataService.cs b/LiteDB/DataStructure/Services/DataService.cs index eaa22bb4c..dc410cc60 100644 --- a/LiteDB/DataStructure/Services/DataService.cs +++ b/LiteDB/DataStructure/Services/DataService.cs @@ -48,14 +48,14 @@ public DataBlock Insert(CollectionPage col, byte[] data) // update freebytes + items count dataPage.UpdateItemCount(); - dataPage.IsDirty = true; + _pager.SetDirty(dataPage); // add/remove dataPage on freelist if has space _pager.AddOrRemoveToFreeList(dataPage.FreeBytes > DataPage.DATA_RESERVED_BYTES, dataPage, col, ref col.FreeDataPageID); col.DocumentCount++; - col.IsDirty = true; + _pager.SetDirty(col); return block; } @@ -110,7 +110,7 @@ public DataBlock Update(CollectionPage col, PageAddress blockAddress, byte[] dat // add/remove dataPage on freelist if has space AND its on/off free list _pager.AddOrRemoveToFreeList(dataPage.FreeBytes > DataPage.DATA_RESERVED_BYTES, dataPage, col, ref col.FreeDataPageID); - dataPage.IsDirty = true; + _pager.SetDirty(dataPage); return block; } @@ -185,8 +185,8 @@ public DataBlock Delete(CollectionPage col, PageAddress blockAddress) col.DocumentCount--; - col.IsDirty = true; - page.IsDirty = true; + _pager.SetDirty(col); + _pager.SetDirty(page); return block; } @@ -210,7 +210,7 @@ public void StoreExtendData(ExtendPage page, byte[] data) // updates free bytes + items count page.UpdateItemCount(); - page.IsDirty = true; + _pager.SetDirty(page); bytesLeft -= bytesToCopy; offset += bytesToCopy; diff --git a/LiteDB/DataStructure/Services/IndexService.cs b/LiteDB/DataStructure/Services/IndexService.cs index 4cdfb4f90..3e619e947 100644 --- a/LiteDB/DataStructure/Services/IndexService.cs +++ b/LiteDB/DataStructure/Services/IndexService.cs @@ -63,8 +63,8 @@ public CollectionIndex CreateIndex(CollectionPage col) index.TailNode = tail.Position; - index.Page.IsDirty = true; - page.IsDirty = true; + _pager.SetDirty(index.Page); + _pager.SetDirty(page); return index; } @@ -141,17 +141,17 @@ private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) { next.Prev[i] = node.Position; - next.Page.IsDirty = true; + _pager.SetDirty(next.Page); } - cur.Page.IsDirty = true; + _pager.SetDirty(cur.Page); } } // add/remove indexPage on freelist if has space _pager.AddOrRemoveToFreeList(page.FreeBytes > IndexPage.INDEX_RESERVED_BYTES, page, index.Page, ref index.FreeIndexPageID); - page.IsDirty = true; + _pager.SetDirty(page); return node; } @@ -173,12 +173,12 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) if (prev != null) { prev.Next[i] = node.Next[i]; - prev.Page.IsDirty = true; + _pager.SetDirty(prev.Page); } if (next != null) { next.Prev[i] = node.Prev[i]; - next.Page.IsDirty = true; + _pager.SetDirty(next.Page); } } @@ -201,7 +201,7 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) _pager.AddOrRemoveToFreeList(page.FreeBytes > IndexPage.INDEX_RESERVED_BYTES, node.Page, index.Page, ref index.FreeIndexPageID); } - page.IsDirty = true; + _pager.SetDirty(page); } diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs index 4eaadf867..089115131 100644 --- a/LiteDB/DataStructure/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -38,6 +38,11 @@ public T GetPage(uint pageID) return page; } + public void SetDirty(BasePage page) + { + _cache.AddPage(page, true); + } + public HeaderPage Header { get { return this.GetPage(0); } } /// @@ -86,12 +91,12 @@ public T NewPage(BasePage prevPage = null) { page.PrevPageID = prevPage.PageID; prevPage.NextPageID = page.PageID; - prevPage.IsDirty = true; + this.SetDirty(prevPage); } // mark header and this new page as dirty, and then add to cache - page.IsDirty = true; - this.Header.IsDirty = true; + this.SetDirty(page); + this.SetDirty(this.Header); _cache.AddPage(page); @@ -110,7 +115,7 @@ public void DeletePage(uint pageID, bool addSequence = false) { // update page to mark as completly empty page page.Clear(); - page.IsDirty = true; + this.SetDirty(page); // add to empty free list this.AddOrRemoveToFreeList(true, page, this.Header, ref this.Header.FreeEmptyPageID); @@ -196,23 +201,23 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage // link next page to my page next.PrevPageID = page.PageID; - next.IsDirty = true; + this.SetDirty(next); // my page is the new first page on list if (page.PrevPageID == 0) { fieldPageID = page.PageID; - startPage.IsDirty = true; + this.SetDirty(startPage); } else { // if not the first, ajust links from previous page var prev = this.GetPage(page.PrevPageID); prev.NextPageID = page.PageID; - prev.IsDirty = true; + this.SetDirty(prev); } - page.IsDirty = true; + this.SetDirty(page); return; // job done - exit } @@ -226,17 +231,17 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage // it's first page on list page.PrevPageID = 0; fieldPageID = page.PageID; - startPage.IsDirty = true; + this.SetDirty(startPage); } else { // it's last position on list (next = last page on list) page.PrevPageID = next.PageID; next.NextPageID = page.PageID; - next.IsDirty = true; + this.SetDirty(next); } - page.IsDirty = true; + this.SetDirty(page); } /// @@ -248,14 +253,14 @@ private void RemoveToFreeList(BasePage page, BasePage startPage, ref uint fieldP if (page.PrevPageID == 0) { fieldPageID = page.NextPageID; - startPage.IsDirty = true; + this.SetDirty(startPage); } else { // if not the first, get previous page to remove NextPageId var prevPage = this.GetPage(page.PrevPageID); prevPage.NextPageID = page.NextPageID; - prevPage.IsDirty = true; + this.SetDirty(prevPage); } // if my page is not the last on sequence, ajust the last page @@ -263,11 +268,11 @@ private void RemoveToFreeList(BasePage page, BasePage startPage, ref uint fieldP { var nextPage = this.GetPage(page.NextPageID); nextPage.PrevPageID = page.PrevPageID; - nextPage.IsDirty = true; + this.SetDirty(nextPage); } page.PrevPageID = page.NextPageID = uint.MaxValue; - page.IsDirty = true; + this.SetDirty(page); } /// diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 75335f1ac..4611cdca6 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -20,6 +20,8 @@ internal TransactionService(IDiskService disk, CacheService cache) { _disk = disk; _cache = cache; + + // _cache.OnPageChange((page) => _disk.PageChange(page.PageID, page.OriginalBuffer); } public bool IsInTransaction { get { return _level > 0; } } @@ -41,7 +43,7 @@ public void Begin() } /// - /// Abort a transaction is used when begin and has no changes yet - no writes, no checks + /// Abort a transaction is used when begin and has no changes yet - no writes, no checks (it's simple than rollback) /// public void Abort() { @@ -75,20 +77,19 @@ public void Commit() // increase file changeID (back to 0 when overflow) header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); - header.IsDirty = true; + _cache.AddPage(header, true); _disk.StartWrite(); foreach(var page in _cache.GetDirtyPages()) { + Console.WriteLine("save dirty page " + page.PageID); _disk.WritePage(page.PageID, page.WritePage()); - - page.IsDirty = false; } _disk.EndWrite(); - _cache.RemoveLowerMRU(); + _cache.ClearDirty(); } // unlock datafile @@ -109,6 +110,9 @@ public void Rollback() // clear all pages from memory _cache.Clear(); + // tell disk service there is no more writes + _disk.EndWrite(); + // unlock datafile _disk.Unlock(); From b148611746f36bf147689dbae6dbd2c40d6c1b74 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 5 Nov 2015 13:22:09 -0200 Subject: [PATCH 17/91] Journal first version --- LiteDB-TODO.txt | 10 +- LiteDB/Core/LiteDatabase.cs | 16 +- LiteDB/DataStructure/Disks/FileDiskService.cs | 160 ++++++++++++++++-- LiteDB/DataStructure/Disks/IDiskService.cs | 6 +- LiteDB/DataStructure/Pages/BasePage.cs | 23 +-- LiteDB/DataStructure/Services/CacheService.cs | 20 ++- .../Services/TransactionService.cs | 24 ++- 7 files changed, 194 insertions(+), 65 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 2f0ed48db..eda3cf334 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,12 +1,14 @@ # LiteDB v.2 - TODO -- Journal -- BasePage.OriginalBuffer +- Revisar transação - Remover possibilidade + - Rollback continua existindo? Ou só commit? O que fazer quando ocorre erro na transacao (journal) + +- Toda operação do LiteDB agora é atomica (sem transacao) +- Queria que o IDiskService não tivesse informações sobre Journal/Recovery + - Revisar BsonArray.Length não faz cache -- Remover/Revisar transacao - Locks - Implementar StreamDiskService() - Thread Safe -- InitialSize para FileDiskService - Remove auto-register do shell diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index fcf150119..c9e20ec0e 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -30,11 +30,8 @@ public partial class LiteDatabase : IDisposable public BsonMapper Mapper { get; private set; } - public TimeSpan Timeout { get; private set; } - private LiteDatabase() { - this.Timeout = new TimeSpan(0, 1, 0); this.Mapper = BsonMapper.Global; } @@ -44,14 +41,17 @@ private LiteDatabase() public LiteDatabase(string connectionString) : this() { - var connStr = new ConnectionString(connectionString); + var str = new ConnectionString(connectionString); - this.Timeout = connStr.GetValue("timeout", this.Timeout); + var filename = str.GetValue("filename", ""); + var journal = str.GetValue("journal", true); + var timeout = str.GetValue("timeout", new TimeSpan(0, 1, 0)); + var readOnly = str.GetValue("readonly", false); + var password = str.GetValue("password", null); - var filename = connStr.GetValue("filename", ""); - var journal = connStr.GetValue("journal", true); + if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); - this.Disk = new FileDiskService(filename, journal, this.Timeout); + this.Disk = new FileDiskService(filename, journal, timeout, readOnly, password); this.Initialize(); } diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index c05baf0df..01a0347b4 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -17,37 +17,56 @@ internal class FileDiskService : IDiskService private FileStream _stream; private string _filename; + + private FileStream _journal; + private string _journalFilename; + private bool _journalEnabled; + private TimeSpan _timeout; - private bool _journal; - //private bool _readonly; + private bool _readonly; + private string _password; private byte[] _buffer = new byte[BasePage.PAGE_SIZE]; - public FileDiskService(string filename, bool journal, TimeSpan timeout) + public FileDiskService(string filename, bool journalEnabled, TimeSpan timeout, bool readOnly, string password) { _filename = filename; - _journal = journal; _timeout = timeout; + _readonly = readOnly; + _password = password; + + _journalEnabled = _readonly ? false : journalEnabled; // readonly? no journal + _journalFilename = Path.Combine(Path.GetDirectoryName(_filename), + Path.GetFileNameWithoutExtension(_filename) + "-journal" + + Path.GetExtension(_filename)); } - + /// /// Open datafile - returns true if new /// public bool Initialize() { - // open data file - _stream = new FileStream(_filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, BasePage.PAGE_SIZE); + // open data file (r/w or r) + _stream = new FileStream(_filename, + _readonly ? FileMode.Open : FileMode.OpenOrCreate, + _readonly ? FileAccess.Read : FileAccess.ReadWrite, + _readonly ? FileShare.Read : FileShare.ReadWrite, + BasePage.PAGE_SIZE); + + this.TryRecovery(); // returns true if new file return _stream.Length == 0; } + #region Lock/Unlock + /// /// Lock datafile agains other process read/write /// public void Lock() { - TryExec(() => + this.TryExec(() => _stream.Lock(LOCK_POSITION, 1) ); } @@ -60,6 +79,10 @@ public void Unlock() _stream.Unlock(LOCK_POSITION, 1); } + #endregion + + #region Read/Write + /// /// Read first 2 bytes from datafile - contains changeID (avoid to read all header page) /// @@ -78,7 +101,7 @@ public byte[] ReadPage(uint pageID) { var position = (long)pageID * (long)BasePage.PAGE_SIZE; - TryExec(() => + this.TryExec(() => { // position cursor if (_stream.Position != position) @@ -110,24 +133,49 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } - private void TryRecovery() - { - // se tiver journal, faz recovery - } + #endregion + + #region Journal file - public void ChangePage(uint pageID, byte[] original) + public void WriteJournal(uint pageID, byte[] data) { - // grava o journal-file + if(_journalEnabled == false) return; + + // open journal file if not used yet + if(_journal == null) + { + // open journal file in EXCLUSIVE mode + this.TryExec(() => + { + _journal = new FileStream(_journalFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); + }); + } + + //TODO: avisa q esta gerando + Console.WriteLine("journal write " + pageID); + + // just write original bytes in order that are changed + _journal.Write(data, 0, BasePage.PAGE_SIZE); } - public void StartWrite() + public void CommitJournal() { - // comita o journal file + if (_journalEnabled == false) return; + + // flush all journal file data to disk + _journal.Flush(); } - public void EndWrite() + public void DeleteJournal() { + if (_journalEnabled == false) return; // remove journal file + + // close journal stream and delete file + _journal.Dispose(); + _journal = null; + + File.Delete(_journalFilename); } public void Dispose() @@ -138,6 +186,60 @@ public void Dispose() } } + #endregion + + #region Recovery datafile + + private void TryRecovery() + { + // if I can open journal file, test FINISH_POSITION. If no journal, do not call action() + this.OpenExclusiveFile(_journalFilename, (journal) => + { + this.Recovery(journal); + + //TODO: valida se precisa ser feito recovery (FINISH) + + // close stream for delete file + journal.Close(); + + File.Delete(_journalFilename); + }); + } + + private void Recovery(FileStream journal) + { + var fileSize = _stream.Length; + var buffer = new byte[BasePage.PAGE_SIZE]; + + while (journal.Position < journal.Length) + { + // read page bytes from journal file + journal.Read(buffer, 0, BasePage.PAGE_SIZE); + + // read pageID (first 4 bytes) + var pageID = BitConverter.ToUInt32(buffer, 0); + + // if header, read all byte (to get original filesize) + if(pageID == 0) + { + var header = new HeaderPage(); + header.ReadPage(buffer); + fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; + } + + // write in stream + _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); + _stream.Write(buffer, 0, BasePage.PAGE_SIZE); + } + + // redim filesize if grow more than original before rollback + _stream.SetLength(fileSize); + } + + #endregion + + #region Utils + /// /// Try run an operation over datafile - keep tring if locked /// @@ -164,5 +266,27 @@ private void TryExec(Action action) throw LiteException.LockTimeout(_timeout); } + + private void OpenExclusiveFile(string filename, Action success) + { + // check if is using by another process, if not, call fn passing stream opened + try + { + using (var stream = File.Open(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + success(stream); + } + } + catch (FileNotFoundException) + { + // do nothing - no journal, no recovery + } + catch (IOException ex) + { + ex.WaitIfLocked(0); + } + } + + #endregion } } diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs index f5e9282fc..35380abed 100644 --- a/LiteDB/DataStructure/Disks/IDiskService.cs +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -15,9 +15,9 @@ public interface IDiskService : IDisposable void Lock(); void Unlock(); - void ChangePage(uint pageID, byte[] original); // WriteJournal() - void StartWrite(); // EndJournal - void EndWrite(); // DeleteJournal() + void WriteJournal(uint pageID, byte[] original); + void CommitJournal(); + void DeleteJournal(); byte[] ReadPage(uint pageID); void WritePage(uint pageID, byte[] buffer); diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index 0daa60ecf..0ee2e46bf 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -79,6 +79,7 @@ public BasePage() this.PageType = LiteDB.PageType.Empty; this.ItemCount = 0; this.FreeBytes = PAGE_AVAILABLE_BYTES; + this.DiskData = new byte[0]; } /// @@ -104,24 +105,6 @@ public virtual void Clear() this.DiskData = new byte[0]; } - /// - /// Create a new espefic page, copy all header content - /// - public T CopyTo() - where T : BasePage, new() - { - var page = new T(); - page.PageID = this.PageID; - page.PrevPageID = this.PrevPageID; - page.NextPageID = this.NextPageID; - page.PageType = this.PageType; - page.ItemCount = this.ItemCount; - page.FreeBytes = this.FreeBytes; - page.IsDirty = this.IsDirty; - - return page; - } - #region Read/Write page public virtual void ReadHeader(ByteReader reader) @@ -163,7 +146,7 @@ public void ReadPage(byte[] buffer) this.ReadContent(reader); } - //this.DiskData = buffer; + this.DiskData = buffer; } public byte[] WritePage() @@ -177,6 +160,8 @@ public byte[] WritePage() WriteContent(writer); } + this.DiskData = writer.Buffer; + return writer.Buffer; } diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 3c6aa793e..1909d1516 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -18,6 +18,8 @@ internal class CacheService : IDisposable private SortedDictionary _cache; private SortedDictionary _dirty; + public Action MarkAsDirtyAction = (page) => { }; + public CacheService() { _cache = new SortedDictionary(); @@ -59,21 +61,31 @@ public void AddPage(BasePage page, bool dirty = false) { page.IsDirty = true; _dirty[page.PageID] = page; - //TODO: dispara metodo de pagina alterada + + // if page is new (not exits on datafile), there is no journal for them + if(page.DiskData.Length > 0) + { + // call action passing dirty page - used for journal file writes + MarkAsDirtyAction(page); + } } } /// - /// Empty cache and header page + /// Empty cache and dirty pages - returns true if has dirty pages /// - public void Clear() + public bool Clear() { + var hasDirty = _dirty.Count > 0; + _dirty.Clear(); _cache.Clear(); + + return hasDirty; } /// - /// Clear dirty cache only + /// Set all pages as clear and remove them from dirty list /// public void ClearDirty() { diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 4611cdca6..50cb0e934 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -20,8 +20,7 @@ internal TransactionService(IDiskService disk, CacheService cache) { _disk = disk; _cache = cache; - - // _cache.OnPageChange((page) => _disk.PageChange(page.PageID, page.OriginalBuffer); + _cache.MarkAsDirtyAction = (page) => _disk.WriteJournal(page.PageID, page.DiskData); } public bool IsInTransaction { get { return _level > 0; } } @@ -79,16 +78,22 @@ public void Commit() _cache.AddPage(header, true); - _disk.StartWrite(); + // commit journal file - it will be used if write operation fails + _disk.CommitJournal(); + // write all dirty pages in data file foreach(var page in _cache.GetDirtyPages()) { Console.WriteLine("save dirty page " + page.PageID); _disk.WritePage(page.PageID, page.WritePage()); } - _disk.EndWrite(); + throw new Exception("erro corrompe file"); + + // delete journal file - datafile is consist here + _disk.DeleteJournal(); + // set all dirty pages as clear on cache _cache.ClearDirty(); } @@ -107,11 +112,12 @@ public void Rollback() { if (_level == 0) return; - // clear all pages from memory - _cache.Clear(); - - // tell disk service there is no more writes - _disk.EndWrite(); + // clear all pages from memory (return true if has dirty pages on cache) + if(_cache.Clear()) + { + // if has dirty page, has journal file - delete it (is not valid) + //_disk.DeleteJournal(); + } // unlock datafile _disk.Unlock(); From ba4eafa04d5e3ac3234cb7c9ae52ecb3212060c3 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 5 Nov 2015 14:03:11 -0200 Subject: [PATCH 18/91] Remove external acess to transaction --- LiteDB/Core/Collections/InsertBulk.cs | 2 +- LiteDB/Core/LiteDatabase.cs | 30 ------------------- .../Services/TransactionService.cs | 2 -- LiteDB/LiteDB.csproj | 3 -- LiteDB/Shell/Commands/Collections/Exec.cs | 6 ++-- LiteDB/Shell/Commands/Transactions/Begin.cs | 23 -------------- LiteDB/Shell/Commands/Transactions/Commit.cs | 23 -------------- .../Shell/Commands/Transactions/Rollback.cs | 23 -------------- 8 files changed, 4 insertions(+), 108 deletions(-) delete mode 100644 LiteDB/Shell/Commands/Transactions/Begin.cs delete mode 100644 LiteDB/Shell/Commands/Transactions/Commit.cs delete mode 100644 LiteDB/Shell/Commands/Transactions/Rollback.cs diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs index abf6e3ed1..760ebda3d 100644 --- a/LiteDB/Core/Collections/InsertBulk.cs +++ b/LiteDB/Core/Collections/InsertBulk.cs @@ -47,7 +47,7 @@ public int InsertBulk(IEnumerable docs, int buffer = 32768) } catch { - this.Database.Rollback(); + this.Database.Transaction.Rollback(); throw; } } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index c9e20ec0e..56093716e 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -188,36 +188,6 @@ public LiteFileStorage FileStorage #endregion - #region Transaction - - /// - /// Starts a new transaction. After this command, all write operations will be first in memory and will persist on disk - /// only when call Commit() method. If any error occurs, a Rollback() method will run. - /// - public void BeginTrans() - { - this.Transaction.Begin(); - } - - /// - /// Persist all changes on disk. Always use this method to finish your changes on database - /// - public void Commit() - { - this.Transaction.Commit(); - } - - /// - /// Cancel all write operations and keep datafile as is before BeginTrans() called. - /// Rollback are implicit on a database operation error, so you do not need call for database errors (only on business rules). - /// - public void Rollback() - { - this.Transaction.Rollback(); - } - - #endregion - #region Shell private LiteShell _shell = null; diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 50cb0e934..ec72dbbc9 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -88,8 +88,6 @@ public void Commit() _disk.WritePage(page.PageID, page.WritePage()); } - throw new Exception("erro corrompe file"); - // delete journal file - datafile is consist here _disk.DeleteJournal(); diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 70babd767..a0a409786 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -127,9 +127,6 @@ - - - diff --git a/LiteDB/Shell/Commands/Collections/Exec.cs b/LiteDB/Shell/Commands/Collections/Exec.cs index 21d8c884d..f4aa49be4 100644 --- a/LiteDB/Shell/Commands/Collections/Exec.cs +++ b/LiteDB/Shell/Commands/Collections/Exec.cs @@ -25,20 +25,20 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) try { - db.BeginTrans(); + db.Transaction.Begin(); foreach (var doc in docs) { code(doc["_id"], doc, col, db); } - db.Commit(); + db.Transaction.Commit(); return docs.Length; } catch (Exception ex) { - db.Rollback(); + db.Transaction.Rollback(); throw ex; } } diff --git a/LiteDB/Shell/Commands/Transactions/Begin.cs b/LiteDB/Shell/Commands/Transactions/Begin.cs deleted file mode 100644 index 9ce130a99..000000000 --- a/LiteDB/Shell/Commands/Transactions/Begin.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; - -namespace LiteDB.Shell.Commands -{ - internal class Begin : ILiteCommand - { - public bool IsCommand(StringScanner s) - { - return s.Scan(@"begin(\s+trans)?$").Length > 0; - } - - public BsonValue Execute(LiteDatabase db, StringScanner s) - { - db.BeginTrans(); - - return BsonValue.Null; - } - } -} diff --git a/LiteDB/Shell/Commands/Transactions/Commit.cs b/LiteDB/Shell/Commands/Transactions/Commit.cs deleted file mode 100644 index 388444a16..000000000 --- a/LiteDB/Shell/Commands/Transactions/Commit.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; - -namespace LiteDB.Shell.Commands -{ - internal class Commit : ILiteCommand - { - public bool IsCommand(StringScanner s) - { - return s.Scan(@"commit(\s+trans)?$").Length > 0; - } - - public BsonValue Execute(LiteDatabase db, StringScanner s) - { - db.Commit(); - - return BsonValue.Null; - } - } -} diff --git a/LiteDB/Shell/Commands/Transactions/Rollback.cs b/LiteDB/Shell/Commands/Transactions/Rollback.cs deleted file mode 100644 index cf07cb7c6..000000000 --- a/LiteDB/Shell/Commands/Transactions/Rollback.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; - -namespace LiteDB.Shell.Commands -{ - internal class Rollback : ILiteCommand - { - public bool IsCommand(StringScanner s) - { - return s.Scan(@"rollback(\s+trans)?$").Length > 0; - } - - public BsonValue Execute(LiteDatabase db, StringScanner s) - { - db.Rollback(); - - return BsonValue.Null; - } - } -} From f215a1db12d1deb14a2eb4efd0f97b7280a47ace Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 5 Nov 2015 14:16:40 -0200 Subject: [PATCH 19/91] Update my todos --- LiteDB-TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index eda3cf334..76bc2d339 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -10,6 +10,8 @@ - Implementar StreamDiskService() - Thread Safe - Remove auto-register do shell +- Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... + db.Debugger.TextWriters = Stream(); ## Cache From f18b9b0d333e204523a96459645272401b075ee6 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 5 Nov 2015 15:08:31 -0200 Subject: [PATCH 20/91] Update transaction --- LiteDB/DataStructure/Services/TransactionService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index ec72dbbc9..c0fb14172 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -76,6 +76,7 @@ public void Commit() // increase file changeID (back to 0 when overflow) header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); + // add header page as dirty to cache _cache.AddPage(header, true); // commit journal file - it will be used if write operation fails @@ -114,7 +115,7 @@ public void Rollback() if(_cache.Clear()) { // if has dirty page, has journal file - delete it (is not valid) - //_disk.DeleteJournal(); + _disk.DeleteJournal(); } // unlock datafile From 0af5a2e93e210b6dc40676d2a517de4d3fd00cd7 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 6 Nov 2015 19:58:06 -0200 Subject: [PATCH 21/91] Removing old BsonSerializer --- LiteDB-TODO.txt | 3 +- LiteDB.Tests/BsonPerfTest.cs | 71 --------- LiteDB.Tests/LiteDB.Tests.csproj | 1 - LiteDB/LiteDB.csproj | 9 +- LiteDB/Serializer/Bson/BsonReader.cs | 117 +++++++------- LiteDB/Serializer/Bson/BsonSerializer.cs | 19 +-- LiteDB/Serializer/Bson/BsonWriter.cs | 85 +++++------ LiteDB/Serializer/Bson2/BsonReader2.cs | 169 --------------------- LiteDB/Serializer/Bson2/BsonSerializer2.cs | 36 ----- LiteDB/Serializer/Bson2/BsonWriter2.cs | 155 ------------------- 10 files changed, 119 insertions(+), 546 deletions(-) delete mode 100644 LiteDB.Tests/BsonPerfTest.cs delete mode 100644 LiteDB/Serializer/Bson2/BsonReader2.cs delete mode 100644 LiteDB/Serializer/Bson2/BsonSerializer2.cs delete mode 100644 LiteDB/Serializer/Bson2/BsonWriter2.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 76bc2d339..87b0ea89d 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,7 +2,8 @@ - Revisar transação - Remover possibilidade - Rollback continua existindo? Ou só commit? O que fazer quando ocorre erro na transacao (journal) - + +- Revisar auto-id de Int32 (q usa collection) - Toda operação do LiteDB agora é atomica (sem transacao) - Queria que o IDiskService não tivesse informações sobre Journal/Recovery diff --git a/LiteDB.Tests/BsonPerfTest.cs b/LiteDB.Tests/BsonPerfTest.cs deleted file mode 100644 index a695244bc..000000000 --- a/LiteDB.Tests/BsonPerfTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; - -namespace UnitTest -{ - [TestClass] - public class BsonPerfTest - { - private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; - - [TestMethod] - public void BsonWritePerf_Test() - { - var o = JsonSerializer.Deserialize(json).AsDocument; - - var w1 = new Stopwatch(); - var w2 = new Stopwatch(); - - for(var i = 0; i < 20000; i++) - { - w1.Start(); - var bson1 = BsonSerializer.Serialize(o); - w1.Stop(); - - w2.Start(); - var bson2 = BsonSerializer2.Serialize(o); - w2.Stop(); - - } - - Debug.WriteLine("Time to write 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); - Debug.WriteLine("Time to write 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); - - } - - [TestMethod] - public void BsonReaderPerf_Test() - { - var bytes1 = BsonSerializer.Serialize(JsonSerializer.Deserialize(json).AsDocument); - var bytes2 = BsonSerializer2.Serialize(JsonSerializer.Deserialize(json).AsDocument); - - var w1 = new Stopwatch(); - var w2 = new Stopwatch(); - - for (var i = 0; i < 1000; i++) - { - w1.Start(); - var doc1 = BsonSerializer.Deserialize(bytes1); - w1.Stop(); - - w2.Start(); - var doc2 = BsonSerializer2.Deserialize(bytes2); - w2.Stop(); - - Assert.AreEqual(doc1.ToString(), doc2.ToString()); - } - - - - Debug.WriteLine("Time to read 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); - Debug.WriteLine("Time to read 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); - - - } - } -} \ No newline at end of file diff --git a/LiteDB.Tests/LiteDB.Tests.csproj b/LiteDB.Tests/LiteDB.Tests.csproj index 7dde2ec46..495ff3795 100644 --- a/LiteDB.Tests/LiteDB.Tests.csproj +++ b/LiteDB.Tests/LiteDB.Tests.csproj @@ -56,7 +56,6 @@ - diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index a0a409786..87e22e192 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -52,9 +52,9 @@ - - - + + + @@ -90,9 +90,6 @@ - - - diff --git a/LiteDB/Serializer/Bson/BsonReader.cs b/LiteDB/Serializer/Bson/BsonReader.cs index 2c8e8d3d1..625a28b7b 100644 --- a/LiteDB/Serializer/Bson/BsonReader.cs +++ b/LiteDB/Serializer/Bson/BsonReader.cs @@ -8,16 +8,65 @@ namespace LiteDB { + /// + /// Internal class to deserialize a byte[] into a BsonDocument using BSON data format + /// internal class BsonReader { - public BsonDocument Deserialize(Stream stream) + /// + /// Main method - deserialize using ByteReader helper + /// + public BsonDocument Deserialize(byte[] bson) { - var reader = new BinaryReader(stream); + return this.ReadDocument(new ByteReader(bson)); + } + + /// + /// Read a BsonDocument from reader + /// + private BsonDocument ReadDocument(ByteReader reader) + { + var length = reader.ReadInt32(); + var end = reader.Position + length - 5; + var obj = new BsonDocument(); - return this.ReadDocument(reader); + while (reader.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + obj.RawValue[name] = value; + } + + reader.ReadByte(); // zero + + return obj; } - private BsonValue ReadElement(BinaryReader reader, out string name) + /// + /// Read an BsonArray from reader + /// + private BsonArray ReadArray(ByteReader reader) + { + var length = reader.ReadInt32(); + var end = reader.Position + length - 5; + var arr = new BsonArray(); + + while (reader.Position < end) + { + string name; + var value = this.ReadElement(reader, out name); + arr.Add(value); + } + + reader.ReadByte(); // zero + + return arr; + } + + /// + /// Reads an element (key-value) from an reader + /// + private BsonValue ReadElement(ByteReader reader, out string name) { var type = reader.ReadByte(); name = this.ReadCString(reader); @@ -92,43 +141,7 @@ private BsonValue ReadElement(BinaryReader reader, out string name) throw new NotSupportedException("BSON type not supported"); } - public BsonDocument ReadDocument(BinaryReader reader) - { - var length = reader.ReadInt32(); - var end = (int)reader.BaseStream.Position + length - 1; - var obj = new BsonDocument(); - - while (reader.BaseStream.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - obj.RawValue[name] = value; - } - - reader.ReadByte(); // zero - - return obj; - } - - public BsonArray ReadArray(BinaryReader reader) - { - var length = reader.ReadInt32(); - var end = (int)reader.BaseStream.Position + length - 1; - var arr = new BsonArray(); - - while (reader.BaseStream.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - arr.Add(value); - } - - reader.ReadByte(); // zero - - return arr; - } - - private string ReadString(BinaryReader reader) + private string ReadString(ByteReader reader) { var length = reader.ReadInt32(); var bytes = reader.ReadBytes(length - 1); @@ -136,19 +149,21 @@ private string ReadString(BinaryReader reader) return Encoding.UTF8.GetString(bytes); } - private string ReadCString(BinaryReader reader) + // use byte array buffer for CString (key-only) + private byte[] _strBuffer = new byte[1000]; + + private string ReadCString(ByteReader reader) { - using (var ms = new MemoryStream()) - { - while (true) - { - byte buf = reader.ReadByte(); - if (buf == 0x00) break; - ms.WriteByte(buf); - } + var pos = 0; - return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Position); + while (true) + { + byte buf = reader.ReadByte(); + if (buf == 0x00) break; + _strBuffer[pos++] = buf; } + + return Encoding.UTF8.GetString(_strBuffer, 0, pos); } } } diff --git a/LiteDB/Serializer/Bson/BsonSerializer.cs b/LiteDB/Serializer/Bson/BsonSerializer.cs index 5d42d500c..873acb759 100644 --- a/LiteDB/Serializer/Bson/BsonSerializer.cs +++ b/LiteDB/Serializer/Bson/BsonSerializer.cs @@ -15,29 +15,22 @@ namespace LiteDB /// public class BsonSerializer { - public static byte[] Serialize(BsonDocument value) + public static byte[] Serialize(BsonDocument doc) { - if (value == null) throw new ArgumentNullException("value"); + if (doc == null) throw new ArgumentNullException("doc"); - using (var mem = new MemoryStream()) - { - var writer = new BsonWriter(); - writer.Serialize(mem, value); + var writer = new BsonWriter(); - return mem.ToArray(); - } + return writer.Serialize(doc); } public static BsonDocument Deserialize(byte[] bson) { if (bson == null || bson.Length == 0) throw new ArgumentNullException("bson"); - using (var mem = new MemoryStream(bson)) - { - var reader = new BsonReader(); + var reader = new BsonReader(); - return reader.Deserialize(mem); - } + return reader.Deserialize(bson); } } } diff --git a/LiteDB/Serializer/Bson/BsonWriter.cs b/LiteDB/Serializer/Bson/BsonWriter.cs index d4b6b1159..12d32d8fc 100644 --- a/LiteDB/Serializer/Bson/BsonWriter.cs +++ b/LiteDB/Serializer/Bson/BsonWriter.cs @@ -7,16 +7,52 @@ namespace LiteDB { + /// + /// Internal class to serialize a BsonDocument to BSON data format (byte[]) + /// internal class BsonWriter { - public void Serialize(Stream stream, BsonDocument value) + /// + /// Main method - serialize document. Uses ByteWriter + /// + public byte[] Serialize(BsonDocument doc) + { + var count = doc.GetBytesCount(true); + var writer = new ByteWriter(count); + + this.WriteDocument(writer, doc); + + return writer.Buffer; + } + + /// + /// Write a bson document + /// + public void WriteDocument(ByteWriter writer, BsonDocument doc) { - var writer = new BinaryWriter(stream); + writer.Write(doc.GetBytesCount(false)); - this.WriteDocument(writer, value); + foreach (var key in doc.Keys) + { + this.WriteElement(writer, key, doc[key] ?? BsonValue.Null); + } + + writer.Write((byte)0x00); } - private void WriteElement(BinaryWriter writer, string key, BsonValue value) + private void WriteArray(ByteWriter writer, BsonArray array) + { + writer.Write(array.GetBytesCount(false)); + + for (var i = 0; i < array.Count; i++) + { + this.WriteElement(writer, i.ToString(), array[i] ?? BsonValue.Null); + } + + writer.Write((byte)0x00); + } + + private void WriteElement(ByteWriter writer, string key, BsonValue value) { // cast RawValue to avoid one if on As switch (value.Type) @@ -101,44 +137,7 @@ private void WriteElement(BinaryWriter writer, string key, BsonValue value) } } - /// - /// Write a bson document - /// - internal void WriteDocument(BinaryWriter writer, BsonDocument doc) - { - using (var mem = new MemoryStream()) - { - var w = new BinaryWriter(mem); - - foreach (var key in doc.Keys) - { - this.WriteElement(w, key, doc[key] ?? BsonValue.Null); - } - - writer.Write((Int32)mem.Position); - writer.Write(mem.GetBuffer(), 0, (int)mem.Position); - writer.Write((byte)0x00); - } - } - - internal void WriteArray(BinaryWriter writer, BsonArray arr) - { - using (var mem = new MemoryStream()) - { - var w = new BinaryWriter(mem); - - for (var i = 0; i < arr.Count; i++) - { - this.WriteElement(w, i.ToString(), arr[i] ?? BsonValue.Null); - } - - writer.Write((Int32)mem.Position); - writer.Write(mem.GetBuffer(), 0, (int)mem.Position); - writer.Write((byte)0x00); - } - } - - private void WriteString(BinaryWriter writer, string s) + private void WriteString(ByteWriter writer, string s) { var bytes = Encoding.UTF8.GetBytes(s); writer.Write(bytes.Length + 1); @@ -146,7 +145,7 @@ private void WriteString(BinaryWriter writer, string s) writer.Write((byte)0x00); } - private void WriteCString(BinaryWriter writer, string s) + private void WriteCString(ByteWriter writer, string s) { var bytes = Encoding.UTF8.GetBytes(s); writer.Write(bytes); diff --git a/LiteDB/Serializer/Bson2/BsonReader2.cs b/LiteDB/Serializer/Bson2/BsonReader2.cs deleted file mode 100644 index 3de8b0e3b..000000000 --- a/LiteDB/Serializer/Bson2/BsonReader2.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace LiteDB -{ - /// - /// Internal class to deserialize a byte[] into a BsonDocument using BSON data format - /// - internal class BsonReader2 - { - /// - /// Main method - deserialize using ByteReader helper - /// - public BsonDocument Deserialize(byte[] bson) - { - return this.ReadDocument(new ByteReader(bson)); - } - - /// - /// Read a BsonDocument from reader - /// - private BsonDocument ReadDocument(ByteReader reader) - { - var length = reader.ReadInt32(); - var end = reader.Position + length - 5; - var obj = new BsonDocument(); - - while (reader.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - obj.RawValue[name] = value; - } - - reader.ReadByte(); // zero - - return obj; - } - - /// - /// Read an BsonArray from reader - /// - private BsonArray ReadArray(ByteReader reader) - { - var length = reader.ReadInt32(); - var end = reader.Position + length - 5; - var arr = new BsonArray(); - - while (reader.Position < end) - { - string name; - var value = this.ReadElement(reader, out name); - arr.Add(value); - } - - reader.ReadByte(); // zero - - return arr; - } - - /// - /// Reads an element (key-value) from an reader - /// - private BsonValue ReadElement(ByteReader reader, out string name) - { - var type = reader.ReadByte(); - name = this.ReadCString(reader); - - if (type == 0x01) // Double - { - return reader.ReadDouble(); - } - else if (type == 0x02) // String - { - return this.ReadString(reader); - } - else if (type == 0x03) // Document - { - return this.ReadDocument(reader); - } - else if (type == 0x04) // Array - { - return this.ReadArray(reader); - } - else if (type == 0x05) // Binary - { - var length = reader.ReadInt32(); - var subType = reader.ReadByte(); - var bytes = reader.ReadBytes(length); - - switch (subType) - { - case 0x00: return bytes; - case 0x04: return new Guid(bytes); - } - } - else if (type == 0x07) // ObjectId - { - return new ObjectId(reader.ReadBytes(12)); - } - else if (type == 0x08) // Boolean - { - return reader.ReadBoolean(); - } - else if (type == 0x09) // DateTime - { - var ts = reader.ReadInt64(); - - // catch specific values for MaxValue / MinValue #19 - if (ts == 253402300800000) return DateTime.MaxValue; - if (ts == -62135596800000) return DateTime.MinValue; - - return BsonValue.UnixEpoch.AddMilliseconds(ts).ToLocalTime(); - } - else if (type == 0x0A) // Null - { - return BsonValue.Null; - } - else if (type == 0x10) // Int32 - { - return reader.ReadInt32(); - } - else if (type == 0x12) // Int64 - { - return reader.ReadInt64(); - } - else if (type == 0xFF) // MinKey - { - return BsonValue.MinValue; - } - else if (type == 0x7F) // MaxKey - { - return BsonValue.MaxValue; - } - - throw new NotSupportedException("BSON type not supported"); - } - - private string ReadString(ByteReader reader) - { - var length = reader.ReadInt32(); - var bytes = reader.ReadBytes(length - 1); - reader.ReadByte(); // discard \x00 - return Encoding.UTF8.GetString(bytes); - } - - // use byte array buffer for CString (key-only) - private byte[] _strBuffer = new byte[1000]; - - private string ReadCString(ByteReader reader) - { - var pos = 0; - - while (true) - { - byte buf = reader.ReadByte(); - if (buf == 0x00) break; - _strBuffer[pos++] = buf; - } - - return Encoding.UTF8.GetString(_strBuffer, 0, pos); - } - } -} diff --git a/LiteDB/Serializer/Bson2/BsonSerializer2.cs b/LiteDB/Serializer/Bson2/BsonSerializer2.cs deleted file mode 100644 index 930a43974..000000000 --- a/LiteDB/Serializer/Bson2/BsonSerializer2.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.Serialization.Formatters.Binary; -using System.Text; -using System.Text.RegularExpressions; - -namespace LiteDB -{ - /// - /// Class to call method for convert BsonDocument to/from byte[] - based on http://bsonspec.org/spec.html - /// - public class BsonSerializer2 - { - public static byte[] Serialize(BsonDocument doc) - { - if (doc == null) throw new ArgumentNullException("doc"); - - var writer = new BsonWriter2(); - - return writer.Serialize(doc); - } - - public static BsonDocument Deserialize(byte[] bson) - { - if (bson == null || bson.Length == 0) throw new ArgumentNullException("bson"); - - var reader = new BsonReader2(); - - return reader.Deserialize(bson); - } - } -} diff --git a/LiteDB/Serializer/Bson2/BsonWriter2.cs b/LiteDB/Serializer/Bson2/BsonWriter2.cs deleted file mode 100644 index b46076389..000000000 --- a/LiteDB/Serializer/Bson2/BsonWriter2.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - /// - /// Internal class to serialize a BsonDocument to BSON data format (byte[]) - /// - internal class BsonWriter2 - { - /// - /// Main method - serialize document. Uses ByteWriter - /// - public byte[] Serialize(BsonDocument doc) - { - var count = doc.GetBytesCount(true); - var writer = new ByteWriter(count); - - this.WriteDocument(writer, doc); - - return writer.Buffer; - } - - /// - /// Write a bson document - /// - public void WriteDocument(ByteWriter writer, BsonDocument doc) - { - writer.Write(doc.GetBytesCount(false)); - - foreach (var key in doc.Keys) - { - this.WriteElement(writer, key, doc[key] ?? BsonValue.Null); - } - - writer.Write((byte)0x00); - } - - private void WriteArray(ByteWriter writer, BsonArray array) - { - writer.Write(array.GetBytesCount(false)); - - for (var i = 0; i < array.Count; i++) - { - this.WriteElement(writer, i.ToString(), array[i] ?? BsonValue.Null); - } - - writer.Write((byte)0x00); - } - - private void WriteElement(ByteWriter writer, string key, BsonValue value) - { - // cast RawValue to avoid one if on As - switch (value.Type) - { - case BsonType.Double: - writer.Write((byte)0x01); - this.WriteCString(writer, key); - writer.Write((Double)value.RawValue); - break; - case BsonType.String: - writer.Write((byte)0x02); - this.WriteCString(writer, key); - this.WriteString(writer, (String)value.RawValue); - break; - case BsonType.Document: - writer.Write((byte)0x03); - this.WriteCString(writer, key); - this.WriteDocument(writer, new BsonDocument((Dictionary)value.RawValue)); - break; - case BsonType.Array: - writer.Write((byte)0x04); - this.WriteCString(writer, key); - this.WriteArray(writer, new BsonArray((List)value.RawValue)); - break; - case BsonType.Binary: - writer.Write((byte)0x05); - this.WriteCString(writer, key); - var bytes = (byte[])value.RawValue; - writer.Write(bytes.Length); - writer.Write((byte)0x00); // subtype 00 - Generic binary subtype - writer.Write(bytes); - break; - case BsonType.Guid: - writer.Write((byte)0x05); - this.WriteCString(writer, key); - var guid = ((Guid)value.RawValue).ToByteArray(); - writer.Write(guid.Length); - writer.Write((byte)0x04); // UUID - writer.Write(guid); - break; - case BsonType.ObjectId: - writer.Write((byte)0x07); - this.WriteCString(writer, key); - writer.Write(((ObjectId)value.RawValue).ToByteArray()); - break; - case BsonType.Boolean: - writer.Write((byte)0x08); - this.WriteCString(writer, key); - writer.Write((byte)(((Boolean)value.RawValue) ? 0x01 : 0x00)); - break; - case BsonType.DateTime: - writer.Write((byte)0x09); - this.WriteCString(writer, key); - var date = (DateTime)value.RawValue; - // do not convert to UTC min/max date values - #19 - var utc = (date == DateTime.MinValue || date == DateTime.MaxValue) ? date : date.ToUniversalTime(); - var ts = utc - BsonValue.UnixEpoch; - writer.Write(Convert.ToInt64(ts.TotalMilliseconds)); - break; - case BsonType.Null: - writer.Write((byte)0x0A); - this.WriteCString(writer, key); - break; - case BsonType.Int32: - writer.Write((byte)0x10); - this.WriteCString(writer, key); - writer.Write((Int32)value.RawValue); - break; - case BsonType.Int64: - writer.Write((byte)0x12); - this.WriteCString(writer, key); - writer.Write((Int64)value.RawValue); - break; - case BsonType.MinValue: - writer.Write((byte)0xFF); - this.WriteCString(writer, key); - break; - case BsonType.MaxValue: - writer.Write((byte)0x7F); - this.WriteCString(writer, key); - break; - } - } - - private void WriteString(ByteWriter writer, string s) - { - var bytes = Encoding.UTF8.GetBytes(s); - writer.Write(bytes.Length + 1); - writer.Write(bytes); - writer.Write((byte)0x00); - } - - private void WriteCString(ByteWriter writer, string s) - { - var bytes = Encoding.UTF8.GetBytes(s); - writer.Write(bytes); - writer.Write((byte)0x00); - } - } -} From 4155a1f9bf4573b472ec4b95160539a117450c42 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 7 Nov 2015 22:30:19 -0200 Subject: [PATCH 22/91] Fix collection method --- LiteDB-TODO.txt | 93 ++++++++++- LiteDB/Core/Collections/Aggregate.cs | 156 ++++++++++++++++++ LiteDB/Core/Collections/Find.cs | 138 +--------------- .../{FindIndex.cs => FindAndModify.cs} | 0 LiteDB/Core/Collections/Index.cs | 37 +++++ LiteDB/Core/Collections/Insert.cs | 36 ++-- LiteDB/Core/Collections/LiteCollection.cs | 2 +- LiteDB/Core/Collections/Rename.cs | 46 ++++++ LiteDB/Core/Collections/Update.cs | 4 +- LiteDB/Core/LiteDatabase.cs | 25 +-- LiteDB/LiteDB.csproj | 4 +- LiteDB/Query/Query.cs | 33 +--- LiteDB/Serializer/Mapper/BsonMapper.cs | 8 + LiteDB/Utils/LiteException.cs | 9 +- 14 files changed, 379 insertions(+), 212 deletions(-) create mode 100644 LiteDB/Core/Collections/Aggregate.cs rename LiteDB/Core/Collections/{FindIndex.cs => FindAndModify.cs} (100%) create mode 100644 LiteDB/Core/Collections/Rename.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 87b0ea89d..6a310a47a 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -4,8 +4,8 @@ - Rollback continua existindo? Ou só commit? O que fazer quando ocorre erro na transacao (journal) - Revisar auto-id de Int32 (q usa collection) -- Toda operação do LiteDB agora é atomica (sem transacao) - Queria que o IDiskService não tivesse informações sobre Journal/Recovery +- FindAndModify(Query, Action) - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() @@ -13,7 +13,14 @@ - Remove auto-register do shell - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.TextWriters = Stream(); + db.Debbuger.Counters["Abc"].Times | Count +- Adicionar as operacoes do banco para um servico - manter no Collection apenas as chamadas + Linq conv + API + - DocumentService (_docsrv) * Insert/Update/Delete/EnsureIndex/Find + - Não contempla transacao + - + +A gestão da transacao deve ser feita sempre no metodo mais externo (begin/commit/roll/avoidDrty) ## Cache @@ -53,3 +60,87 @@ e) Leitura conforme indice (retorno poucos registros) - Mapeador/Configuration estilo EF db.Configuration.Add(new CredorConfiguration()) +## Mapping + ++ Cai fora a forma de implementar hoje com RegisterType (_customSerializer) ? Acho que não, vide Uri + +db.Mapper.Entity() + .HasKey(x => x.IdUsuario) + .Property(x => x.NoUsuario, "nome_usuario") + .Property(x => x.CdUsuario, "codigo_usuario", (x) => x.CdUsuario, (bson) => bson) + .Property(Expression, FieldName, Func serialize, Func deserialize) + .Ignore(x => x.FgAtivo) + .Index(x => x.NoUsuario) + + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; + +namespace UnitTest +{ + [TestClass] + public class BsonPerfTest + { + private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; + + [TestMethod] + public void BsonWritePerf_Test() + { + var o = JsonSerializer.Deserialize(json).AsDocument; + + var w1 = new Stopwatch(); + var w2 = new Stopwatch(); + + for(var i = 0; i < 20000; i++) + { + w1.Start(); + var bson1 = BsonSerializer2.Serialize(o); + w1.Stop(); + + w2.Start(); + var bson2 = BsonSerializer2.Serialize(o); + w2.Stop(); + + } + + Debug.WriteLine("Time to write 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to write 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + + } + + [TestMethod] + public void BsonReaderPerf_Test() + { + var bytes1 = BsonSerializer.Serialize(JsonSerializer.Deserialize(json).AsDocument); + var bytes2 = BsonSerializer2.Serialize(JsonSerializer.Deserialize(json).AsDocument); + + var w1 = new Stopwatch(); + var w2 = new Stopwatch(); + + for (var i = 0; i < 1000; i++) + { + w1.Start(); + var doc1 = BsonSerializer.Deserialize(bytes1); + w1.Stop(); + + w2.Start(); + var doc2 = BsonSerializer2.Deserialize(bytes2); + w2.Stop(); + + Assert.AreEqual(doc1.ToString(), doc2.ToString()); + } + + + + Debug.WriteLine("Time to read 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); + Debug.WriteLine("Time to read 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); + + + } + } +} \ No newline at end of file diff --git a/LiteDB/Core/Collections/Aggregate.cs b/LiteDB/Core/Collections/Aggregate.cs new file mode 100644 index 000000000..11aad1d30 --- /dev/null +++ b/LiteDB/Core/Collections/Aggregate.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace LiteDB +{ + public partial class LiteCollection + { + #region Count/Exits + + /// + /// Get document count using property on collection. + /// + public int Count() + { + this.Database.Transaction.AvoidDirtyRead(); + + var col = this.GetCollectionPage(false); + + if (col == null) return 0; + + return Convert.ToInt32(col.DocumentCount); + } + + /// + /// Count documnets with a query. This method does not deserialize any document. Needs indexes on query expression + /// + public int Count(Query query) + { + if (query == null) throw new ArgumentNullException("query"); + + this.Database.Transaction.AvoidDirtyRead(); + + var nodes = query.Run(this); + + return nodes.Count(); + } + + /// + /// Count documnets with a query. This method does not deserialize any document. Needs indexes on query expression + /// + public int Count(Expression> predicate) + { + return this.Count(_visitor.Visit(predicate)); + } + + /// + /// Returns true if query returns any document. This method does not deserialize any document. Needs indexes on query expression + /// + public bool Exists(Query query) + { + if (query == null) throw new ArgumentNullException("query"); + + this.Database.Transaction.AvoidDirtyRead(); + + var nodes = query.Run(this); + + return nodes.FirstOrDefault() != null; + } + + /// + /// Returns true if query returns any document. This method does not deserialize any document. Needs indexes on query expression + /// + public bool Exists(Expression> predicate) + { + return this.Exists(_visitor.Visit(predicate)); + } + + #endregion + + #region Min/Max + + /// + /// Returns the first/min value from a index field + /// + public BsonValue Min(string field) + { + if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); + + this.Database.Transaction.AvoidDirtyRead(); + + var index = this.GetOrCreateIndex(field); + + if (index == null) return BsonValue.MinValue; + + var head = this.Database.Indexer.GetNode(index.HeadNode); + var next = this.Database.Indexer.GetNode(head.Next[0]); + + if (next.IsHeadTail(index)) return BsonValue.MinValue; + + return next.Key; + } + + /// + /// Returns the first/min _id field + /// + public BsonValue Min() + { + return this.Min("_id"); + } + + /// + /// Returns the first/min field using a linq expression + /// + public BsonValue Min(Expression> property) + { + var field = _visitor.GetBsonField(property); + + return this.Min(field); + } + + /// + /// Returns the last/max value from a index field + /// + public BsonValue Max(string field) + { + if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); + + this.Database.Transaction.AvoidDirtyRead(); + + var index = this.GetOrCreateIndex(field); + + if (index == null) return BsonValue.MinValue; + + var tail = this.Database.Indexer.GetNode(index.TailNode); + var prev = this.Database.Indexer.GetNode(tail.Prev[0]); + + if (prev.IsHeadTail(index)) return BsonValue.MaxValue; + + return prev.Key; + } + + /// + /// Returns the last/max _id field + /// + public BsonValue Max() + { + return this.Max("_id"); + } + + /// + /// Returns the last/max field using a linq expression + /// + public BsonValue Max(Expression> property) + { + var field = _visitor.GetBsonField(property); + + return this.Max(field); + } + + #endregion + } +} diff --git a/LiteDB/Core/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs index 0e490ab9a..2c8ad093e 100644 --- a/LiteDB/Core/Collections/Find.cs +++ b/LiteDB/Core/Collections/Find.cs @@ -18,6 +18,8 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) { if (query == null) throw new ArgumentNullException("query"); + this.Database.Transaction.AvoidDirtyRead(); + var nodes = query.Run(this); if (skip > 0) nodes = nodes.Skip(skip); @@ -89,141 +91,5 @@ public IEnumerable FindAll() } #endregion - - #region Count/Exits - - /// - /// Get document count using property on collection. - /// - public int Count() - { - var col = this.GetCollectionPage(false); - - if (col == null) return 0; - - return Convert.ToInt32(col.DocumentCount); - } - - /// - /// Count documnets with a query. This method does not deserialize any document. Needs indexes on query expression - /// - public int Count(Query query) - { - if (query == null) throw new ArgumentNullException("query"); - - var nodes = query.Run(this); - - return nodes.Count(); - } - - /// - /// Count documnets with a query. This method does not deserialize any document. Needs indexes on query expression - /// - public int Count(Expression> predicate) - { - return this.Count(_visitor.Visit(predicate)); - } - - /// - /// Returns true if query returns any document. This method does not deserialize any document. Needs indexes on query expression - /// - public bool Exists(Query query) - { - if (query == null) throw new ArgumentNullException("query"); - - var nodes = query.Run(this); - - return nodes.FirstOrDefault() != null; - } - - /// - /// Returns true if query returns any document. This method does not deserialize any document. Needs indexes on query expression - /// - public bool Exists(Expression> predicate) - { - return this.Exists(_visitor.Visit(predicate)); - } - - #endregion - - #region Min/Max - - /// - /// Returns the first/min value from a index field - /// - public BsonValue Min(string field) - { - if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - - var col = this.GetCollectionPage(false); - - if (col == null) return BsonValue.MinValue; - - var index = col.GetIndex(field); - var head = this.Database.Indexer.GetNode(index.HeadNode); - var next = this.Database.Indexer.GetNode(head.Next[0]); - - if (next.IsHeadTail(index)) return BsonValue.MinValue; - - return next.Key; - } - - /// - /// Returns the first/min _id field - /// - public BsonValue Min() - { - return this.Min("_id"); - } - - /// - /// Returns the first/min field using a linq expression - /// - public BsonValue Min(Expression> property) - { - var field = _visitor.GetBsonField(property); - - return this.Min(field); - } - - /// - /// Returns the last/max value from a index field - /// - public BsonValue Max(string field) - { - if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - - var col = this.GetCollectionPage(false); - - if (col == null) return BsonValue.MaxValue; - - var index = col.GetIndex(field); - var tail = this.Database.Indexer.GetNode(index.TailNode); - var prev = this.Database.Indexer.GetNode(tail.Prev[0]); - - if (prev.IsHeadTail(index)) return BsonValue.MaxValue; - - return prev.Key; - } - - /// - /// Returns the last/max _id field - /// - public BsonValue Max() - { - return this.Max("_id"); - } - - /// - /// Returns the last/max field using a linq expression - /// - public BsonValue Max(Expression> property) - { - var field = _visitor.GetBsonField(property); - - return this.Max(field); - } - - #endregion } } diff --git a/LiteDB/Core/Collections/FindIndex.cs b/LiteDB/Core/Collections/FindAndModify.cs similarity index 100% rename from LiteDB/Core/Collections/FindIndex.cs rename to LiteDB/Core/Collections/FindAndModify.cs diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index f20ac56d8..2a8532753 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -152,6 +152,43 @@ public IEnumerable GetIndexes() } } + internal CollectionIndex GetOrCreateIndex(string field) + { + // get collection page that contains all indexes + var col = this.GetCollectionPage(false); + + // no collection, no index + if(col == null) return null; + + // get index + var index = col.GetIndex(field); + + // if index not found, lets check if type T has [BsonIndex] + if (index == null && typeof(T) != typeof(BsonDocument)) + { + var options = this.Database.Mapper.GetIndexFromAttribute(field); + + // create a new index using BsonIndex options + if (options != null) + { + this.EnsureIndex(field, options); + + index = col.GetIndex(field); + } + } + + // if no index, let's auto create an index with default index options + if (index == null) + { + this.EnsureIndex(field); + + index = col.GetIndex(field); + } + + return index; + } + + /// /// Drop index and release slot for another index /// diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 6864c52ac..d49132635 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -15,29 +15,33 @@ public virtual BsonValue Insert(T document) { if (document == null) throw new ArgumentNullException("document"); - // set an id value if document object needs - this.Database.Mapper.SetAutoId(document, this.GetBsonCollection()); + this.Database.Transaction.Begin(); - var doc = this.Database.Mapper.ToDocument(document); + try + { + // set an id value if document object needs + this.Database.Mapper.SetAutoId(document, this.GetBsonCollection()); - BsonValue id; + var doc = this.Database.Mapper.ToDocument(document); - // add ObjectId to _id if _id not found - if (!doc.RawValue.TryGetValue("_id", out id)) - { - id = doc["_id"] = ObjectId.NewObjectId(); - } + BsonValue id; - // test if _id is a valid type - if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); + // add ObjectId to _id if _id not found + if (!doc.RawValue.TryGetValue("_id", out id)) + { + id = doc["_id"] = ObjectId.NewObjectId(); + } - // serialize object - var bytes = BsonSerializer.Serialize(doc); + // test if _id is a valid type + if (id.IsNull || id.IsMinValue || id.IsMaxValue) + { + this.Database.Transaction.Abort(); + throw LiteException.InvalidDataType("_id", id); + } - this.Database.Transaction.Begin(); + // serialize object + var bytes = BsonSerializer.Serialize(doc); - try - { var col = this.GetCollectionPage(true); // storage in data pages - returns dataBlock address diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 319d70eb6..9e565b514 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -71,7 +71,7 @@ internal CollectionPage GetCollectionPage(bool addIfNotExits) } /// - /// Returns a new instance of this collection but using BsonDocument as T - Copy _pageID to avoid new collection page search + /// Returns a new instance of this collection but using BsonDocument insted T - Copy _pageID to avoid new collection page search /// internal LiteCollection GetBsonCollection() { diff --git a/LiteDB/Core/Collections/Rename.cs b/LiteDB/Core/Collections/Rename.cs new file mode 100644 index 000000000..d7fa689a0 --- /dev/null +++ b/LiteDB/Core/Collections/Rename.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + public partial class LiteCollection + { + /// + /// Drop a collection deleting all documents and indexes + /// + public bool Rename(string newName) + { + this.Database.Transaction.Begin(); + + try + { + // get collection page + var col = this.GetCollectionPage(false); + + if (col == null) + { + this.Database.Transaction.Abort(); + return false; + } + + // change collection name + col.CollectionName = newName; + + // set page as dirty + this.Database.Pager.SetDirty(col); + + this.Database.Transaction.Commit(); + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } + + return true; + } + } +} diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index 6b0d4b13a..9f0183483 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -11,7 +11,7 @@ public partial class LiteCollection /// /// Update a document in this collection. Returns false if not found document in collection /// - public virtual bool Update(T document) + public bool Update(T document) { if (document == null) throw new ArgumentNullException("document"); @@ -28,7 +28,7 @@ public virtual bool Update(T document) /// /// Update a document in this collection. Returns false if not found document in collection /// - public virtual bool Update(BsonValue id, T document) + public bool Update(BsonValue id, T document) { if (document == null) throw new ArgumentNullException("document"); if (id == null || id.IsNull) throw new ArgumentNullException("id"); diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 56093716e..a1f83d4c4 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -146,30 +146,7 @@ public bool DropCollection(string name) /// public bool RenameCollection(string oldName, string newName) { - this.Transaction.Begin(); - - try - { - var col = this.Collections.Get(oldName); - - if (col == null || this.CollectionExists(newName)) - { - this.Transaction.Abort(); - return false; - } - - col.CollectionName = newName; - this.Pager.SetDirty(col); - - this.Transaction.Commit(); - } - catch - { - this.Transaction.Rollback(); - throw; - } - - return true; + return this.GetCollection(oldName).Rename(newName); } #endregion diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 87e22e192..141df3048 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -50,7 +50,9 @@ - + + + diff --git a/LiteDB/Query/Query.cs b/LiteDB/Query/Query.cs index 36681ce55..ef82960db 100644 --- a/LiteDB/Query/Query.cs +++ b/LiteDB/Query/Query.cs @@ -169,41 +169,16 @@ public static Query Or(Query left, Query right) internal abstract IEnumerable ExecuteIndex(IndexService indexer, CollectionIndex index); /// - /// Find witch index will be used and run Execute method - define ExecuteMode here + /// Find witch index will be used and run Execute method /// internal virtual IEnumerable Run(LiteCollection collection) where T : new() { - // get collection page - no collection, no results - var col = collection.GetCollectionPage(false); + // get or create new index for Field + var index = collection.GetOrCreateIndex(this.Field); // no collection just returns an empty list of indexnode - if (col == null) return new List(); - - // get index - var index = col.GetIndex(this.Field); - - // if index not found, lets check if type T has [BsonIndex] - if (index == null && typeof(T) != typeof(BsonDocument)) - { - var options = collection.Database.Mapper.GetIndexFromAttribute(this.Field); - - // create a new index using BsonIndex options - if (options != null) - { - collection.EnsureIndex(this.Field, options); - - index = col.GetIndex(this.Field); - } - } - - // if no index, let's auto create an index with default index options - if (index == null) - { - collection.EnsureIndex(this.Field); - - index = col.GetIndex(this.Field); - } + if (index == null) return new List(); // execute query to get all IndexNodes return this.ExecuteIndex(collection.Database.Indexer, index); diff --git a/LiteDB/Serializer/Mapper/BsonMapper.cs b/LiteDB/Serializer/Mapper/BsonMapper.cs index d03a7ac67..d01cf5c43 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.cs @@ -70,6 +70,8 @@ public BsonMapper() this.EmptyStringToNull = true; this.ResolvePropertyName = (s) => s; + #region Register CustomTypes + // register custom types this.RegisterType ( @@ -104,6 +106,10 @@ public BsonMapper() } ); + #endregion + + #region Register custom AutoId functions + // register AutoId for ObjectId, Guid and Int32 this.RegisterAutoId ( @@ -126,6 +132,8 @@ public BsonMapper() return max.IsMaxValue ? 1 : (max + 1); } ); + + #endregion } /// diff --git a/LiteDB/Utils/LiteException.cs b/LiteDB/Utils/LiteException.cs index 04ca197c5..4d8e01a81 100644 --- a/LiteDB/Utils/LiteException.cs +++ b/LiteDB/Utils/LiteException.cs @@ -87,14 +87,19 @@ public static LiteException IndexKeyTooLong() return new LiteException(111, "Index key must be less than {0} bytes", IndexService.MAX_INDEX_LENGTH); } + public static LiteException IndexNotFound(string name) + { + return new LiteException(112, "Index not found on '{0}'", name); + } + public static LiteException LockTimeout(TimeSpan ts) { - return new LiteException(112, "Timeout. Database is locked for more than {0}", ts.ToString()); + return new LiteException(120, "Timeout. Database is locked for more than {0}", ts.ToString()); } public static LiteException InvalidCommand(string command) { - return new LiteException(113, "Command '{0}' is not a valid shell command", command); + return new LiteException(121, "Command '{0}' is not a valid shell command", command); } #endregion From 8b80b4f537c16c4812dc7b6ce44bdc77d788c7e0 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 8 Nov 2015 01:24:45 -0200 Subject: [PATCH 23/91] Bugfix on read data page --- LiteDB-TODO.txt | 3 ++- LiteDB/DataStructure/Disks/FileDiskService.cs | 11 +++++----- LiteDB/DataStructure/Services/CacheService.cs | 4 ++-- .../DataStructure/Services/DocumentService.cs | 20 +++++++++++++++++++ LiteDB/DataStructure/Services/PageService.cs | 6 +++++- .../Services/TransactionService.cs | 4 ++-- LiteDB/LiteDB.csproj | 1 + 7 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 LiteDB/DataStructure/Services/DocumentService.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 6a310a47a..de749f3b0 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -16,7 +16,8 @@ db.Debbuger.Counters["Abc"].Times | Count - Adicionar as operacoes do banco para um servico - manter no Collection apenas as chamadas + Linq conv + API - - DocumentService (_docsrv) * Insert/Update/Delete/EnsureIndex/Find + - DocumentService (_docsrv) * Insert/Update/Delete/Find + - EnsureIndex/Find - Não contempla transacao - diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 01a0347b4..31bf1773c 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -26,8 +26,6 @@ internal class FileDiskService : IDiskService private bool _readonly; private string _password; - private byte[] _buffer = new byte[BasePage.PAGE_SIZE]; - public FileDiskService(string filename, bool journalEnabled, TimeSpan timeout, bool readOnly, string password) { _filename = filename; @@ -99,6 +97,7 @@ public ushort GetChangeID() /// public byte[] ReadPage(uint pageID) { + var buffer = new byte[BasePage.PAGE_SIZE]; var position = (long)pageID * (long)BasePage.PAGE_SIZE; this.TryExec(() => @@ -110,11 +109,11 @@ public byte[] ReadPage(uint pageID) } // read bytes from data file - _stream.Read(_buffer, 0, BasePage.PAGE_SIZE); + _stream.Read(buffer, 0, BasePage.PAGE_SIZE); }); - return _buffer; + return buffer; } /// @@ -169,12 +168,12 @@ public void CommitJournal() public void DeleteJournal() { if (_journalEnabled == false) return; - // remove journal file // close journal stream and delete file _journal.Dispose(); _journal = null; + // remove journal file File.Delete(_journalFilename); } @@ -227,6 +226,8 @@ private void Recovery(FileStream journal) fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } + Console.WriteLine("recovery " + pageID); + // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); _stream.Write(buffer, 0, BasePage.PAGE_SIZE); diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 1909d1516..be822f901 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -61,6 +61,7 @@ public void AddPage(BasePage page, bool dirty = false) { page.IsDirty = true; _dirty[page.PageID] = page; + _cache[page.PageID] = page; // if page is new (not exits on datafile), there is no journal for them if(page.DiskData.Length > 0) @@ -72,7 +73,7 @@ public void AddPage(BasePage page, bool dirty = false) } /// - /// Empty cache and dirty pages - returns true if has dirty pages + /// Empty cache and dirty pages - returns true if had dirty pages /// public bool Clear() { @@ -89,7 +90,6 @@ public bool Clear() /// public void ClearDirty() { - // clear page and clear special list (will keep in _cache list) foreach(var page in _dirty.Values) { page.IsDirty = false; diff --git a/LiteDB/DataStructure/Services/DocumentService.cs b/LiteDB/DataStructure/Services/DocumentService.cs new file mode 100644 index 000000000..c2db62967 --- /dev/null +++ b/LiteDB/DataStructure/Services/DocumentService.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + /// + /// Highest level services - take care all basics operation about a document: CRUD+Index + /// + internal class DocumentService + { + + + internal DocumentService() + { + } + } +} diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs index 089115131..795579f59 100644 --- a/LiteDB/DataStructure/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -30,7 +30,11 @@ public T GetPage(uint pageID) { page = new T(); - page.ReadPage(_disk.ReadPage(pageID)); + var buffer = _disk.ReadPage(pageID); + + page.ReadPage(buffer); + + Console.WriteLine("read " + pageID); _cache.AddPage(page); } diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index c0fb14172..25f5a00d2 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -83,9 +83,9 @@ public void Commit() _disk.CommitJournal(); // write all dirty pages in data file - foreach(var page in _cache.GetDirtyPages()) + foreach (var page in _cache.GetDirtyPages()) { - Console.WriteLine("save dirty page " + page.PageID); + Console.WriteLine("save page " + page.PageID); _disk.WritePage(page.PageID, page.WritePage()); } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 141df3048..8a1bada57 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -54,6 +54,7 @@ + From 354f12692bf584291f25c9cff3bc36312acf595e Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 8 Nov 2015 09:51:30 -0200 Subject: [PATCH 24/91] Add EntityBuilder fluent api #12 --- LiteDB.Tests/MapperTest.cs | 35 ++++++- LiteDB/LiteDB.csproj | 1 + LiteDB/Serializer/Mapper/BsonMapper.cs | 10 ++ LiteDB/Serializer/Mapper/EntityBuilder.cs | 113 ++++++++++++++++++++++ LiteDB/Serializer/Mapper/Reflection.cs | 2 +- 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 LiteDB/Serializer/Mapper/EntityBuilder.cs diff --git a/LiteDB.Tests/MapperTest.cs b/LiteDB.Tests/MapperTest.cs index d64dd0246..a62eb2c69 100644 --- a/LiteDB.Tests/MapperTest.cs +++ b/LiteDB.Tests/MapperTest.cs @@ -65,8 +65,6 @@ public class MyClass public List MyObjectList { get; set; } } - - public interface IMyInterface { string Name { get; set; } @@ -77,6 +75,14 @@ public class MyImpl : IMyInterface public string Name { get; set; } } + public class MyFluentEntity + { + public int MyPrimaryPk { get; set; } + public string CustomName { get; set; } + public bool DoNotIncludeMe { get; set; } + public DateTime MyDateIndexed { get; set; } + } + [TestClass] public class MapperTest { @@ -177,5 +183,30 @@ public void Mapper_Test() Assert.AreEqual(nobj.MyInternalProperty, null); } + + [TestMethod] + public void MapperEntityBuilder_Test() + { + var mapper = new BsonMapper(); + + var obj = new MyFluentEntity { MyPrimaryPk = 1, CustomName = "John", DoNotIncludeMe = true, MyDateIndexed = DateTime.Now }; + + mapper.Entity() + .Key(x => x.MyPrimaryPk) + .Map(x => x.CustomName, "custom_field_name") + .Ignore(x => x.DoNotIncludeMe) + .Index(x => x.MyDateIndexed, true); + + var doc = mapper.ToDocument(obj); + + var json = JsonSerializer.Serialize(doc, true); + + var nobj = mapper.ToObject(doc); + + // compare object to document + Assert.AreEqual(doc["_id"].AsInt32, obj.MyPrimaryPk); + Assert.AreEqual(doc["custom_field_name"].AsString, obj.CustomName); + Assert.AreNotEqual(doc["DoNotIncludeMe"].AsBoolean, obj.DoNotIncludeMe); + } } } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 8a1bada57..c332e39c7 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -58,6 +58,7 @@ + diff --git a/LiteDB/Serializer/Mapper/BsonMapper.cs b/LiteDB/Serializer/Mapper/BsonMapper.cs index d01cf5c43..978706f64 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.cs @@ -194,6 +194,16 @@ public void SetAutoId(object entity, LiteCollection col) } } + /// + /// Map your entity class to BsonDocument using fluent API + /// + public EntityBuilder Entity() + { + var mapper = this.GetPropertyMapper(typeof(T)); + + return new EntityBuilder(mapper); + } + #region Predefinded Property Resolvers public void UseCamelCase() diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs new file mode 100644 index 000000000..c7285b127 --- /dev/null +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace LiteDB +{ + /// + /// Helper class to modify your entity mapping to document. Can be used instead attribute decorates + /// + /// + public class EntityBuilder + { + private Dictionary _mapper; + + internal EntityBuilder(Dictionary mapper) + { + _mapper = mapper; + } + + /// + /// Define which property will not be mapped to document + /// + public EntityBuilder Ignore(Expression> property) + { + _mapper.Remove(this.GetProperty(property)); + + return this; + } + + /// + /// Define a custom name for a property when mapping to document + /// + public EntityBuilder Map(Expression> property, string field) + { + PropertyMapper prop; + + if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + { + prop.FieldName = field; + } + + return this; + } + + /// + /// Define which property is your document id (primary key). Define if this property supports auto-id + /// + public EntityBuilder Key(Expression> property, bool autoId = true) + { + PropertyMapper prop; + + if(_mapper.TryGetValue(this.GetProperty(property), out prop)) + { + prop.FieldName = "_id"; + prop.AutoId = autoId; + } + + return this; + } + + /// + /// Define an index based in a field on entity + /// + public EntityBuilder Index(Expression> property, bool unique = false) + { + PropertyMapper prop; + + if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + { + prop.IndexOptions = new IndexOptions { Unique = unique }; + } + + return this; + } + + /// + /// Define an index based in a field on entity + /// + public EntityBuilder Index(Expression> property, IndexOptions options) + { + PropertyMapper prop; + + if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + { + prop.IndexOptions = options; + } + + return this; + } + + /// + /// Get a property based on a expression. Eg.: 'x => x.UserId' return string "UserId" + /// + private string GetProperty(Expression> expr) + { + var member = expr.Body as MemberExpression; + + if (member == null) + { + throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", expr.ToString())); + } + + return ((PropertyInfo)member.Member).Name; + } + } +} diff --git a/LiteDB/Serializer/Mapper/Reflection.cs b/LiteDB/Serializer/Mapper/Reflection.cs index d687a4378..91631988a 100644 --- a/LiteDB/Serializer/Mapper/Reflection.cs +++ b/LiteDB/Serializer/Mapper/Reflection.cs @@ -73,7 +73,6 @@ public static Dictionary GetProperties(Type type, Func 0) continue; @@ -85,6 +84,7 @@ public static Dictionary GetProperties(Type type, Func Date: Sun, 8 Nov 2015 11:36:53 -0200 Subject: [PATCH 25/91] Removing DbRef as class --- LiteDB.Tests/IncludeTest.cs | 39 +++++++---- LiteDB/Core/Collections/Aggregate.cs | 2 +- LiteDB/Core/Collections/Index.cs | 2 +- LiteDB/Document/BsonDocument.cs | 2 +- LiteDB/LiteDB.csproj | 1 - .../Mapper/BsonMapper.Deserialize.cs | 10 ++- .../Serializer/Mapper/BsonMapper.Serialize.cs | 10 ++- LiteDB/Serializer/Mapper/BsonMapper.cs | 4 +- LiteDB/Serializer/Mapper/DbRef.cs | 69 ------------------- LiteDB/Serializer/Mapper/EntityBuilder.cs | 40 ++++++++++- LiteDB/Serializer/Mapper/PropertyMapper.cs | 4 ++ 11 files changed, 93 insertions(+), 90 deletions(-) delete mode 100644 LiteDB/Serializer/Mapper/DbRef.cs diff --git a/LiteDB.Tests/IncludeTest.cs b/LiteDB.Tests/IncludeTest.cs index b518f4593..9478a954e 100644 --- a/LiteDB.Tests/IncludeTest.cs +++ b/LiteDB.Tests/IncludeTest.cs @@ -11,15 +11,21 @@ namespace UnitTest public class Order { public ObjectId Id { get; set; } - - public DbRef Customer { get; set; } + public Customer Customer { get; set; } + public List Products { get; set; } } public class Customer { - public ObjectId Id { get; set; } + public int Id { get; set; } + public string Name { get; set; } + } + public class Product + { + public int ProductId { get; set; } public string Name { get; set; } + public decimal Price { get; set; } } [TestClass] @@ -31,30 +37,39 @@ public void Include_Test() using (var db = new LiteDatabase(DB.Path())) { var customers = db.GetCollection("customers"); + var products = db.GetCollection("products"); var orders = db.GetCollection("orders"); - var customer = new Customer - { - Name = "John Doe" - }; + db.Mapper.Entity() + //.DbRef(x => x.Products, "products") + .DbRef(x => x.Customer, "customers"); + + var customer = new Customer { Name = "John Doe" }; + + var product1 = new Product { Name = "TV", Price = 800 }; + var product2 = new Product { Name = "DVD", Price = 200 }; - // insert and set customer.Id + // insert ref documents customers.Insert(customer); + products.Insert(new Product[] { product1, product2 }); var order = new Order { - Customer = new DbRef(customers, customer.Id) + Customer = customer, + Products = new List() { product1, product2 } }; + var orderJson = JsonSerializer.Serialize(db.Mapper.ToDocument(order), true); + + var nOrder = db.Mapper.Deserialize(JsonSerializer.Deserialize(orderJson)); + orders.Insert(order); var query = orders - .Include((x) => x.Customer.Fetch(db)) + //.Include((x) => x.Customer.Fetch(db)) .FindAll() - .Select(x => new { CustomerName = x.Customer.Item.Name }) .FirstOrDefault(); - Assert.AreEqual(customer.Name, query.CustomerName); } } diff --git a/LiteDB/Core/Collections/Aggregate.cs b/LiteDB/Core/Collections/Aggregate.cs index 11aad1d30..456439ffb 100644 --- a/LiteDB/Core/Collections/Aggregate.cs +++ b/LiteDB/Core/Collections/Aggregate.cs @@ -123,7 +123,7 @@ public BsonValue Max(string field) var index = this.GetOrCreateIndex(field); - if (index == null) return BsonValue.MinValue; + if (index == null) return BsonValue.MaxValue; var tail = this.Database.Indexer.GetNode(index.TailNode); var prev = this.Database.Indexer.GetNode(tail.Prev[0]); diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index 2a8532753..1f77ecb48 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -166,7 +166,7 @@ internal CollectionIndex GetOrCreateIndex(string field) // if index not found, lets check if type T has [BsonIndex] if (index == null && typeof(T) != typeof(BsonDocument)) { - var options = this.Database.Mapper.GetIndexFromAttribute(field); + var options = this.Database.Mapper.GetIndexFromMapper(field); // create a new index using BsonIndex options if (options != null) diff --git a/LiteDB/Document/BsonDocument.cs b/LiteDB/Document/BsonDocument.cs index ab7835439..b7fd057fe 100644 --- a/LiteDB/Document/BsonDocument.cs +++ b/LiteDB/Document/BsonDocument.cs @@ -117,7 +117,7 @@ internal static bool IsValidFieldName(string field) { var c = field[i]; - if(char.IsLetterOrDigit(c) || c == '_') + if(char.IsLetterOrDigit(c) || c == '_' || c == '$') { continue; } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index c332e39c7..442078e44 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -59,7 +59,6 @@ - diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs index fb279288f..f3667f285 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs @@ -219,7 +219,15 @@ private void DeserializeObject(Type type, object obj, BsonDocument value) if (!val.IsNull) { - prop.Setter(obj, this.Deserialize(prop.PropertyType, val)); + // check if has a custom deserialize function + if(prop.Deserialize != null) + { + prop.Setter(obj, prop.Deserialize(val)); + } + else + { + prop.Setter(obj, this.Deserialize(prop.PropertyType, val)); + } } } } diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index cd075b31d..20c411fc2 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -158,7 +158,15 @@ private BsonDocument SerializeObject(Type type, object obj, int depth) if (value == null && this.SerializeNullValues == false && prop.FieldName != "_id") continue; - dict[prop.FieldName] = this.Serialize(prop.PropertyType, value, depth); + // if prop has a custom serialization, use it + if (prop.Serialize != null) + { + dict[prop.FieldName] = prop.Serialize(value); + } + else + { + dict[prop.FieldName] = this.Serialize(prop.PropertyType, value, depth); + } } return o; diff --git a/LiteDB/Serializer/Mapper/BsonMapper.cs b/LiteDB/Serializer/Mapper/BsonMapper.cs index 978706f64..74fa83ab7 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.cs @@ -238,9 +238,9 @@ internal Dictionary GetPropertyMapper(Type type) } /// - /// Search for [BsonIndex] in PropertyMapper. If not found, returns null + /// Search for [BsonIndex]/Entity.Index() in PropertyMapper. If not found, returns null /// - internal IndexOptions GetIndexFromAttribute(string field) + internal IndexOptions GetIndexFromMapper(string field) { var props = this.GetPropertyMapper(typeof(T)); diff --git a/LiteDB/Serializer/Mapper/DbRef.cs b/LiteDB/Serializer/Mapper/DbRef.cs deleted file mode 100644 index 64d92b350..000000000 --- a/LiteDB/Serializer/Mapper/DbRef.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace LiteDB -{ - /// - /// Creates a field that is a reference for another document from another collection. T is another type - /// - public class DbRef - where T : new() - { - /// - /// Used only for serialization/deserialize - /// - public DbRef() - { - } - - /// - /// Initialize using reference collection name and collection Id - /// - public DbRef(string collection, BsonValue id) - { - if (string.IsNullOrEmpty(collection)) throw new ArgumentNullException("collection"); - if (id == null || id.IsNull || id.IsMinValue || id.IsMaxValue) throw new ArgumentNullException("id"); - - this.Collection = collection; - this.Id = id; - } - - /// - /// Initialize using reference collection name and collection Id - /// - public DbRef(LiteCollection collection, BsonValue id) - { - if (collection == null) throw new ArgumentNullException("collection"); - if (id == null || id.IsNull || id.IsMinValue || id.IsMaxValue) throw new ArgumentNullException("id"); - - this.Collection = collection.Name; - this.Id = id; - } - - [BsonField("$ref")] - public string Collection { get; set; } - - [BsonField("$id")] - public BsonValue Id { get; set; } - - [BsonIgnore] - public T Item { get; private set; } - - /// - /// Fetch document reference return them. After fetch, you can use "Item" proerty do get ref document - /// - public T Fetch(LiteDatabase db) - { - if (this.Item == null) - { - this.Item = db.GetCollection(this.Collection).FindById(this.Id); - } - - return this.Item; - } - } -} diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs index c7285b127..54a9107a9 100644 --- a/LiteDB/Serializer/Mapper/EntityBuilder.cs +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -14,7 +14,6 @@ namespace LiteDB /// /// Helper class to modify your entity mapping to document. Can be used instead attribute decorates /// - /// public class EntityBuilder { private Dictionary _mapper; @@ -80,6 +79,45 @@ public EntityBuilder Index(Expression> property, bool unique = return this; } + /// + /// Define a subdocument (or a list of) as a reference + /// + public EntityBuilder DbRef(Expression> property, string collectionName) + { + PropertyMapper prop; + + if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + { + prop.Serialize = (v) => + { + //TODO: nao devo pegar a versao global + var mapper = BsonMapper.Global.GetPropertyMapper(v.GetType()); + + var idfield = mapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); + + var id = idfield.Getter(v); + + return new BsonDocument().Add("$id", new BsonValue(id)).Add("$ref", collectionName); + }; + + prop.Deserialize = (b) => + { + var mapper = BsonMapper.Global.GetPropertyMapper(typeof(K)); + + var idfield = mapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); + + var instance = Reflection.CreateInstance(typeof(K)); + + idfield.Setter(instance, b.AsDocument["$id"].RawValue); + + return instance; + }; + + } + + return this; + } + /// /// Define an index based in a field on entity /// diff --git a/LiteDB/Serializer/Mapper/PropertyMapper.cs b/LiteDB/Serializer/Mapper/PropertyMapper.cs index 43dac8392..93592d440 100644 --- a/LiteDB/Serializer/Mapper/PropertyMapper.cs +++ b/LiteDB/Serializer/Mapper/PropertyMapper.cs @@ -23,6 +23,10 @@ internal class PropertyMapper public GenericGetter Getter { get; set; } public GenericSetter Setter { get; set; } + // used when a property has a custom serialization/deserialization (like DbRef) + public Func Serialize { get; set; } + public Func Deserialize { get; set; } + // if this field has a [BsonIndex] store indexoptions public IndexOptions IndexOptions { get; set; } } From e1af8492270d775a13a7bcbf3f5f16415c6d90cc Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 8 Nov 2015 15:23:20 -0200 Subject: [PATCH 26/91] New DbRef --- LiteDB.Tests/IncludeTest.cs | 13 +- LiteDB/Core/Collections/Include.cs | 17 +- LiteDB/Serializer/Mapper/BsonMapper.cs | 4 +- LiteDB/Serializer/Mapper/EntityBuilder.cs | 179 ++++++++++++++------- LiteDB/Serializer/Mapper/PropertyMapper.cs | 1 + LiteDB/Serializer/Mapper/Reflection.cs | 30 ++++ 6 files changed, 178 insertions(+), 66 deletions(-) diff --git a/LiteDB.Tests/IncludeTest.cs b/LiteDB.Tests/IncludeTest.cs index 9478a954e..509654780 100644 --- a/LiteDB.Tests/IncludeTest.cs +++ b/LiteDB.Tests/IncludeTest.cs @@ -13,6 +13,8 @@ public class Order public ObjectId Id { get; set; } public Customer Customer { get; set; } public List Products { get; set; } + public Product[] ProductArray { get; set; } + public ICollection ProductColl { get; set; } } public class Customer @@ -41,7 +43,9 @@ public void Include_Test() var orders = db.GetCollection("orders"); db.Mapper.Entity() - //.DbRef(x => x.Products, "products") + .DbRef(x => x.Products, "products") + .DbRef(x => x.ProductArray, "products") + .DbRef(x => x.ProductColl, "products") .DbRef(x => x.Customer, "customers"); var customer = new Customer { Name = "John Doe" }; @@ -56,7 +60,9 @@ public void Include_Test() var order = new Order { Customer = customer, - Products = new List() { product1, product2 } + Products = new List() { product1, product2 }, + ProductArray = new Product[] { product1 }, + ProductColl = new List() { product2 } }; var orderJson = JsonSerializer.Serialize(db.Mapper.ToDocument(order), true); @@ -66,10 +72,11 @@ public void Include_Test() orders.Insert(order); var query = orders - //.Include((x) => x.Customer.Fetch(db)) + .Include(x => x.Customer) .FindAll() .FirstOrDefault(); + var customerName = query.Customer.Name; } } diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index 4c8108c24..49b3185fa 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -10,19 +10,28 @@ namespace LiteDB public partial class LiteCollection { /// - /// Run an include action in each document returned by Find(), FindById(), FindOne() and All() methods. Useful for load reference documents when nedded. + /// Run an include action in each document returned by Find(), FindById(), FindOne() and All() methods to load DbRef documents /// Returns a new Collection with this action included /// - public LiteCollection Include(Action action) + public LiteCollection Include(Expression> dbref) { - if (action == null) throw new ArgumentNullException("action"); + if (dbref == null) throw new ArgumentNullException("dbref"); var col = new LiteCollection(this.Database, Name); col._pageID = _pageID; col._includes.AddRange(_includes); - col._includes.Add(action); + var mapper = this.Database.Mapper.GetPropertyMapper(typeof(T)); + var prop = mapper[""]; + + Action action = (o) => + { + + }; + + + //col._includes.Add(action); return col; } diff --git a/LiteDB/Serializer/Mapper/BsonMapper.cs b/LiteDB/Serializer/Mapper/BsonMapper.cs index 74fa83ab7..8ded019ff 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.cs @@ -199,9 +199,7 @@ public void SetAutoId(object entity, LiteCollection col) /// public EntityBuilder Entity() { - var mapper = this.GetPropertyMapper(typeof(T)); - - return new EntityBuilder(mapper); + return new EntityBuilder(this); } #region Predefinded Property Resolvers diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs index 54a9107a9..e97a0175b 100644 --- a/LiteDB/Serializer/Mapper/EntityBuilder.cs +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -16,11 +16,13 @@ namespace LiteDB /// public class EntityBuilder { - private Dictionary _mapper; + private BsonMapper _mapper; + private Dictionary _prop; - internal EntityBuilder(Dictionary mapper) + internal EntityBuilder(BsonMapper mapper) { _mapper = mapper; + _prop = mapper.GetPropertyMapper(typeof(T)); } /// @@ -28,9 +30,10 @@ internal EntityBuilder(Dictionary mapper) /// public EntityBuilder Ignore(Expression> property) { - _mapper.Remove(this.GetProperty(property)); - - return this; + return this.GetProperty(property, (p) => + { + _prop.Remove(p.PropertyName); + }); } /// @@ -38,14 +41,10 @@ public EntityBuilder Ignore(Expression> property) /// public EntityBuilder Map(Expression> property, string field) { - PropertyMapper prop; - - if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + return this.GetProperty(property, (p) => { - prop.FieldName = field; - } - - return this; + p.FieldName = field; + }); } /// @@ -53,15 +52,11 @@ public EntityBuilder Map(Expression> property, string field) /// public EntityBuilder Key(Expression> property, bool autoId = true) { - PropertyMapper prop; - - if(_mapper.TryGetValue(this.GetProperty(property), out prop)) + return this.GetProperty(property, (p) => { - prop.FieldName = "_id"; - prop.AutoId = autoId; - } - - return this; + p.FieldName = "_id"; + p.AutoId = autoId; + }); } /// @@ -69,74 +64,142 @@ public EntityBuilder Key(Expression> property, bool autoId = tr /// public EntityBuilder Index(Expression> property, bool unique = false) { - PropertyMapper prop; - - if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + return this.GetProperty(property, (p) => { - prop.IndexOptions = new IndexOptions { Unique = unique }; - } + p.IndexOptions = new IndexOptions { Unique = unique }; + }); + } - return this; + /// + /// Define an index based in a field on entity + /// + public EntityBuilder Index(Expression> property, IndexOptions options) + { + return this.GetProperty(property, (p) => + { + p.IndexOptions = options; + }); } + #region DbRef + /// /// Define a subdocument (or a list of) as a reference /// public EntityBuilder DbRef(Expression> property, string collectionName) { - PropertyMapper prop; - - if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + return this.GetProperty(property, (p) => { - prop.Serialize = (v) => - { - //TODO: nao devo pegar a versao global - var mapper = BsonMapper.Global.GetPropertyMapper(v.GetType()); - - var idfield = mapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); + var typeK = typeof(K); - var id = idfield.Getter(v); + p.CollectionRef = collectionName; - return new BsonDocument().Add("$id", new BsonValue(id)).Add("$ref", collectionName); - }; + if (Reflection.IsList(typeK)) + { + var itemType = typeK.IsArray ? typeK.GetElementType() : Reflection.UnderlyingTypeOf(typeK); + var mapper = _mapper.GetPropertyMapper(itemType); - prop.Deserialize = (b) => + RegisterDbRefList(p, collectionName, typeK, itemType, mapper); + } + else { - var mapper = BsonMapper.Global.GetPropertyMapper(typeof(K)); + var mapper = _mapper.GetPropertyMapper(typeK); - var idfield = mapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); + RegisterDbRef(p, collectionName, typeK, mapper); + } + }); + } - var instance = Reflection.CreateInstance(typeof(K)); + /// + /// Register a property as a DbRef - implement a custom Serialize/Deserialize actions to convert entity to $id, $ref only + /// + internal static void RegisterDbRef(PropertyMapper p, string collectionName, Type itemType, Dictionary itemMapper) + { + p.Serialize = (obj) => + { + var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); - idfield.Setter(instance, b.AsDocument["$id"].RawValue); + var id = idField.Getter(obj); - return instance; - }; + return new BsonDocument() + .Add("$id", new BsonValue(id)) + .Add("$ref", collectionName); + }; - } + p.Deserialize = (bson) => + { + var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); - return this; + var instance = Reflection.CreateInstance(itemType); + + idField.Setter(instance, bson.AsDocument["$id"].RawValue); + + return instance; + }; } /// - /// Define an index based in a field on entity + /// Register a property as a DbRefList - implement a custom Serialize/Deserialize actions to convert entity to $id, $ref only /// - public EntityBuilder Index(Expression> property, IndexOptions options) + internal static void RegisterDbRefList(PropertyMapper p, string collectionName, Type listType, Type itemType, Dictionary itemMapper) { - PropertyMapper prop; + p.Serialize = (list) => + { + var result = new BsonArray(); + var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); + + foreach (var item in (IEnumerable)list) + { + result.Add(new BsonDocument() + .Add("$id", new BsonValue(idField.Getter(item))) + .Add("$ref", collectionName)); + } - if (_mapper.TryGetValue(this.GetProperty(property), out prop)) + return result; + }; + + p.Deserialize = (bson) => { - prop.IndexOptions = options; - } + var array = bson.AsArray; + var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); - return this; + // test if is an native array or a IList implementation + if (listType.IsArray) + { + var arr = Array.CreateInstance(itemType, array.Count); + var idx = 0; + + foreach (var item in array) + { + var obj = Reflection.CreateInstance(itemType); + idField.Setter(obj, item.AsDocument["$id"].RawValue); + arr.SetValue(obj, idx++); + } + + return arr; + } + else + { + var list = (IList)Reflection.CreateInstance(listType); + + foreach (var item in array) + { + var obj = Reflection.CreateInstance(itemType); + idField.Setter(obj, item.AsDocument["$id"].RawValue); + list.Add(obj); + } + + return list; + } + }; } + #endregion + /// /// Get a property based on a expression. Eg.: 'x => x.UserId' return string "UserId" /// - private string GetProperty(Expression> expr) + private EntityBuilder GetProperty(Expression> expr, Action action) { var member = expr.Body as MemberExpression; @@ -145,7 +208,11 @@ private string GetProperty(Expression> expr) throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", expr.ToString())); } - return ((PropertyInfo)member.Member).Name; + var prop = _prop[((PropertyInfo)member.Member).Name]; + + action(prop); + + return this; } } } diff --git a/LiteDB/Serializer/Mapper/PropertyMapper.cs b/LiteDB/Serializer/Mapper/PropertyMapper.cs index 93592d440..959bd2c36 100644 --- a/LiteDB/Serializer/Mapper/PropertyMapper.cs +++ b/LiteDB/Serializer/Mapper/PropertyMapper.cs @@ -26,6 +26,7 @@ internal class PropertyMapper // used when a property has a custom serialization/deserialization (like DbRef) public Func Serialize { get; set; } public Func Deserialize { get; set; } + public string CollectionRef { get; set; } // if this field has a [BsonIndex] store indexoptions public IndexOptions IndexOptions { get; set; } diff --git a/LiteDB/Serializer/Mapper/Reflection.cs b/LiteDB/Serializer/Mapper/Reflection.cs index 91631988a..f7fff8935 100644 --- a/LiteDB/Serializer/Mapper/Reflection.cs +++ b/LiteDB/Serializer/Mapper/Reflection.cs @@ -165,6 +165,14 @@ public static object CreateInstance(Type type) { return CreateInstance(GetGenericListOfType(UnderlyingTypeOf(type))); } + else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ICollection<>)) + { + return CreateInstance(GetGenericListOfType(UnderlyingTypeOf(type))); + } + else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return CreateInstance(GetGenericListOfType(UnderlyingTypeOf(type))); + } else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>)) { var k = type.GetGenericArguments()[0]; @@ -299,6 +307,28 @@ public static Type GetGenericDictionaryOfType(Type k, Type v) return listType.MakeGenericType(k, v); } + /// + /// Returns true if Type is any kind of Array/IList/ICollection/.... + /// + public static bool IsList(Type type) + { + if(type.IsArray) return true; + + foreach (Type @interface in type.GetInterfaces()) + { + if (@interface.IsGenericType) + { + if (@interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + // if needed, you can also return the type used as generic argument + return true; + } + } + } + + return false; + } + #endregion } } From 24a12fcfa5036e4f2a314581bda71c33ae43eea3 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 8 Nov 2015 19:11:38 -0200 Subject: [PATCH 27/91] Better way to DbRef --- LiteDB-TODO.txt | 28 +++++++-- LiteDB.Tests/IncludeTest.cs | 19 +++++- LiteDB/Core/Collections/Find.cs | 9 +-- LiteDB/Core/Collections/Include.cs | 46 +++++++++++--- LiteDB/Core/Collections/LiteCollection.cs | 4 +- .../Mapper/BsonMapper.Deserialize.cs | 4 +- .../Serializer/Mapper/BsonMapper.Serialize.cs | 2 +- LiteDB/Serializer/Mapper/EntityBuilder.cs | 63 ++++++++----------- LiteDB/Serializer/Mapper/PropertyMapper.cs | 5 +- LiteDB/Serializer/Mapper/Reflection.cs | 5 ++ 10 files changed, 119 insertions(+), 66 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index de749f3b0..681d6a3e4 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,15 +1,16 @@ # LiteDB v.2 - TODO - Revisar transação - Remover possibilidade - - Rollback continua existindo? Ou só commit? O que fazer quando ocorre erro na transacao (journal) + - A transacao deve ficar sempre na operacao mais externa + +- Thread Safe + -- Revisar auto-id de Int32 (q usa collection) - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - FindAndModify(Query, Action) - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() -- Thread Safe - Remove auto-register do shell - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.TextWriters = Stream(); @@ -19,9 +20,24 @@ - DocumentService (_docsrv) * Insert/Update/Delete/Find - EnsureIndex/Find - Não contempla transacao - - - -A gestão da transacao deve ser feita sempre no metodo mais externo (begin/commit/roll/avoidDrty) + +## DbRef + +- Implementar DbRefAttribute +- Revisar codigos - estão muito confusos os nomes e falta documentacao (é complexo e cheio de reflection) +- Implementar sub-document +- Deveria funcionar para , IList, K[], Dictionary sem muita gambi + + + +{ + _id: 1, + address: { + street: "jacipuia", + country: { $id: "br", $ref="countries" } + } +} + ## Cache diff --git a/LiteDB.Tests/IncludeTest.cs b/LiteDB.Tests/IncludeTest.cs index 509654780..41e6f2740 100644 --- a/LiteDB.Tests/IncludeTest.cs +++ b/LiteDB.Tests/IncludeTest.cs @@ -12,9 +12,13 @@ public class Order { public ObjectId Id { get; set; } public Customer Customer { get; set; } + public Customer CustomerNull { get; set; } + public List Products { get; set; } public Product[] ProductArray { get; set; } public ICollection ProductColl { get; set; } + public List ProductEmpty { get; set; } + public List ProductsNull { get; set; } } public class Customer @@ -46,7 +50,10 @@ public void Include_Test() .DbRef(x => x.Products, "products") .DbRef(x => x.ProductArray, "products") .DbRef(x => x.ProductColl, "products") - .DbRef(x => x.Customer, "customers"); + .DbRef(x => x.ProductEmpty, "products") + .DbRef(x => x.ProductsNull, "products") + .DbRef(x => x.Customer, "customers") + .DbRef(x => x.CustomerNull, "customers"); var customer = new Customer { Name = "John Doe" }; @@ -60,9 +67,12 @@ public void Include_Test() var order = new Order { Customer = customer, + CustomerNull = null, Products = new List() { product1, product2 }, ProductArray = new Product[] { product1 }, - ProductColl = new List() { product2 } + ProductColl = new List() { product2 }, + ProductEmpty = new List(), + ProductsNull = null }; var orderJson = JsonSerializer.Serialize(db.Mapper.ToDocument(order), true); @@ -73,6 +83,11 @@ public void Include_Test() var query = orders .Include(x => x.Customer) + .Include(x => x.CustomerNull) + .Include(x => x.Products) + .Include(x => x.ProductArray) + .Include(x => x.ProductColl) + .Include(x => x.ProductsNull) .FindAll() .FirstOrDefault(); diff --git a/LiteDB/Core/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs index 2c8ad093e..e6c241747 100644 --- a/LiteDB/Core/Collections/Find.cs +++ b/LiteDB/Core/Collections/Find.cs @@ -32,14 +32,15 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - // get object from BsonDocument - var obj = this.Database.Mapper.ToObject(doc); - + // executing all includes in BsonDocument foreach (var action in _includes) { - action(obj); + action(doc); } + // get object from BsonDocument + var obj = this.Database.Mapper.ToObject(doc); + yield return obj; } } diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index 49b3185fa..95e6ae888 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -1,8 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; namespace LiteDB @@ -17,23 +19,49 @@ public LiteCollection Include(Expression> dbref) { if (dbref == null) throw new ArgumentNullException("dbref"); - var col = new LiteCollection(this.Database, Name); + var member = dbref.Body as MemberExpression; + if (member == null) throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", dbref.ToString())); + var propName = ((PropertyInfo)member.Member).Name; - col._pageID = _pageID; - col._includes.AddRange(_includes); + Action action = (bson) => + { + var prop = bson.Get(propName); - var mapper = this.Database.Mapper.GetPropertyMapper(typeof(T)); - var prop = mapper[""]; + if(prop.IsNull) return; - Action action = (o) => - { + // if property value is an array, populate all values + if(prop.IsArray) + { + var array = prop.AsArray; + if(array.Count == 0) return; + + // all doc refs in an array must be same collection, lets take first only + var col = this.Database.GetCollection(array[0].AsDocument["$ref"]); + for(var i = 0; i < array.Count; i++) + { + var obj = col.FindById(array[i].AsDocument["$id"]); + array[i] = obj; + } + } + else + { + // for BsonDocument, get property value e update with full object refence + var doc = prop.AsDocument; + var col = this.Database.GetCollection(doc["$ref"]); + var obj = col.FindById(doc["$id"]); + bson.Set(propName, obj); + } }; + // cloning this collection and adding this include + var newcol = new LiteCollection(this.Database, Name); - //col._includes.Add(action); + newcol._pageID = _pageID; + newcol._includes.AddRange(_includes); + newcol._includes.Add(action); - return col; + return newcol; } } } diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 9e565b514..1b85ab8f2 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -10,7 +10,7 @@ public partial class LiteCollection where T : new() { private uint _pageID; - private List> _includes; + private List> _includes; private QueryVisitor _visitor; /// @@ -29,7 +29,7 @@ internal LiteCollection(LiteDatabase db, string name) this.Database = db; _pageID = uint.MaxValue; _visitor = new QueryVisitor(db.Mapper); - _includes = new List>(); + _includes = new List>(); } /// diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs index f3667f285..9b94a3a96 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Deserialize.cs @@ -71,7 +71,7 @@ public T Deserialize(BsonValue value) #endregion - private object Deserialize(Type type, BsonValue value) + internal object Deserialize(Type type, BsonValue value) { Func custom; @@ -222,7 +222,7 @@ private void DeserializeObject(Type type, object obj, BsonDocument value) // check if has a custom deserialize function if(prop.Deserialize != null) { - prop.Setter(obj, prop.Deserialize(val)); + prop.Setter(obj, prop.Deserialize(val, this)); } else { diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index 20c411fc2..64c0f0d40 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -161,7 +161,7 @@ private BsonDocument SerializeObject(Type type, object obj, int depth) // if prop has a custom serialization, use it if (prop.Serialize != null) { - dict[prop.FieldName] = prop.Serialize(value); + dict[prop.FieldName] = prop.Serialize(value, this); } else { diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs index e97a0175b..e0444b7e0 100644 --- a/LiteDB/Serializer/Mapper/EntityBuilder.cs +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -90,22 +90,20 @@ public EntityBuilder DbRef(Expression> property, string collect { return this.GetProperty(property, (p) => { - var typeK = typeof(K); + var typeRef = typeof(K); - p.CollectionRef = collectionName; - - if (Reflection.IsList(typeK)) + if (Reflection.IsList(typeRef)) { - var itemType = typeK.IsArray ? typeK.GetElementType() : Reflection.UnderlyingTypeOf(typeK); + var itemType = typeRef.IsArray ? typeRef.GetElementType() : Reflection.UnderlyingTypeOf(typeRef); var mapper = _mapper.GetPropertyMapper(itemType); - RegisterDbRefList(p, collectionName, typeK, itemType, mapper); + RegisterDbRefList(p, collectionName, typeRef, itemType, mapper); } else { - var mapper = _mapper.GetPropertyMapper(typeK); + var mapper = _mapper.GetPropertyMapper(typeRef); - RegisterDbRef(p, collectionName, typeK, mapper); + RegisterDbRef(p, collectionName, typeRef, mapper); } }); } @@ -115,7 +113,7 @@ public EntityBuilder DbRef(Expression> property, string collect /// internal static void RegisterDbRef(PropertyMapper p, string collectionName, Type itemType, Dictionary itemMapper) { - p.Serialize = (obj) => + p.Serialize = (obj, m) => { var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); @@ -126,15 +124,14 @@ internal static void RegisterDbRef(PropertyMapper p, string collectionName, Type .Add("$ref", collectionName); }; - p.Deserialize = (bson) => + p.Deserialize = (bson, m) => { - var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); - - var instance = Reflection.CreateInstance(itemType); + var idRef = bson.AsDocument["$id"]; - idField.Setter(instance, bson.AsDocument["$id"].RawValue); - - return instance; + return m.Deserialize(itemType, + idRef.IsNull ? + bson : // if has no $id object was full loaded (via Include) - so deserialize using normal function + new BsonDocument().Add("_id", idRef)); // if has $id, deserialize object using only _id object }; } @@ -143,7 +140,7 @@ internal static void RegisterDbRef(PropertyMapper p, string collectionName, Type /// internal static void RegisterDbRefList(PropertyMapper p, string collectionName, Type listType, Type itemType, Dictionary itemMapper) { - p.Serialize = (list) => + p.Serialize = (list, m) => { var result = new BsonArray(); var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); @@ -158,38 +155,30 @@ internal static void RegisterDbRefList(PropertyMapper p, string collectionName, return result; }; - p.Deserialize = (bson) => + p.Deserialize = (bson, m) => { var array = bson.AsArray; - var idField = itemMapper.Select(x => x.Value).FirstOrDefault(x => x.FieldName == "_id"); - // test if is an native array or a IList implementation - if (listType.IsArray) - { - var arr = Array.CreateInstance(itemType, array.Count); - var idx = 0; + if(array.Count == 0) return m.Deserialize(listType, array); - foreach (var item in array) - { - var obj = Reflection.CreateInstance(itemType); - idField.Setter(obj, item.AsDocument["$id"].RawValue); - arr.SetValue(obj, idx++); - } + var hasIdRef = array[0].AsDocument["$id"].IsNull; - return arr; + if(hasIdRef) + { + // if no $id, deserialize as full (was loaded via Include) + return m.Deserialize(listType, array); } else { - var list = (IList)Reflection.CreateInstance(listType); + // copy array changing $id to _id + var arr = new BsonArray(); - foreach (var item in array) + foreach(var item in array) { - var obj = Reflection.CreateInstance(itemType); - idField.Setter(obj, item.AsDocument["$id"].RawValue); - list.Add(obj); + arr.Add(new BsonDocument().Add("_id", item.AsDocument["$id"])); } - return list; + return m.Deserialize(listType, arr); } }; } diff --git a/LiteDB/Serializer/Mapper/PropertyMapper.cs b/LiteDB/Serializer/Mapper/PropertyMapper.cs index 959bd2c36..99f1bba2a 100644 --- a/LiteDB/Serializer/Mapper/PropertyMapper.cs +++ b/LiteDB/Serializer/Mapper/PropertyMapper.cs @@ -24,9 +24,8 @@ internal class PropertyMapper public GenericSetter Setter { get; set; } // used when a property has a custom serialization/deserialization (like DbRef) - public Func Serialize { get; set; } - public Func Deserialize { get; set; } - public string CollectionRef { get; set; } + public Func Serialize { get; set; } + public Func Deserialize { get; set; } // if this field has a [BsonIndex] store indexoptions public IndexOptions IndexOptions { get; set; } diff --git a/LiteDB/Serializer/Mapper/Reflection.cs b/LiteDB/Serializer/Mapper/Reflection.cs index f7fff8935..a235b0bbf 100644 --- a/LiteDB/Serializer/Mapper/Reflection.cs +++ b/LiteDB/Serializer/Mapper/Reflection.cs @@ -131,6 +131,11 @@ public static Dictionary GetProperties(Type type, Func Date: Sun, 8 Nov 2015 22:18:43 -0200 Subject: [PATCH 28/91] Journal finish mark --- LiteDB-TODO.txt | 130 ++++-------------- LiteDB/Core/Collections/Include.cs | 8 +- LiteDB/DataStructure/Disks/FileDiskService.cs | 31 ++++- LiteDB/DataStructure/Pages/BasePage.cs | 8 +- LiteDB/DataStructure/Services/CacheService.cs | 2 +- LiteDB/DataStructure/Services/PageService.cs | 2 +- LiteDB/LiteDB.csproj | 2 + LiteDB/Serializer/Mapper/EntityBuilder.cs | 9 +- LiteDB/Utils/ExpressionExtensions.cs | 43 ++++++ LiteDB/Utils/StreamExtensions.cs | 25 ++++ 10 files changed, 129 insertions(+), 131 deletions(-) create mode 100644 LiteDB/Utils/ExpressionExtensions.cs create mode 100644 LiteDB/Utils/StreamExtensions.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 681d6a3e4..bb23a6e9c 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,42 +2,18 @@ - Revisar transação - Remover possibilidade - A transacao deve ficar sempre na operacao mais externa - - Thread Safe - - - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - FindAndModify(Query, Action) - - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() +- Implementar ctor new LiteDatabase(Stream) - Remove auto-register do shell - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.TextWriters = Stream(); db.Debbuger.Counters["Abc"].Times | Count - -- Adicionar as operacoes do banco para um servico - manter no Collection apenas as chamadas + Linq conv + API - - DocumentService (_docsrv) * Insert/Update/Delete/Find - - EnsureIndex/Find - - Não contempla transacao - -## DbRef - -- Implementar DbRefAttribute -- Revisar codigos - estão muito confusos os nomes e falta documentacao (é complexo e cheio de reflection) -- Implementar sub-document -- Deveria funcionar para , IList, K[], Dictionary sem muita gambi - - - -{ - _id: 1, - address: { - street: "jacipuia", - country: { $id: "br", $ref="countries" } - } -} - + +- Ta lendo paginas repetidas (para BasePage e depois para Data|IndexPage) ## Cache @@ -64,23 +40,8 @@ e) Leitura conforme indice (retorno poucos registros) ?? Implementer SortDictionaryMRU ?? Implementar cache de 2 repos -## A pensar: - -- Voltar UserVersion?? -- Solução de Migrations (protected?) - db.Migrations.Add(new Migration1()); -- DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? -- Folder root: "src", "test", "nuget" -- Encrypt datafile (EncryptDiskService : FileDiskService) - -- Como implementar pro DNX -- Mapeador/Configuration estilo EF - db.Configuration.Add(new CredorConfiguration()) - ## Mapping -+ Cai fora a forma de implementar hoje com RegisterType (_customSerializer) ? Acho que não, vide Uri - db.Mapper.Entity() .HasKey(x => x.IdUsuario) .Property(x => x.NoUsuario, "nome_usuario") @@ -89,75 +50,30 @@ db.Mapper.Entity() .Ignore(x => x.FgAtivo) .Index(x => x.NoUsuario) +## DbRef -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; - -namespace UnitTest -{ - [TestClass] - public class BsonPerfTest - { - private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; - - [TestMethod] - public void BsonWritePerf_Test() - { - var o = JsonSerializer.Deserialize(json).AsDocument; - - var w1 = new Stopwatch(); - var w2 = new Stopwatch(); - - for(var i = 0; i < 20000; i++) - { - w1.Start(); - var bson1 = BsonSerializer2.Serialize(o); - w1.Stop(); - - w2.Start(); - var bson2 = BsonSerializer2.Serialize(o); - w2.Stop(); - - } - - Debug.WriteLine("Time to write 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); - Debug.WriteLine("Time to write 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); - - } - - [TestMethod] - public void BsonReaderPerf_Test() - { - var bytes1 = BsonSerializer.Serialize(JsonSerializer.Deserialize(json).AsDocument); - var bytes2 = BsonSerializer2.Serialize(JsonSerializer.Deserialize(json).AsDocument); - - var w1 = new Stopwatch(); - var w2 = new Stopwatch(); - - for (var i = 0; i < 1000; i++) - { - w1.Start(); - var doc1 = BsonSerializer.Deserialize(bytes1); - w1.Stop(); - - w2.Start(); - var doc2 = BsonSerializer2.Deserialize(bytes2); - w2.Stop(); +- Implementar DbRefAttribute +- Implementar testes - Assert.AreEqual(doc1.ToString(), doc2.ToString()); - } +## A pensar: +- Voltar UserVersion?? +- Solução de Migrations (protected?) + db.Migrations.Add(new Migration1()); +- DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? +- Folder root: "src", "test", "nuget" +- Encrypt datafile (EncryptDiskService : FileDiskService) +- Como implementar pro DNX +- Mapeador/Configuration estilo EF + db.Configuration.Add(new CredorConfiguration()) - Debug.WriteLine("Time to read 1 " + w1.ElapsedTicks.ToString().PadLeft(15, ' ')); - Debug.WriteLine("Time to read 2 " + w2.ElapsedTicks.ToString().PadLeft(15, ' ')); +- Adicionar as operacoes do banco para um servico - manter no Collection apenas as chamadas + Linq conv + API + - DocumentService (_docsrv) * Insert/Update/Delete/Find + - EnsureIndex/Find + - Não contempla transacao +================================================================================================================ +================================================================================================================ - } - } -} \ No newline at end of file +private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index 95e6ae888..c24e0d044 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -19,13 +19,11 @@ public LiteCollection Include(Expression> dbref) { if (dbref == null) throw new ArgumentNullException("dbref"); - var member = dbref.Body as MemberExpression; - if (member == null) throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", dbref.ToString())); - var propName = ((PropertyInfo)member.Member).Name; + var propPath = dbref.GetPath(); Action action = (bson) => { - var prop = bson.Get(propName); + var prop = bson.Get(propPath); if(prop.IsNull) return; @@ -50,7 +48,7 @@ public LiteCollection Include(Expression> dbref) var doc = prop.AsDocument; var col = this.Database.GetCollection(doc["$ref"]); var obj = col.FindById(doc["$id"]); - bson.Set(propName, obj); + bson.Set(propPath, obj); } }; diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 31bf1773c..a66e50717 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -15,6 +15,11 @@ internal class FileDiskService : IDiskService /// private const int LOCK_POSITION = 0; + /// + /// Position on disk to write a mark to know when journal is finish and valid + /// + private const int JOURNAL_FINISH_POSITION = 19; + private FileStream _stream; private string _filename; @@ -51,10 +56,15 @@ public bool Initialize() _readonly ? FileShare.Read : FileShare.ReadWrite, BasePage.PAGE_SIZE); - this.TryRecovery(); - - // returns true if new file - return _stream.Length == 0; + if(_stream.Length == 0) + { + return true; + } + else + { + this.TryRecovery(); + return false; + } } #region Lock/Unlock @@ -150,7 +160,6 @@ public void WriteJournal(uint pageID, byte[] data) }); } - //TODO: avisa q esta gerando Console.WriteLine("journal write " + pageID); // just write original bytes in order that are changed @@ -161,6 +170,10 @@ public void CommitJournal() { if (_journalEnabled == false) return; + // write a mark (byte 1) to know when journal is finish + // after that, if found a non-exclusive-open journal file, must be recovery + _journal.WriteByte(JOURNAL_FINISH_POSITION, 1); + // flush all journal file data to disk _journal.Flush(); } @@ -194,9 +207,13 @@ private void TryRecovery() // if I can open journal file, test FINISH_POSITION. If no journal, do not call action() this.OpenExclusiveFile(_journalFilename, (journal) => { - this.Recovery(journal); + var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); - //TODO: valida se precisa ser feito recovery (FINISH) + // test if journal was finish + if(finish == 1) + { + this.Recovery(journal); + } // close stream for delete file journal.Close(); diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index 0ee2e46bf..0ef5421e2 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -18,12 +18,12 @@ internal class BasePage public const int PAGE_SIZE = 4096; /// - /// This size is used bytes in header pages 17 bytes + /// This size is used bytes in header pages 17 bytes (+3 free) /// - public const int PAGE_HEADER_SIZE = 17; + public const int PAGE_HEADER_SIZE = 20; /// - /// Bytes avaiable to store data removing page header size - 4079 bytes + /// Bytes avaiable to store data removing page header size - 4076 bytes /// public const int PAGE_AVAILABLE_BYTES = PAGE_SIZE - PAGE_HEADER_SIZE; @@ -115,6 +115,7 @@ public virtual void ReadHeader(ByteReader reader) this.PageType = (PageType)reader.ReadByte(); this.ItemCount = reader.ReadUInt16(); this.FreeBytes = reader.ReadUInt16(); + reader.ReadBytes(3); // reserved 3 bytes } public virtual void WriteHeader(ByteWriter writer) @@ -125,6 +126,7 @@ public virtual void WriteHeader(ByteWriter writer) writer.Write((byte)this.PageType); writer.Write((UInt16)this.ItemCount); writer.Write((UInt16)this.FreeBytes); + writer.Write(new byte[3]); // reserved 3 bytes } public virtual void ReadContent(ByteReader reader) diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index be822f901..2d5f363e5 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -51,7 +51,7 @@ public T GetPage(uint pageID) public void AddPage(BasePage page, bool dirty = false) { // do not cache extend page - never will be reused - if (page.PageType != PageType.Extend) + //if (page.PageType != PageType.Extend) { _cache[page.PageID] = page; } diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs index 795579f59..a192b0dc9 100644 --- a/LiteDB/DataStructure/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -34,7 +34,7 @@ public T GetPage(uint pageID) page.ReadPage(buffer); - Console.WriteLine("read " + pageID); + Console.WriteLine("read " + pageID + " (" + typeof(T).Name + ")"); _cache.AddPage(page); } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 442078e44..75b876c6e 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -144,6 +144,8 @@ + + diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs index e0444b7e0..c230bab5c 100644 --- a/LiteDB/Serializer/Mapper/EntityBuilder.cs +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -190,14 +190,9 @@ internal static void RegisterDbRefList(PropertyMapper p, string collectionName, /// private EntityBuilder GetProperty(Expression> expr, Action action) { - var member = expr.Body as MemberExpression; + var prop = _prop[expr.GetPath()]; - if (member == null) - { - throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", expr.ToString())); - } - - var prop = _prop[((PropertyInfo)member.Member).Name]; + if(prop == null) throw new ArgumentNullException(expr.GetPath()); action(prop); diff --git a/LiteDB/Utils/ExpressionExtensions.cs b/LiteDB/Utils/ExpressionExtensions.cs new file mode 100644 index 000000000..26fbb7dd6 --- /dev/null +++ b/LiteDB/Utils/ExpressionExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Linq.Expressions; + +namespace LiteDB +{ + internal static class ExpressionExtensions + { + public static string GetPath(this Expression> expr) + { + MemberExpression me; + + switch (expr.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = expr.Body as UnaryExpression; + me = ((ue != null) ? ue.Operand : null) as MemberExpression; + break; + default: + me = expr.Body as MemberExpression; + break; + } + + var sb = new StringBuilder(); + + while (me != null) + { + var propertyName = me.Member.Name; + var propertyType = me.Type; + + sb.Insert(0, propertyName + (sb.Length > 0 ? "." : "")); + + me = me.Expression as MemberExpression; + } + + return sb.ToString(); + } + } +} diff --git a/LiteDB/Utils/StreamExtensions.cs b/LiteDB/Utils/StreamExtensions.cs new file mode 100644 index 000000000..2f11ccb31 --- /dev/null +++ b/LiteDB/Utils/StreamExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace LiteDB +{ + internal static class StreamExtensions + { + public static byte ReadByte(this Stream stream, long position) + { + var buffer = new byte[1]; + stream.Seek(position, SeekOrigin.Begin); + stream.Read(buffer, 0, 1); + return buffer[0]; + } + + public static void WriteByte(this Stream stream, long position, byte value) + { + stream.Seek(position, SeekOrigin.Begin); + stream.Write(new byte[] { value }, 0, 1); + } + } +} From b71073124645d8e452c172193d0148cbf947715e Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 9 Nov 2015 15:13:46 -0200 Subject: [PATCH 29/91] Fix changeID and double read page --- LiteDB/DataStructure/Disks/FileDiskService.cs | 2 +- LiteDB/DataStructure/Pages/BasePage.cs | 35 ++++++++++++++++++- LiteDB/DataStructure/Pages/HeaderPage.cs | 6 ++-- LiteDB/DataStructure/Services/CacheService.cs | 13 +++++-- .../Services/TransactionService.cs | 2 ++ 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index a66e50717..9f8e83450 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -16,7 +16,7 @@ internal class FileDiskService : IDiskService private const int LOCK_POSITION = 0; /// - /// Position on disk to write a mark to know when journal is finish and valid + /// Position on disk to write a mark to know when journal is finish and valid (byte 19 is free header area) /// private const int JOURNAL_FINISH_POSITION = 19; diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/DataStructure/Pages/BasePage.cs index 0ef5421e2..cfc6dd96c 100644 --- a/LiteDB/DataStructure/Pages/BasePage.cs +++ b/LiteDB/DataStructure/Pages/BasePage.cs @@ -18,7 +18,7 @@ internal class BasePage public const int PAGE_SIZE = 4096; /// - /// This size is used bytes in header pages 17 bytes (+3 free) + /// This size is used bytes in header pages 17 bytes (+3 free) = 20 bytes /// public const int PAGE_HEADER_SIZE = 20; @@ -105,6 +105,39 @@ public virtual void Clear() this.DiskData = new byte[0]; } + /// + /// Convert a BasePage to a specific page keeping same page header vars and re-loading disk content + /// + public T CopyTo() + where T : BasePage, new() + { + if (this.DiskData.Length == 0) throw new SystemException("No diskdata in this page"); + + var page = new T(); + page.PageID = this.PageID; + page.PrevPageID = this.PrevPageID; + page.NextPageID = this.NextPageID; + // page.PageType = this.PageType; + page.ItemCount = this.ItemCount; + page.FreeBytes = this.FreeBytes; + page.IsDirty = this.IsDirty; + page.DiskData = new byte[BasePage.PAGE_SIZE]; + + Buffer.BlockCopy(this.DiskData, 0, page.DiskData, 0, BasePage.PAGE_SIZE); + + var reader = new ByteReader(this.DiskData); + + // skip header - i copyed from "this" instance (including possible changes) + reader.ReadBytes(BasePage.PAGE_HEADER_SIZE); + + if (page.PageType != LiteDB.PageType.Empty) + { + this.ReadContent(reader); + } + + return page; + } + #region Read/Write page public virtual void ReadHeader(ByteReader reader) diff --git a/LiteDB/DataStructure/Pages/HeaderPage.cs b/LiteDB/DataStructure/Pages/HeaderPage.cs index 528c72891..c67ce2c2e 100644 --- a/LiteDB/DataStructure/Pages/HeaderPage.cs +++ b/LiteDB/DataStructure/Pages/HeaderPage.cs @@ -9,12 +9,12 @@ namespace LiteDB internal class HeaderPage : BasePage { /// - /// ChangeID in file position + /// ChangeID in file position (can be calc?) /// - public const int CHANGE_ID_POSITION = 49; + public const int CHANGE_ID_POSITION = 52; /// - /// Header info the validate that datafile is a LiteDB file + /// Header info the validate that datafile is a LiteDB file (27 bytes) /// private const string HEADER_INFO = "** This is a LiteDB file **"; diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 2d5f363e5..66b34bf2f 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -31,14 +31,23 @@ public CacheService() /// If T is more specific than page that I have in cache, returns null (eg. Page 2 is BasePage in cache and this method call for IndexPage PageId 2) /// public T GetPage(uint pageID) - where T : BasePage + where T : BasePage, new() { var page = _cache.GetOrDefault(pageID, null); // if a need a specific page but has a BasePage, returns null if (page != null && page.GetType() == typeof(BasePage) && typeof(T) != typeof(BasePage)) { - return null; + if(page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); + + var specificPage = new T(); + + specificPage.ReadPage(page.DiskData); + + _cache[pageID] = specificPage; + + return specificPage; + //return null; } return (T)page; diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 25f5a00d2..bf45bdd83 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -143,6 +143,8 @@ public void AvoidDirtyRead() // if changeID was changed, file was changed by another process - clear all cache if (cache.ChangeID != change) { + Console.WriteLine("Datafile changed, clear cache"); + _cache.Clear(); } } From fd399307016a36315d4e34efb8ba02427c77e1c7 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 9 Nov 2015 16:26:03 -0200 Subject: [PATCH 30/91] Added FindAndModify --- LiteDB-TODO.txt | 11 ++++++++ LiteDB/Core/Collections/FindAndModify.cs | 36 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index bb23a6e9c..7e11a8f30 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -72,6 +72,17 @@ db.Mapper.Entity() - DocumentService (_docsrv) * Insert/Update/Delete/Find - EnsureIndex/Find - Não contempla transacao + +## Para versão 2 + +- Criar site simples, bacana, estilos: + http://pouchdb.com/ + vuejs.org https://github.com/vuejs/vuejs.org + https://hexo.io/ (esse é bem simples e acho q atende bem) +- Fazer o web shell na mesma pagina (nem q seja via iframe) +- Colocar documentação neste site +- Logotipo + Nome +- Atualizar codeproject ================================================================================================================ ================================================================================================================ diff --git a/LiteDB/Core/Collections/FindAndModify.cs b/LiteDB/Core/Collections/FindAndModify.cs index 1fade3d19..a27c4ac8f 100644 --- a/LiteDB/Core/Collections/FindAndModify.cs +++ b/LiteDB/Core/Collections/FindAndModify.cs @@ -7,8 +7,42 @@ namespace LiteDB { - // Execute find operation in index only - do not deserialize documents public partial class LiteCollection { + /// + /// Query documents and execute, for each document, action method. All data is locked during execution + /// + public void FindAndModify(Query query, Action action) + { + if (query == null) throw new ArgumentNullException("query"); + if (action == null) throw new ArgumentNullException("action"); + + this.Database.Transaction.Begin(); + + try + { + var docs = this.Find(query); + + foreach(var doc in docs) + { + action(doc); + } + + this.Database.Transaction.Commit(); + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } + } + + /// + /// Query documents and execute, for each document, action method. All data is locked during execution + /// + public void FindAndModify(Expression> predicate, Action action) + { + this.FindAndModify(_visitor.Visit(predicate), action); + } } } From 5525c4db141b68aba18b3b9ea5e5e9aacf3db084 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 9 Nov 2015 17:11:15 -0200 Subject: [PATCH 31/91] Shell commands identified by class name --- LiteDB-TODO.txt | 14 -------------- LiteDB/Shell/LiteShell.cs | 11 ++++++----- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 7e11a8f30..d3ee4ef68 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -4,16 +4,12 @@ - A transacao deve ficar sempre na operacao mais externa - Thread Safe - Queria que o IDiskService não tivesse informações sobre Journal/Recovery -- FindAndModify(Query, Action) - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() - Implementar ctor new LiteDatabase(Stream) -- Remove auto-register do shell - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.TextWriters = Stream(); db.Debbuger.Counters["Abc"].Times | Count - -- Ta lendo paginas repetidas (para BasePage e depois para Data|IndexPage) ## Cache @@ -40,16 +36,6 @@ e) Leitura conforme indice (retorno poucos registros) ?? Implementer SortDictionaryMRU ?? Implementar cache de 2 repos -## Mapping - -db.Mapper.Entity() - .HasKey(x => x.IdUsuario) - .Property(x => x.NoUsuario, "nome_usuario") - .Property(x => x.CdUsuario, "codigo_usuario", (x) => x.CdUsuario, (bson) => bson) - .Property(Expression, FieldName, Func serialize, Func deserialize) - .Ignore(x => x.FgAtivo) - .Index(x => x.NoUsuario) - ## DbRef - Implementar DbRefAttribute diff --git a/LiteDB/Shell/LiteShell.cs b/LiteDB/Shell/LiteShell.cs index d4e6cda60..8acfb672b 100644 --- a/LiteDB/Shell/LiteShell.cs +++ b/LiteDB/Shell/LiteShell.cs @@ -9,14 +9,14 @@ namespace LiteDB.Shell { public class LiteShell { - public List Commands { get; set; } + public Dictionary Commands { get; set; } public LiteDatabase Database { get; set; } public LiteShell(LiteDatabase db) { this.Database = db; - this.Commands = new List(); + this.Commands = new Dictionary(); var type = typeof(ILiteCommand); var types = AppDomain.CurrentDomain.GetAssemblies() @@ -25,7 +25,8 @@ public LiteShell(LiteDatabase db) foreach (var t in types) { - this.Commands.Add((ILiteCommand)Activator.CreateInstance(t)); + var cmd = (ILiteCommand)Activator.CreateInstance(t); + this.Commands.Add(t.Name, cmd); } } @@ -37,14 +38,14 @@ public BsonValue Run(string command) foreach (var cmd in this.Commands) { - if (cmd.IsCommand(s)) + if (cmd.Value.IsCommand(s)) { if (this.Database == null) { throw LiteException.NoDatabase(); } - return cmd.Execute(this.Database, s); + return cmd.Value.Execute(this.Database, s); } } From 5d2089767c5610d94be6711853b47334c1abc52c Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 9 Nov 2015 22:25:38 -0200 Subject: [PATCH 32/91] Update my todos --- LiteDB-TODO.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index d3ee4ef68..97cb098d6 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -11,6 +11,18 @@ db.Debugger.TextWriters = Stream(); db.Debbuger.Counters["Abc"].Times | Count +## Debbuger + +db.Debug.Write((s) => Console.Write(s)); +db.Debug.Counters.Enabled = true; +db.Debug.Counters.DiskReader +db.Debug.Counters.DiskWriter +db.Debug.Counters.BsonSerialize +db.Debug.Counters.BsonDeserialize +db.Debug.Counters.JournalWrite + + + ## Cache Para saber qual caminho seguir em relação a cache, acho que precisa fazer branchs para testar: From da53efa5b81471133270e4b614ab56cadbbfa19e Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 10 Nov 2015 18:12:58 -0200 Subject: [PATCH 33/91] Add litedb logo --- logo_litedb.svg | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 logo_litedb.svg diff --git a/logo_litedb.svg b/logo_litedb.svg new file mode 100644 index 000000000..f56a949a8 --- /dev/null +++ b/logo_litedb.svg @@ -0,0 +1,29 @@ + + + + + background + + + + Layer 1 + + + + + + + + + + + + + + + + + Lite + DB + + \ No newline at end of file From b4d97194e79eb7beaaa404ed69f0c69befe3484f Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 10 Nov 2015 18:44:38 -0200 Subject: [PATCH 34/91] Support document/array in indexes --- LiteDB/DataStructure/Services/IndexService.cs | 15 +++++++++------ LiteDB/Query/Impl/QueryContains.cs | 4 +++- LiteDB/Serializer/Bson/BsonReader.cs | 4 ++-- LiteDB/Serializer/Bson/BsonWriter.cs | 2 +- LiteDB/Utils/ByteReader.cs | 4 ++-- LiteDB/Utils/ByteWriter.cs | 4 ++-- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/LiteDB/DataStructure/Services/IndexService.cs b/LiteDB/DataStructure/Services/IndexService.cs index 3e619e947..bca6bb534 100644 --- a/LiteDB/DataStructure/Services/IndexService.cs +++ b/LiteDB/DataStructure/Services/IndexService.cs @@ -83,18 +83,21 @@ public IndexNode AddNode(CollectionIndex index, BsonValue key) /// private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) { + // calc key size + var keyLength = key.GetBytesCount(false); + + if (keyLength > MAX_INDEX_LENGTH) + { + throw LiteException.IndexKeyTooLong(); + } + // creating a new index node var node = new IndexNode(level) { Key = key, - KeyLength = (ushort)key.GetBytesCount(false) + KeyLength = (ushort)keyLength }; - if (node.KeyLength > MAX_INDEX_LENGTH) - { - throw LiteException.IndexKeyTooLong(); - } - // get a free page to insert my index node var page = _pager.GetFreePage(index.FreeIndexPageID, node.Length); diff --git a/LiteDB/Query/Impl/QueryContains.cs b/LiteDB/Query/Impl/QueryContains.cs index 6b7a33ed0..576d93468 100644 --- a/LiteDB/Query/Impl/QueryContains.cs +++ b/LiteDB/Query/Impl/QueryContains.cs @@ -23,7 +23,9 @@ internal override IEnumerable ExecuteIndex(IndexService indexer, Coll { var v = _value.Normalize(index.Options); - return indexer.FindAll(index, Query.Ascending).Where(x => x.Key.AsString.Contains(v)); + return indexer + .FindAll(index, Query.Ascending) + .Where(x => x.Key.IsString && x.Key.AsString.Contains(v)); } } } diff --git a/LiteDB/Serializer/Bson/BsonReader.cs b/LiteDB/Serializer/Bson/BsonReader.cs index 625a28b7b..402b5f4c8 100644 --- a/LiteDB/Serializer/Bson/BsonReader.cs +++ b/LiteDB/Serializer/Bson/BsonReader.cs @@ -24,7 +24,7 @@ public BsonDocument Deserialize(byte[] bson) /// /// Read a BsonDocument from reader /// - private BsonDocument ReadDocument(ByteReader reader) + public BsonDocument ReadDocument(ByteReader reader) { var length = reader.ReadInt32(); var end = reader.Position + length - 5; @@ -45,7 +45,7 @@ private BsonDocument ReadDocument(ByteReader reader) /// /// Read an BsonArray from reader /// - private BsonArray ReadArray(ByteReader reader) + public BsonArray ReadArray(ByteReader reader) { var length = reader.ReadInt32(); var end = reader.Position + length - 5; diff --git a/LiteDB/Serializer/Bson/BsonWriter.cs b/LiteDB/Serializer/Bson/BsonWriter.cs index 12d32d8fc..54cacba11 100644 --- a/LiteDB/Serializer/Bson/BsonWriter.cs +++ b/LiteDB/Serializer/Bson/BsonWriter.cs @@ -40,7 +40,7 @@ public void WriteDocument(ByteWriter writer, BsonDocument doc) writer.Write((byte)0x00); } - private void WriteArray(ByteWriter writer, BsonArray array) + public void WriteArray(ByteWriter writer, BsonArray array) { writer.Write(array.GetBytesCount(false)); diff --git a/LiteDB/Utils/ByteReader.cs b/LiteDB/Utils/ByteReader.cs index 767c815ea..db7de8369 100644 --- a/LiteDB/Utils/ByteReader.cs +++ b/LiteDB/Utils/ByteReader.cs @@ -173,8 +173,8 @@ public BsonValue ReadBsonValue(ushort length) case BsonType.String: return this.ReadString(length); - //case BsonType.Document: return new BsonReader().ReadDocument(reader); - //case BsonType.Array: return new BsonReader().ReadArray(reader); + case BsonType.Document: return new BsonReader().ReadDocument(this); + case BsonType.Array: return new BsonReader().ReadArray(this); case BsonType.Binary: return this.ReadBytes(length); case BsonType.ObjectId: return this.ReadObjectId(); diff --git a/LiteDB/Utils/ByteWriter.cs b/LiteDB/Utils/ByteWriter.cs index 1f4c863c2..6d585d8ce 100644 --- a/LiteDB/Utils/ByteWriter.cs +++ b/LiteDB/Utils/ByteWriter.cs @@ -210,8 +210,8 @@ public void WriteBsonValue(BsonValue value, ushort length) case BsonType.String: this.Write((String)value.RawValue, length); break; - //case BsonType.Document: new BsonWriter().WriteDocument(writer, value.AsDocument); break; - //case BsonType.Array: new BsonWriter().WriteArray(writer, value.AsArray); break; + case BsonType.Document: new BsonWriter().WriteDocument(this, value.AsDocument); break; + case BsonType.Array: new BsonWriter().WriteArray(this, value.AsArray); break; case BsonType.Binary: this.Write((Byte[])value.RawValue); break; case BsonType.ObjectId: this.Write((ObjectId)value.RawValue); break; From 180e329053a18a4771a3cc312ea0d6f482c1124a Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 10 Nov 2015 21:00:11 -0200 Subject: [PATCH 35/91] Restore no cache for extended page --- LiteDB/Core/Collections/Index.cs | 5 +++-- LiteDB/DataStructure/Services/CacheService.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index 1f77ecb48..9cd9e2179 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -41,6 +41,7 @@ public virtual bool EnsureIndex(string field, IndexOptions options) } else { + // index already exists and are the same options return false; } } @@ -51,7 +52,7 @@ public virtual bool EnsureIndex(string field, IndexOptions options) try { - // if not collection yet, create a new now + // if not exists collection yet, create a new now if (col == null) { col = this.Database.Collections.Add(this.Name); @@ -65,7 +66,7 @@ public virtual bool EnsureIndex(string field, IndexOptions options) index.Options = options; // read all objects (read from PK index) - foreach (var node in new QueryAll("_id", 1).Run(this)) + foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) { var dataBlock = this.Database.Data.Read(node.DataBlock, true); diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 66b34bf2f..2824c27ee 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -60,7 +60,7 @@ public T GetPage(uint pageID) public void AddPage(BasePage page, bool dirty = false) { // do not cache extend page - never will be reused - //if (page.PageType != PageType.Extend) + if (page.PageType != PageType.Extend) { _cache[page.PageID] = page; } From afa54b1bc9707f0f22f0836ae5de96f1bf81e62d Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 10 Nov 2015 21:09:53 -0200 Subject: [PATCH 36/91] Remove all virtual keyword in collections --- LiteDB/Core/Collections/Delete.cs | 8 ++++---- LiteDB/Core/Collections/Drop.cs | 2 +- LiteDB/Core/Collections/FindAndModify.cs | 2 ++ LiteDB/Core/Collections/Index.cs | 8 ++++---- LiteDB/Core/Collections/Insert.cs | 2 +- LiteDB/Core/Collections/LiteCollection.cs | 2 +- LiteDB/DataStructure/Services/CacheService.cs | 1 - 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/LiteDB/Core/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs index 4003c6799..74ce170fb 100644 --- a/LiteDB/Core/Collections/Delete.cs +++ b/LiteDB/Core/Collections/Delete.cs @@ -12,7 +12,7 @@ public partial class LiteCollection /// /// Remove an document in collection using Document Id - returns false if not found document /// - public virtual bool Delete(BsonValue id) + public bool Delete(BsonValue id) { if (id == null || id.IsNull) throw new ArgumentNullException("id"); @@ -59,7 +59,7 @@ public virtual bool Delete(BsonValue id) /// /// Remove all document based on a Query object. Returns removed document counts /// - public virtual int Delete(Query query) + public int Delete(Query query) { // start transaction this.Database.Transaction.Begin(); @@ -107,12 +107,12 @@ public virtual int Delete(Query query) /// /// Remove all document based on a LINQ query. Returns removed document counts /// - public virtual int Delete(Expression> predicate) + public int Delete(Expression> predicate) { return this.Delete(_visitor.Visit(predicate)); } - internal virtual void Delete(CollectionPage col, IndexNode node) + internal void Delete(CollectionPage col, IndexNode node) { // read dataBlock var dataBlock = this.Database.Data.Read(node.DataBlock, false); diff --git a/LiteDB/Core/Collections/Drop.cs b/LiteDB/Core/Collections/Drop.cs index 30c58b9e8..7b0eff95b 100644 --- a/LiteDB/Core/Collections/Drop.cs +++ b/LiteDB/Core/Collections/Drop.cs @@ -11,7 +11,7 @@ public partial class LiteCollection /// /// Drop a collection deleting all documents and indexes /// - public virtual bool Drop() + public bool Drop() { // start transaction this.Database.Transaction.Begin(); diff --git a/LiteDB/Core/Collections/FindAndModify.cs b/LiteDB/Core/Collections/FindAndModify.cs index a27c4ac8f..11dfee3f7 100644 --- a/LiteDB/Core/Collections/FindAndModify.cs +++ b/LiteDB/Core/Collections/FindAndModify.cs @@ -26,6 +26,8 @@ public void FindAndModify(Query query, Action action) foreach(var doc in docs) { action(doc); + + this.Update(doc); } this.Database.Transaction.Commit(); diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index 9cd9e2179..d4203d2c1 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -16,7 +16,7 @@ public partial class LiteCollection /// /// Document field name (case sensitive) /// All index options - public virtual bool EnsureIndex(string field, IndexOptions options) + public bool EnsureIndex(string field, IndexOptions options) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); if (options == null) throw new ArgumentNullException("options"); @@ -104,7 +104,7 @@ public virtual bool EnsureIndex(string field, IndexOptions options) /// /// Document field name (case sensitive) /// All index options - public virtual bool EnsureIndex(string field, bool unique = false) + public bool EnsureIndex(string field, bool unique = false) { return this.EnsureIndex(field, new IndexOptions { Unique = unique }); } @@ -114,7 +114,7 @@ public virtual bool EnsureIndex(string field, bool unique = false) /// /// Property linq expression /// Create a unique values index? - public virtual bool EnsureIndex(Expression> property, bool unique = false) + public bool EnsureIndex(Expression> property, bool unique = false) { var field = _visitor.GetBsonField(property); @@ -126,7 +126,7 @@ public virtual bool EnsureIndex(Expression> property, bool unique /// /// Property linq expression /// Use all indexes options - public virtual bool EnsureIndex(Expression> property, IndexOptions options) + public bool EnsureIndex(Expression> property, IndexOptions options) { var field = _visitor.GetBsonField(property); diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index d49132635..393df663a 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -11,7 +11,7 @@ public partial class LiteCollection /// /// Insert a new document to this collection. Document Id must be a new value in collection - Returns document Id /// - public virtual BsonValue Insert(T document) + public BsonValue Insert(T document) { if (document == null) throw new ArgumentNullException("document"); diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 1b85ab8f2..7791a366b 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -6,7 +6,7 @@ namespace LiteDB { - public partial class LiteCollection + public sealed partial class LiteCollection where T : new() { private uint _pageID; diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 2824c27ee..4ea28a452 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -47,7 +47,6 @@ public T GetPage(uint pageID) _cache[pageID] = specificPage; return specificPage; - //return null; } return (T)page; From 11d08a008bb578d510b5ed3f1eb88315564e27aa Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 10 Nov 2015 23:56:12 -0200 Subject: [PATCH 37/91] Thread safe first version --- LiteDB-TODO.txt | 34 ++++ LiteDB/Core/Collections/Delete.cs | 135 +++++--------- LiteDB/Core/Collections/Drop.cs | 43 +++-- LiteDB/Core/Collections/FindAndModify.cs | 50 ----- LiteDB/Core/Collections/Index.cs | 173 +++++++++--------- LiteDB/Core/Collections/Insert.cs | 142 +++++++------- LiteDB/Core/Collections/InsertBulk.cs | 45 ++--- LiteDB/Core/Collections/LiteCollection.cs | 11 +- LiteDB/Core/Collections/Rename.cs | 2 +- LiteDB/Core/Collections/Update.cs | 170 +++++++++++------ LiteDB/DataStructure/Disks/FileDiskService.cs | 7 +- LiteDB/DataStructure/Services/CacheService.cs | 64 ++++--- .../DataStructure/Services/DocumentService.cs | 20 -- LiteDB/DataStructure/Services/IndexService.cs | 1 - LiteDB/DataStructure/Services/PageService.cs | 14 +- .../Services/TransactionService.cs | 120 +++++------- LiteDB/FileStorage/LiteFileStorage.cs | 5 - LiteDB/LiteDB.csproj | 2 - 18 files changed, 521 insertions(+), 517 deletions(-) delete mode 100644 LiteDB/Core/Collections/FindAndModify.cs delete mode 100644 LiteDB/DataStructure/Services/DocumentService.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 97cb098d6..62338840f 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -3,6 +3,40 @@ - Revisar transação - Remover possibilidade - A transacao deve ficar sempre na operacao mais externa - Thread Safe + - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private + - Somente os metodos externos devem ter LOCK + - Somente os metodos externos devem ter AvoidDirtyRead + - E transacao? Fica aonde? No Externo, sempre. No interno não + - `lock` pode ter inner lock do mesmo objeto na mesma thread + - O "lock" deve estar por fora do BeginTrans() + lock(this.Database) + { + this.Database.Transaction.Begin(); + + try + { + var result = this.UpdateDocument(id, doc); + + this.Database.Transaction.Commit(); + + return result; + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } + } + - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) + + - Insert/Update/Delete/EnsureIndex/DropIndex/Rename + - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) + - Avaliar uso do ConcurrentDictionary na cache + +- FindAndModify (UpdateQuery) deve colocar os objetos em memoria e atualizar depois de rodar todos os Actions pra nao ter problema + do cara alterar durante a execuao? Se for o caso acho que posso apenas rodar no foreach(var doc in docs.ToArray()) + + - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() diff --git a/LiteDB/Core/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs index 74ce170fb..564cba19e 100644 --- a/LiteDB/Core/Collections/Delete.cs +++ b/LiteDB/Core/Collections/Delete.cs @@ -9,98 +9,31 @@ namespace LiteDB { public partial class LiteCollection { - /// - /// Remove an document in collection using Document Id - returns false if not found document - /// - public bool Delete(BsonValue id) - { - if (id == null || id.IsNull) throw new ArgumentNullException("id"); - - // start transaction - this.Database.Transaction.Begin(); - - try - { - var col = this.GetCollectionPage(false); - - // if collection not exists, document do not exists too - if (col == null) - { - this.Database.Transaction.Abort(); - return false; - } - - // normalize id before find - var value = id.Normalize(col.PK.Options); - - // find indexNode using PK index - var node = this.Database.Indexer.Find(col.PK, value, false, Query.Ascending); - - // if not found, abort transaction and returns false - if (node == null) - { - this.Database.Transaction.Abort(); - return false; - } - - this.Delete(col, node); - - this.Database.Transaction.Commit(); - - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - /// /// Remove all document based on a Query object. Returns removed document counts /// public int Delete(Query query) { - // start transaction - this.Database.Transaction.Begin(); + if(query == null) throw new ArgumentNullException("query"); - try + lock(_locker) { - var col = this.GetCollectionPage(false); + // start transaction + this.Database.Transaction.Begin(); - // no collection, no document - abort trans - if (col == null) + try { - this.Database.Transaction.Abort(); - return 0; - } + var count = this.DeleteDocuments(query); - var count = 0; + this.Database.Transaction.Commit(); - // find nodes - var nodes = query.Run(this); - - foreach (var node in nodes) - { - this.Delete(col, node); - count++; + return count; } - - // no deletes, just abort transaction (no writes) - if (count == 0) + catch (Exception ex) { - this.Database.Transaction.Abort(); - return 0; + this.Database.Transaction.Rollback(); + throw ex; } - - this.Database.Transaction.Commit(); - - return count; - } - catch (Exception ex) - { - this.Database.Transaction.Rollback(); - throw ex; } } @@ -112,19 +45,49 @@ public int Delete(Expression> predicate) return this.Delete(_visitor.Visit(predicate)); } - internal void Delete(CollectionPage col, IndexNode node) + /// + /// Remove an document in collection using Document Id - returns false if not found document + /// + public bool Delete(BsonValue id) + { + if (id == null || id.IsNull) throw new ArgumentNullException("id"); + + return this.Delete(Query.EQ("_id", id)) > 0; + } + + /// + /// Internal implementation to delete a document - no trans, no locks + /// + internal int DeleteDocuments(Query query) { - // read dataBlock - var dataBlock = this.Database.Data.Read(node.DataBlock, false); + var col = this.GetCollectionPage(false); + + // no collection, no document - abort trans + if (col == null) return 0; + + var count = 0; - // lets remove all indexes that point to this in dataBlock - foreach (var index in col.GetIndexes(true)) + // find nodes + var nodes = query.Run(this); + + foreach (var node in nodes) { - this.Database.Indexer.Delete(index, dataBlock.IndexRef[index.Slot]); + // read dataBlock + var dataBlock = this.Database.Data.Read(node.DataBlock, false); + + // lets remove all indexes that point to this in dataBlock + foreach (var index in col.GetIndexes(true)) + { + this.Database.Indexer.Delete(index, dataBlock.IndexRef[index.Slot]); + } + + // remove object data + this.Database.Data.Delete(col, node.DataBlock); + + count++; } - // remove object data - this.Database.Data.Delete(col, node.DataBlock); + return count; } } } diff --git a/LiteDB/Core/Collections/Drop.cs b/LiteDB/Core/Collections/Drop.cs index 7b0eff95b..08e64204a 100644 --- a/LiteDB/Core/Collections/Drop.cs +++ b/LiteDB/Core/Collections/Drop.cs @@ -13,33 +13,36 @@ public partial class LiteCollection /// public bool Drop() { - // start transaction - this.Database.Transaction.Begin(); - - try + lock(_locker) { - var col = this.GetCollectionPage(false); + // start transaction + this.Database.Transaction.Begin(); - // if collection not exists, no drop - if (col == null) + try { - this.Database.Transaction.Abort(); - return false; - } + var col = this.GetCollectionPage(false); - // delete all data pages + indexes pages - this.Database.Collections.Drop(col); + // if collection not exists, no drop + if (col == null) + { + this.Database.Transaction.Commit(); + return false; + } - _pageID = uint.MaxValue; + // delete all data pages + indexes pages + this.Database.Collections.Drop(col); - this.Database.Transaction.Commit(); + _pageID = uint.MaxValue; - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; + this.Database.Transaction.Commit(); + + return true; + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } } } } diff --git a/LiteDB/Core/Collections/FindAndModify.cs b/LiteDB/Core/Collections/FindAndModify.cs deleted file mode 100644 index 11dfee3f7..000000000 --- a/LiteDB/Core/Collections/FindAndModify.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace LiteDB -{ - public partial class LiteCollection - { - /// - /// Query documents and execute, for each document, action method. All data is locked during execution - /// - public void FindAndModify(Query query, Action action) - { - if (query == null) throw new ArgumentNullException("query"); - if (action == null) throw new ArgumentNullException("action"); - - this.Database.Transaction.Begin(); - - try - { - var docs = this.Find(query); - - foreach(var doc in docs) - { - action(doc); - - this.Update(doc); - } - - this.Database.Transaction.Commit(); - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - - /// - /// Query documents and execute, for each document, action method. All data is locked during execution - /// - public void FindAndModify(Expression> predicate, Action action) - { - this.FindAndModify(_visitor.Visit(predicate), action); - } - } -} diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index d4203d2c1..84659409d 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -24,79 +24,82 @@ public bool EnsureIndex(string field, IndexOptions options) if (!CollectionIndex.IndexPattern.IsMatch(field)) throw LiteException.InvalidFormat("IndexField", field); - // do not create collection at this point - var col = this.GetCollectionPage(false); - - if (col != null) + lock (_locker) { - // check if index already exists but has diferent options - var existsIndex = col.GetIndex(field); + // do not create collection at this point + var col = this.GetCollectionPage(false); - if (existsIndex != null) + if (col != null) { - if(!options.Equals(existsIndex.Options)) + // check if index already exists but has diferent options + var existsIndex = col.GetIndex(field); + + if (existsIndex != null) { - // drop index and create another - this.DropIndex(field); + if(!options.Equals(existsIndex.Options)) + { + // drop index and create another + this.DropIndex(field); + } + else + { + // index already exists and are the same options + return false; + } } - else + }; + + // start transaction + this.Database.Transaction.Begin(); + + try + { + // if not exists collection yet, create a new now + if (col == null) { - // index already exists and are the same options - return false; + col = this.Database.Collections.Add(this.Name); + _pageID = col.PageID; } - } - }; - // start transaction - this.Database.Transaction.Begin(); + // create index head + var index = this.Database.Indexer.CreateIndex(col); - try - { - // if not exists collection yet, create a new now - if (col == null) - { - col = this.Database.Collections.Add(this.Name); - _pageID = col.PageID; - } + index.Field = field; + index.Options = options; - // create index head - var index = this.Database.Indexer.CreateIndex(col); + // read all objects (read from PK index) + foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) + { + var dataBlock = this.Database.Data.Read(node.DataBlock, true); - index.Field = field; - index.Options = options; + // read object + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - // read all objects (read from PK index) - foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) - { - var dataBlock = this.Database.Data.Read(node.DataBlock, true); + // adding index + var key = doc.Get(field); - // read object - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + var newNode = this.Database.Indexer.AddNode(index, key); - // adding index - var key = doc.Get(field); + // adding this new index Node to indexRef + dataBlock.IndexRef[index.Slot] = newNode.Position; - var newNode = this.Database.Indexer.AddNode(index, key); + // link index node to datablock + newNode.DataBlock = dataBlock.Position; - // adding this new index Node to indexRef - dataBlock.IndexRef[index.Slot] = newNode.Position; + // mark datablock page as dirty + this.Database.Pager.SetDirty(dataBlock.Page); + } - // link index node to datablock - newNode.DataBlock = dataBlock.Position; + this.Database.Transaction.Commit(); - // mark datablock page as dirty - this.Database.Pager.SetDirty(dataBlock.Page); + return true; + } + catch + { + this.Database.Transaction.Rollback(); + throw; } - - this.Database.Transaction.Commit(); - } - catch - { - this.Database.Transaction.Rollback(); - throw; } - - return true; } /// @@ -189,7 +192,6 @@ internal CollectionIndex GetOrCreateIndex(string field) return index; } - /// /// Drop index and release slot for another index /// @@ -198,46 +200,49 @@ public bool DropIndex(string field) if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); if (field == "_id") throw LiteException.IndexDropId(); - // start transaction - this.Database.Transaction.Begin(); - - try + lock(_locker) { - var col = this.GetCollectionPage(false); + // start transaction + this.Database.Transaction.Begin(); - // if collection not exists, no drop - if (col == null) + try { - this.Database.Transaction.Abort(); - return false; - } + var col = this.GetCollectionPage(false); - // search for index reference - var index = col.GetIndex(field); + // if collection not exists, no drop + if (col == null) + { + this.Database.Transaction.Commit(); + return false; + } - if (index == null) - { - this.Database.Transaction.Abort(); - return false; - } + // search for index reference + var index = col.GetIndex(field); - // delete all data pages + indexes pages - this.Database.Indexer.DropIndex(index); + if (index == null) + { + this.Database.Transaction.Commit(); + return false; + } - // clear index reference - index.Clear(); + // delete all data pages + indexes pages + this.Database.Indexer.DropIndex(index); - // save collection page - this.Database.Pager.SetDirty(col); + // clear index reference + index.Clear(); - this.Database.Transaction.Commit(); + // save collection page + this.Database.Pager.SetDirty(col); - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; + this.Database.Transaction.Commit(); + + return true; + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } } } } diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 393df663a..728289dda 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -15,97 +15,113 @@ public BsonValue Insert(T document) { if (document == null) throw new ArgumentNullException("document"); - this.Database.Transaction.Begin(); - - try + lock (_locker) { - // set an id value if document object needs - this.Database.Mapper.SetAutoId(document, this.GetBsonCollection()); + this.Database.Transaction.Begin(); - var doc = this.Database.Mapper.ToDocument(document); + try + { + var result = this.InsertDocument(document); - BsonValue id; + this.Database.Transaction.Commit(); - // add ObjectId to _id if _id not found - if (!doc.RawValue.TryGetValue("_id", out id)) - { - id = doc["_id"] = ObjectId.NewObjectId(); + return result; } - - // test if _id is a valid type - if (id.IsNull || id.IsMinValue || id.IsMaxValue) + catch { - this.Database.Transaction.Abort(); - throw LiteException.InvalidDataType("_id", id); + this.Database.Transaction.Rollback(); + throw; } + } + } - // serialize object - var bytes = BsonSerializer.Serialize(doc); - - var col = this.GetCollectionPage(true); - - // storage in data pages - returns dataBlock address - var dataBlock = this.Database.Data.Insert(col, bytes); + /// + /// Insert an array of new documents to this collection. Document Id must be a new value in collection + /// + public int Insert(IEnumerable docs) + { + if (docs == null) throw new ArgumentNullException("docs"); - // store id in a PK index [0 array] - var pk = this.Database.Indexer.AddNode(col.PK, id); + var count = 0; - // do links between index <-> data block - pk.DataBlock = dataBlock.Position; - dataBlock.IndexRef[0] = pk.Position; + lock (_locker) + { + this.Database.Transaction.Begin(); - // for each index, insert new IndexNode - foreach(var index in col.GetIndexes(false)) + try { - var key = doc.Get(index.Field); - - var node = this.Database.Indexer.AddNode(index, key); + foreach (var doc in docs) + { + this.InsertDocument(doc); + count++; + } - // point my index to data object - node.DataBlock = dataBlock.Position; + this.Database.Transaction.Commit(); - // point my dataBlock - dataBlock.IndexRef[index.Slot] = node.Position; + return count; + } + catch + { + this.Database.Transaction.Rollback(); + throw; } - - this.Database.Transaction.Commit(); - - return id; - } - catch - { - this.Database.Transaction.Rollback(); - throw; } } /// - /// Insert an array of new documents to this collection. Document Id must be a new value in collection + /// Internal implementation of Insert a document - no trans, no locks /// - public virtual int Insert(IEnumerable docs) + private BsonValue InsertDocument(T document) { - if (docs == null) throw new ArgumentNullException("docs"); + // set an id value if document object needs + this.Database.Mapper.SetAutoId(document, this.GetBsonCollection()); - this.Database.Transaction.Begin(); - var count = 0; + var doc = this.Database.Mapper.ToDocument(document); - try - { - foreach (var doc in docs) - { - this.Insert(doc); - count++; - } + BsonValue id; - this.Database.Transaction.Commit(); + // add ObjectId to _id if _id not found + if (!doc.RawValue.TryGetValue("_id", out id)) + { + id = doc["_id"] = ObjectId.NewObjectId(); + } - return count; + // test if _id is a valid type + if (id.IsNull || id.IsMinValue || id.IsMaxValue) + { + throw LiteException.InvalidDataType("_id", id); } - catch + + // serialize object + var bytes = BsonSerializer.Serialize(doc); + + var col = this.GetCollectionPage(true); + + // storage in data pages - returns dataBlock address + var dataBlock = this.Database.Data.Insert(col, bytes); + + // store id in a PK index [0 array] + var pk = this.Database.Indexer.AddNode(col.PK, id); + + // do links between index <-> data block + pk.DataBlock = dataBlock.Position; + dataBlock.IndexRef[0] = pk.Position; + + // for each index, insert new IndexNode + foreach (var index in col.GetIndexes(false)) { - this.Database.Transaction.Rollback(); - throw; + var key = doc.Get(index.Field); + + var node = this.Database.Indexer.AddNode(index, key); + + // point my index to data object + node.DataBlock = dataBlock.Position; + + // point my dataBlock + dataBlock.IndexRef[index.Slot] = node.Position; } + + return id; } } } diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs index 760ebda3d..0188b9076 100644 --- a/LiteDB/Core/Collections/InsertBulk.cs +++ b/LiteDB/Core/Collections/InsertBulk.cs @@ -15,41 +15,42 @@ public int InsertBulk(IEnumerable docs, int buffer = 32768) { if (docs == null) throw new ArgumentNullException("docs"); if (buffer < 100) throw new ArgumentException("buffer must be bigger than 100"); - if (this.Database.Transaction.IsInTransaction) throw LiteException.InvalidTransaction(); var enumerator = docs.GetEnumerator(); var count = 0; - while (true) + lock(_locker) { - var buff = buffer; - - this.Database.Transaction.Begin(); - - try + while (true) { - var more = true; + var buff = buffer; - while (buff > 0 && (more = enumerator.MoveNext())) + this.Database.Transaction.Begin(); + + try { - this.Insert(enumerator.Current); - buff--; - count++; - } + var more = true; - this.Database.Transaction.Commit(); - this.Database.Cache.Clear(); + while (buff > 0 && (more = enumerator.MoveNext())) + { + this.InsertDocument(enumerator.Current); + buff--; + count++; + } - if (more == false) + this.Database.Transaction.Commit(); + + if (more == false) + { + return count; + } + } + catch { - return count; + this.Database.Transaction.Rollback(); + throw; } } - catch - { - this.Database.Transaction.Rollback(); - throw; - } } } } diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 7791a366b..12af3a116 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -13,6 +13,9 @@ public sealed partial class LiteCollection private List> _includes; private QueryVisitor _visitor; + // Use a locker object (LiteDatabase) to thread safe + private object _locker; + /// /// Get collection name /// @@ -30,6 +33,7 @@ internal LiteCollection(LiteDatabase db, string name) _pageID = uint.MaxValue; _visitor = new QueryVisitor(db.Mapper); _includes = new List>(); + _locker = db; } /// @@ -37,13 +41,6 @@ internal LiteCollection(LiteDatabase db, string name) /// internal CollectionPage GetCollectionPage(bool addIfNotExits) { - // when a operation is read-only, request collection page without add new one - // use this moment to check if data file was changed (if in transaction, do nothing) - if (addIfNotExits == false) - { - this.Database.Transaction.AvoidDirtyRead(); - } - // _pageID never change, even if data file was changed if (_pageID == uint.MaxValue) { diff --git a/LiteDB/Core/Collections/Rename.cs b/LiteDB/Core/Collections/Rename.cs index d7fa689a0..7ec784625 100644 --- a/LiteDB/Core/Collections/Rename.cs +++ b/LiteDB/Core/Collections/Rename.cs @@ -22,7 +22,7 @@ public bool Rename(string newName) if (col == null) { - this.Database.Transaction.Abort(); + this.Database.Transaction.Commit(); return false; } diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index 9f0183483..0342419e8 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Text; namespace LiteDB @@ -22,7 +23,24 @@ public bool Update(T document) if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - return this.UpdateDoc(id, doc); + lock(_locker) + { + this.Database.Transaction.Begin(); + + try + { + var result = this.UpdateDocument(id, doc); + + this.Database.Transaction.Commit(); + + return result; + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } + } } /// @@ -36,79 +54,129 @@ public bool Update(BsonValue id, T document) // get BsonDocument from object var doc = this.Database.Mapper.ToDocument(document); - return this.UpdateDoc(id, doc); + lock (_locker) + { + this.Database.Transaction.Begin(); + + try + { + var result = this.UpdateDocument(id, doc); + + this.Database.Transaction.Commit(); + + return result; + } + catch + { + this.Database.Transaction.Rollback(); + throw; + } + } } - private bool UpdateDoc(BsonValue id, BsonDocument doc) + /// + /// Query documents and execute, for each document, action method. After action, update each document + /// + public int Update(Query query, Action action) { - // serialize object - var bytes = BsonSerializer.Serialize(doc); + if (query == null) throw new ArgumentNullException("query"); + if (action == null) throw new ArgumentNullException("action"); - // start transaction - this.Database.Transaction.Begin(); - - try + lock(_locker) { - var col = this.GetCollectionPage(false); + this.Database.Transaction.Begin(); - // if no collection, no updates - if (col == null) + try { - this.Database.Transaction.Abort(); - return false; - } + var docs = this.Find(query).ToArray(); // used to avoid changes during Action + var count = 0; - // normalize id before find - var value = id.Normalize(col.PK.Options); + foreach (var doc in docs) + { + action(doc); - // find indexNode from pk index - var indexNode = this.Database.Indexer.Find(col.PK, value, false, Query.Ascending); + // get BsonDocument from object + var bson = this.Database.Mapper.ToDocument(doc); - // if not found document, no updates - if (indexNode == null) - { - this.Database.Transaction.Abort(); - return false; - } + var id = bson["_id"]; + + if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - // update data storage - var dataBlock = this.Database.Data.Update(col, indexNode.DataBlock, bytes); + count += this.UpdateDocument(id, bson) ? 1 : 0; + } - // delete/insert indexes - do not touch on PK - foreach (var index in col.GetIndexes(false)) + this.Database.Transaction.Commit(); + + return count; + } + catch { - var key = doc.Get(index.Field); + this.Database.Transaction.Rollback(); + throw; + } + } + } - var node = this.Database.Indexer.GetNode(dataBlock.IndexRef[index.Slot]); + /// + /// Query documents and execute, for each document, action method. All data is locked during execution + /// + public void Update(Expression> predicate, Action action) + { + this.Update(_visitor.Visit(predicate), action); + } - // check if my index node was changed - if (node.Key.CompareTo(key) != 0) - { - // remove old index node - this.Database.Indexer.Delete(index, node.Position); + /// + /// Internal implementation of Update a document (no lock, no transaction) + /// + private bool UpdateDocument(BsonValue id, BsonDocument doc) + { + // serialize object + var bytes = BsonSerializer.Serialize(doc); - // and add a new one - var newNode = this.Database.Indexer.AddNode(index, key); + var col = this.GetCollectionPage(false); - // point my index to data object - newNode.DataBlock = dataBlock.Position; + // if no collection, no updates + if (col == null) return false; - // point my dataBlock - dataBlock.IndexRef[index.Slot] = newNode.Position; + // normalize id before find + var value = id.Normalize(col.PK.Options); - this.Database.Pager.SetDirty(dataBlock.Page); - } - } + // find indexNode from pk index + var indexNode = this.Database.Indexer.Find(col.PK, value, false, Query.Ascending); - this.Database.Transaction.Commit(); + // if not found document, no updates + if (indexNode == null) return false; - return true; - } - catch + // update data storage + var dataBlock = this.Database.Data.Update(col, indexNode.DataBlock, bytes); + + // delete/insert indexes - do not touch on PK + foreach (var index in col.GetIndexes(false)) { - this.Database.Transaction.Rollback(); - throw; + var key = doc.Get(index.Field); + + var node = this.Database.Indexer.GetNode(dataBlock.IndexRef[index.Slot]); + + // check if my index node was changed + if (node.Key.CompareTo(key) != 0) + { + // remove old index node + this.Database.Indexer.Delete(index, node.Position); + + // and add a new one + var newNode = this.Database.Indexer.AddNode(index, key); + + // point my index to data object + newNode.DataBlock = dataBlock.Position; + + // point my dataBlock + dataBlock.IndexRef[index.Slot] = newNode.Position; + + this.Database.Pager.SetDirty(dataBlock.Page); + } } + + return true; } } } diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index 9f8e83450..c478d4612 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -119,8 +119,7 @@ public byte[] ReadPage(uint pageID) } // read bytes from data file - _stream.Read(buffer, 0, BasePage.PAGE_SIZE); - + _stream.Read(buffer, 0, BasePage.PAGE_SIZE); }); return buffer; @@ -160,7 +159,7 @@ public void WriteJournal(uint pageID, byte[] data) }); } - Console.WriteLine("journal write " + pageID); + //Console.WriteLine("journal write " + pageID); // just write original bytes in order that are changed _journal.Write(data, 0, BasePage.PAGE_SIZE); @@ -243,7 +242,7 @@ private void Recovery(FileStream journal) fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } - Console.WriteLine("recovery " + pageID); + //Console.WriteLine("recovery " + pageID); // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/DataStructure/Services/CacheService.cs index 4ea28a452..0bef37f33 100644 --- a/LiteDB/DataStructure/Services/CacheService.cs +++ b/LiteDB/DataStructure/Services/CacheService.cs @@ -14,7 +14,7 @@ namespace LiteDB /// internal class CacheService : IDisposable { - // single cache structure + // single cache structure - use Sort dictionary for get pages in order (fast to store in sequence on disk) private SortedDictionary _cache; private SortedDictionary _dirty; @@ -44,7 +44,10 @@ public T GetPage(uint pageID) specificPage.ReadPage(page.DiskData); - _cache[pageID] = specificPage; + lock(_cache) + { + _cache[pageID] = specificPage; + } return specificPage; } @@ -58,24 +61,27 @@ public T GetPage(uint pageID) /// public void AddPage(BasePage page, bool dirty = false) { - // do not cache extend page - never will be reused - if (page.PageType != PageType.Extend) + lock(_cache) { - _cache[page.PageID] = page; - } - - // page is dirty? add in a special list too and mark as dirty (all type of pages) - if (dirty && !page.IsDirty) - { - page.IsDirty = true; - _dirty[page.PageID] = page; - _cache[page.PageID] = page; + // do not cache extend page - never will be reused + if (page.PageType != PageType.Extend) + { + _cache[page.PageID] = page; + } - // if page is new (not exits on datafile), there is no journal for them - if(page.DiskData.Length > 0) + // page is dirty? add in a special list too and mark as dirty (all type of pages) + if (dirty && !page.IsDirty) { - // call action passing dirty page - used for journal file writes - MarkAsDirtyAction(page); + page.IsDirty = true; + _dirty[page.PageID] = page; + _cache[page.PageID] = page; + + // if page is new (not exits on datafile), there is no journal for them + if(page.DiskData.Length > 0) + { + // call action passing dirty page - used for journal file writes + MarkAsDirtyAction(page); + } } } } @@ -87,8 +93,11 @@ public bool Clear() { var hasDirty = _dirty.Count > 0; - _dirty.Clear(); - _cache.Clear(); + lock(_cache) + { + _dirty.Clear(); + _cache.Clear(); + } return hasDirty; } @@ -98,12 +107,21 @@ public bool Clear() /// public void ClearDirty() { - foreach(var page in _dirty.Values) + lock(_cache) { - page.IsDirty = false; - } + foreach(var page in _dirty.Values) + { + page.IsDirty = false; - _dirty.Clear(); + // remove all non-header-collection pages (this can be optional in future) + if (page.PageType != PageType.Header && page.PageType != PageType.Collection) + { + _cache.Remove(page.PageID); + } + } + + _dirty.Clear(); + } } public bool HasDirtyPages { get { return _dirty.Count() > 0; } } diff --git a/LiteDB/DataStructure/Services/DocumentService.cs b/LiteDB/DataStructure/Services/DocumentService.cs deleted file mode 100644 index c2db62967..000000000 --- a/LiteDB/DataStructure/Services/DocumentService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - /// - /// Highest level services - take care all basics operation about a document: CRUD+Index - /// - internal class DocumentService - { - - - internal DocumentService() - { - } - } -} diff --git a/LiteDB/DataStructure/Services/IndexService.cs b/LiteDB/DataStructure/Services/IndexService.cs index bca6bb534..f5df0667e 100644 --- a/LiteDB/DataStructure/Services/IndexService.cs +++ b/LiteDB/DataStructure/Services/IndexService.cs @@ -18,7 +18,6 @@ internal class IndexService public const int MAX_INDEX_LENGTH = 512; private PageService _pager; - private Random _rand = new Random(); public IndexService(PageService pager) diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/DataStructure/Services/PageService.cs index a192b0dc9..134714332 100644 --- a/LiteDB/DataStructure/Services/PageService.cs +++ b/LiteDB/DataStructure/Services/PageService.cs @@ -30,18 +30,24 @@ public T GetPage(uint pageID) { page = new T(); - var buffer = _disk.ReadPage(pageID); + lock(_disk) + { + var buffer = _disk.ReadPage(pageID); - page.ReadPage(buffer); + page.ReadPage(buffer); - Console.WriteLine("read " + pageID + " (" + typeof(T).Name + ")"); + //Console.WriteLine("read " + pageID + " (" + typeof(T).Name + ")"); - _cache.AddPage(page); + _cache.AddPage(page); + } } return page; } + /// + /// Add a page to cache and mark them as dirty too. + /// public void SetDirty(BasePage page) { _cache.AddPage(page, true); diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index bf45bdd83..835aa9860 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -13,8 +13,7 @@ internal class TransactionService { private IDiskService _disk; private CacheService _cache; - - private int _level = 0; + private bool _trans = false; internal TransactionService(IDiskService disk, CacheService cache) { @@ -23,41 +22,19 @@ internal TransactionService(IDiskService disk, CacheService cache) _cache.MarkAsDirtyAction = (page) => _disk.WriteJournal(page.PageID, page.DiskData); } - public bool IsInTransaction { get { return _level > 0; } } - /// /// Starts a new transaction - lock database to garantee that only one processes is in a transaction /// public void Begin() { - if (_level == 0) - { - this.AvoidDirtyRead(); + if(_trans == true) throw new SystemException("Begin transaction"); - // lock (or try to) datafile - _disk.Lock(); - } + this.AvoidDirtyRead(); - _level++; - } + // lock (or try to) datafile + _disk.Lock(); - /// - /// Abort a transaction is used when begin and has no changes yet - no writes, no checks (it's simple than rollback) - /// - public void Abort() - { - if (_level == 0) return; - - if (_level == 1) - { - _disk.Unlock(); - - _level = 0; - } - else - { - _level--; - } + _trans = true; } /// @@ -65,54 +42,47 @@ public void Abort() /// public void Commit() { - if (_level == 0) return; + if (_trans == false) throw new SystemException("Commit transaction"); - if (_level == 1) + if (_cache.HasDirtyPages) { - if (_cache.HasDirtyPages) - { - var header = _cache.GetPage(0); + var header = _cache.GetPage(0); - // increase file changeID (back to 0 when overflow) - header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); + // increase file changeID (back to 0 when overflow) + header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); - // add header page as dirty to cache - _cache.AddPage(header, true); + // add header page as dirty to cache + _cache.AddPage(header, true); - // commit journal file - it will be used if write operation fails - _disk.CommitJournal(); + // commit journal file - it will be used if write operation fails + _disk.CommitJournal(); - // write all dirty pages in data file - foreach (var page in _cache.GetDirtyPages()) - { - Console.WriteLine("save page " + page.PageID); - _disk.WritePage(page.PageID, page.WritePage()); - } - - // delete journal file - datafile is consist here - _disk.DeleteJournal(); - - // set all dirty pages as clear on cache - _cache.ClearDirty(); + // write all dirty pages in data file + foreach (var page in _cache.GetDirtyPages()) + { + //Console.WriteLine("save page " + page.PageID); + _disk.WritePage(page.PageID, page.WritePage()); } - // unlock datafile - _disk.Unlock(); + // delete journal file - datafile is consist here + _disk.DeleteJournal(); - _level = 0; - } - else - { - _level--; + // set all dirty pages as clear on cache + _cache.ClearDirty(); } + + // unlock datafile + _disk.Unlock(); + + _trans = false; } public void Rollback() { - if (_level == 0) return; + if (_trans == false) throw new SystemException("Rollback transaction"); // clear all pages from memory (return true if has dirty pages on cache) - if(_cache.Clear()) + if (_cache.Clear()) { // if has dirty page, has journal file - delete it (is not valid) _disk.DeleteJournal(); @@ -121,7 +91,7 @@ public void Rollback() // unlock datafile _disk.Unlock(); - _level = 0; + _trans = false; } /// @@ -130,22 +100,24 @@ public void Rollback() /// public void AvoidDirtyRead() { - // if is in transaction pages are not dirty (begin trans was checked) - if(this.IsInTransaction == true) return; + lock (_disk) + { + // if is in transaction pages are not dirty (begin trans was checked) + if (_trans == true) return; - var cache = _cache.GetPage(0); + var cache = _cache.GetPage(0); - if (cache == null) return; + if (cache == null) return; - // read change direct from disk - var change = _disk.GetChangeID(); + // read change direct from disk + var change = _disk.GetChangeID(); - // if changeID was changed, file was changed by another process - clear all cache - if (cache.ChangeID != change) - { - Console.WriteLine("Datafile changed, clear cache"); - - _cache.Clear(); + // if changeID was changed, file was changed by another process - clear all cache + if (cache.ChangeID != change) + { + //Console.WriteLine("Datafile changed, clear cache"); + _cache.Clear(); + } } } } diff --git a/LiteDB/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs index b7c3a7274..234e818e4 100644 --- a/LiteDB/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -32,9 +32,6 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) if (file == null) throw new ArgumentNullException("id"); if (stream == null) throw new ArgumentNullException("stream"); - // no transaction allowed - if (this.Database.Transaction.IsInTransaction) throw LiteException.InvalidTransaction(); - file.UploadDate = DateTime.Now; // insert file in _files collections with 0 file length @@ -190,8 +187,6 @@ public bool Delete(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id"); - if (this.Database.Transaction.IsInTransaction) throw LiteException.InvalidTransaction(); - // remove file reference in _files var d = this.Files.Delete(id); diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 75b876c6e..97698c453 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -52,9 +52,7 @@ - - From b965ce38d18c2b1ac5ed38a48edb2f7f22c136b4 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 10:21:38 -0200 Subject: [PATCH 38/91] Fix journal recovery bug --- LiteDB/DataStructure/Disks/FileDiskService.cs | 7 ++++++- LiteDB/DataStructure/Disks/IDiskService.cs | 2 +- LiteDB/DataStructure/Services/TransactionService.cs | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/DataStructure/Disks/FileDiskService.cs index c478d4612..2d866837e 100644 --- a/LiteDB/DataStructure/Disks/FileDiskService.cs +++ b/LiteDB/DataStructure/Disks/FileDiskService.cs @@ -165,7 +165,7 @@ public void WriteJournal(uint pageID, byte[] data) _journal.Write(data, 0, BasePage.PAGE_SIZE); } - public void CommitJournal() + public void CommitJournal(long fileSize) { if (_journalEnabled == false) return; @@ -175,6 +175,9 @@ public void CommitJournal() // flush all journal file data to disk _journal.Flush(); + + // fileSize parameter tell me final size of data file - helpful to extend first datafile + _stream.SetLength(fileSize); } public void DeleteJournal() @@ -226,6 +229,8 @@ private void Recovery(FileStream journal) var fileSize = _stream.Length; var buffer = new byte[BasePage.PAGE_SIZE]; + journal.Seek(0, SeekOrigin.Begin); + while (journal.Position < journal.Length) { // read page bytes from journal file diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/DataStructure/Disks/IDiskService.cs index 35380abed..15101301a 100644 --- a/LiteDB/DataStructure/Disks/IDiskService.cs +++ b/LiteDB/DataStructure/Disks/IDiskService.cs @@ -16,7 +16,7 @@ public interface IDiskService : IDisposable void Unlock(); void WriteJournal(uint pageID, byte[] original); - void CommitJournal(); + void CommitJournal(long fileSize); void DeleteJournal(); byte[] ReadPage(uint pageID); diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/DataStructure/Services/TransactionService.cs index 835aa9860..3a3b3ce3c 100644 --- a/LiteDB/DataStructure/Services/TransactionService.cs +++ b/LiteDB/DataStructure/Services/TransactionService.cs @@ -55,13 +55,18 @@ public void Commit() _cache.AddPage(header, true); // commit journal file - it will be used if write operation fails - _disk.CommitJournal(); + _disk.CommitJournal((header.LastPageID + 1) * BasePage.PAGE_SIZE); // write all dirty pages in data file foreach (var page in _cache.GetDirtyPages()) { //Console.WriteLine("save page " + page.PageID); _disk.WritePage(page.PageID, page.WritePage()); + + if(page.PageID == 800) + { + Console.WriteLine("parar"); + } } // delete journal file - datafile is consist here From 06954f878e2f96ba592eaedd8b559806240f068d Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 11:31:11 -0200 Subject: [PATCH 39/91] Fix code issue --- LiteDB-TODO.txt | 41 +++++++- LiteDB/Core/Collections/Include.cs | 2 +- LiteDB/Core/Collections/Index.cs | 160 ++++++++++++++--------------- LiteDB/Core/Collections/Insert.cs | 6 +- 4 files changed, 122 insertions(+), 87 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 62338840f..e0e259f86 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -8,6 +8,7 @@ - Somente os metodos externos devem ter AvoidDirtyRead - E transacao? Fica aonde? No Externo, sempre. No interno não - `lock` pode ter inner lock do mesmo objeto na mesma thread + - A BeginTrans() deve ser aberto antes de fazer consultas no banco - para garantir o AvoidDirtyRead - O "lock" deve estar por fora do BeginTrans() lock(this.Database) { @@ -33,9 +34,6 @@ - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache -- FindAndModify (UpdateQuery) deve colocar os objetos em memoria e atualizar depois de rodar todos os Actions pra nao ter problema - do cara alterar durante a execuao? Se for o caso acho que posso apenas rodar no foreach(var doc in docs.ToArray()) - - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache @@ -44,6 +42,8 @@ - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.TextWriters = Stream(); db.Debbuger.Counters["Abc"].Times | Count + +- Remover conferencia de opções de um indice - se quiser q drop e recria com outras opções ## Debbuger @@ -116,6 +116,41 @@ e) Leitura conforme indice (retorno poucos registros) - Logotipo + Nome - Atualizar codeproject +## Vamos fazer??? Vai ser legal e meio insano - quem sabe em uma outra branch? + +LiteEngine + - Só trabalha com BsonDocument + - Query sem Linq (só bsonDocument) + - Totalmente interna + - Só faz o minimo de acesso a engine + - Cuida das transacoes, locks?! A pensar + - Inclui a query + - A solução final pode não ficar tão eficiente quando que temos hoje, mas acho que há uma melhora na redução de bugs + +LiteEngine(IDiskService) { + // AllServices + + Insert(colName, myDocInBson) : id + Update(colName, myId, myDocInBson): bool + Delete(colName, query) : int + Query(colName, query) : IEnumerable + Min\Max\Count\Exists(colName, query) + EnsureIndex(colName, fieldName) : bool + DropIndex(colName, fieldName) : bool + RenameCollection(colName, newName) : bool + + GetIndexes(colName) ? para que? só se for pro Shell?! + +} + +## Modulos +- Engine + - QueryEngine +- Mapper +- Outros: Linq, shell, fileStorage +- API + + ================================================================================================================ ================================================================================================================ diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index c24e0d044..f4b07a46f 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -53,7 +53,7 @@ public LiteCollection Include(Expression> dbref) }; // cloning this collection and adding this include - var newcol = new LiteCollection(this.Database, Name); + var newcol = new LiteCollection(this.Database, this.Name); newcol._pageID = _pageID; newcol._includes.AddRange(_includes); diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index 84659409d..bb5f3bc8b 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -26,34 +26,21 @@ public bool EnsureIndex(string field, IndexOptions options) lock (_locker) { - // do not create collection at this point - var col = this.GetCollectionPage(false); + // start transaction + this.Database.Transaction.Begin(); - if (col != null) + try { - // check if index already exists but has diferent options - var existsIndex = col.GetIndex(field); + // do not create collection at this point + var col = this.GetCollectionPage(false); - if (existsIndex != null) + // check if index already exists (if collection exists) + if (col != null && col.GetIndex(field) != null) { - if(!options.Equals(existsIndex.Options)) - { - // drop index and create another - this.DropIndex(field); - } - else - { - // index already exists and are the same options - return false; - } + this.Database.Transaction.Commit(); + return false; } - }; - - // start transaction - this.Database.Transaction.Begin(); - try - { // if not exists collection yet, create a new now if (col == null) { @@ -64,31 +51,7 @@ public bool EnsureIndex(string field, IndexOptions options) // create index head var index = this.Database.Indexer.CreateIndex(col); - index.Field = field; - index.Options = options; - - // read all objects (read from PK index) - foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) - { - var dataBlock = this.Database.Data.Read(node.DataBlock, true); - - // read object - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - - // adding index - var key = doc.Get(field); - - var newNode = this.Database.Indexer.AddNode(index, key); - - // adding this new index Node to indexRef - dataBlock.IndexRef[index.Slot] = newNode.Position; - - // link index node to datablock - newNode.DataBlock = dataBlock.Position; - - // mark datablock page as dirty - this.Database.Pager.SetDirty(dataBlock.Page); - } + this.EnsureIndex(col, field, options); this.Database.Transaction.Commit(); @@ -156,6 +119,9 @@ public IEnumerable GetIndexes() } } + /// + /// Get an index or create a new one if not exists - if no collection, no need create an index - no lock, no trans + /// internal CollectionIndex GetOrCreateIndex(string field) { // get collection page that contains all indexes @@ -167,7 +133,7 @@ internal CollectionIndex GetOrCreateIndex(string field) // get index var index = col.GetIndex(field); - // if index not found, lets check if type T has [BsonIndex] + // if index not found, lets check if type T has [BsonIndex] with custom options if (index == null && typeof(T) != typeof(BsonDocument)) { var options = this.Database.Mapper.GetIndexFromMapper(field); @@ -175,18 +141,51 @@ internal CollectionIndex GetOrCreateIndex(string field) // create a new index using BsonIndex options if (options != null) { - this.EnsureIndex(field, options); - - index = col.GetIndex(field); + index = this.EnsureIndex(col, field, options); } } // if no index, let's auto create an index with default index options if (index == null) { - this.EnsureIndex(field); + index = this.EnsureIndex(col, field, new IndexOptions()); + } + + return index; + } + + /// + /// Internal implementation of create an index - no locks, no trans + /// + private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptions options) + { + // create index head + var index = this.Database.Indexer.CreateIndex(col); + + index.Field = field; + index.Options = options; + + // read all objects (read from PK index) + foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) + { + var dataBlock = this.Database.Data.Read(node.DataBlock, true); + + // read object + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - index = col.GetIndex(field); + // adding index + var key = doc.Get(field); + + var newNode = this.Database.Indexer.AddNode(index, key); + + // adding this new index Node to indexRef + dataBlock.IndexRef[index.Slot] = newNode.Position; + + // link index node to datablock + newNode.DataBlock = dataBlock.Position; + + // mark datablock page as dirty + this.Database.Pager.SetDirty(dataBlock.Page); } return index; @@ -200,39 +199,25 @@ public bool DropIndex(string field) if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); if (field == "_id") throw LiteException.IndexDropId(); - lock(_locker) + var col = this.GetCollectionPage(false); + + // no collection, no index + if(col == null) return false; + + // search for index reference + var index = col.GetIndex(field); + + // no index, no drop + if (index == null) return false; + + lock (_locker) { // start transaction this.Database.Transaction.Begin(); try { - var col = this.GetCollectionPage(false); - - // if collection not exists, no drop - if (col == null) - { - this.Database.Transaction.Commit(); - return false; - } - - // search for index reference - var index = col.GetIndex(field); - - if (index == null) - { - this.Database.Transaction.Commit(); - return false; - } - - // delete all data pages + indexes pages - this.Database.Indexer.DropIndex(index); - - // clear index reference - index.Clear(); - - // save collection page - this.Database.Pager.SetDirty(col); + this.DropIndex(col, index); this.Database.Transaction.Commit(); @@ -245,5 +230,20 @@ public bool DropIndex(string field) } } } + + /// + /// Internal implementation of drop an index - no lock, no trans + /// + private void DropIndex(CollectionPage col, CollectionIndex index) + { + // delete all data pages + indexes pages + this.Database.Indexer.DropIndex(index); + + // clear index reference + index.Clear(); + + // save collection page + this.Database.Pager.SetDirty(col); + } } } diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 728289dda..21470ebaa 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -21,11 +21,11 @@ public BsonValue Insert(T document) try { - var result = this.InsertDocument(document); + var id = this.InsertDocument(document); this.Database.Transaction.Commit(); - return result; + return id; } catch { @@ -69,7 +69,7 @@ public int Insert(IEnumerable docs) } /// - /// Internal implementation of Insert a document - no trans, no locks + /// Internal implementation of Insert a document. Returns document _id - no trans, no locks /// private BsonValue InsertDocument(T document) { From 7a52feb64fd71c9b076ace1e2a8c8eaabb3a95b9 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 11:37:25 -0200 Subject: [PATCH 40/91] Update my todo --- LiteDB-TODO.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index e0e259f86..43305d6b1 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -124,8 +124,9 @@ LiteEngine - Totalmente interna - Só faz o minimo de acesso a engine - Cuida das transacoes, locks?! A pensar - - Inclui a query - - A solução final pode não ficar tão eficiente quando que temos hoje, mas acho que há uma melhora na redução de bugs + - Inclui a QueryEngine + - A solução final pode não ficar tão eficiente quando que temos hoje, mas acho que há uma melhora na redução de bugs e complexidade + - Não pode ter estado (begintrans/commit) - a execução de um metodo deve sempre ser igual a outra LiteEngine(IDiskService) { // AllServices @@ -147,7 +148,8 @@ LiteEngine(IDiskService) { - Engine - QueryEngine - Mapper -- Outros: Linq, shell, fileStorage +- Utils + - Linq, shell, fileStorage, fts - API From 12b1c409617755d06338a2cdfed917e9f7329bde Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 14:31:00 -0200 Subject: [PATCH 41/91] Creating LiteEngine --- LiteDB-TODO.txt | 5 +- LiteDB/Core/Collections/Aggregate.cs | 54 ++---- LiteDB/Core/Collections/Delete.cs | 55 +----- LiteDB/Core/Collections/Drop.cs | 49 ----- LiteDB/Core/Collections/Find.cs | 16 +- LiteDB/Core/Collections/Include.cs | 7 +- LiteDB/Core/Collections/Index.cs | 173 +----------------- LiteDB/Core/Collections/Insert.cs | 98 +--------- LiteDB/Core/Collections/InsertBulk.cs | 39 ++-- LiteDB/Core/Collections/LiteCollection.cs | 68 +------ LiteDB/Core/Collections/Rename.cs | 46 ----- LiteDB/Core/Collections/Update.cs | 135 ++------------ LiteDB/Core/LiteDatabase.cs | 80 ++------ .../Disks/FileDiskService.cs | 0 .../Disks/IDiskService.cs | 0 LiteDB/{Utils => Engine}/DumpDatabase.cs | 0 LiteDB/Engine/LiteEngine.cs | 128 +++++++++++++ .../Pages/BasePage.cs | 0 .../Pages/CollectionPage.cs | 0 .../Pages/DataPage.cs | 0 .../Pages/ExtendPage.cs | 0 .../Pages/HeaderPage.cs | 0 .../Pages/IndexPage.cs | 0 .../Services/CacheService.cs | 0 .../Services/CollectionService.cs | 0 .../Services/DataService.cs | 0 .../Services/IndexService.cs | 0 .../Services/PageService.cs | 0 .../Services/TransactionService.cs | 0 .../Structures/CollectionIndex.cs | 0 .../Structures/DataBlock.cs | 0 .../Structures/IndexNode.cs | 0 .../Structures/IndexOptions.cs | 0 .../Structures/PageAddress.cs | 0 LiteDB/LiteDB.csproj | 43 ++--- LiteDB/Query/Impl/QueryAnd.cs | 6 +- LiteDB/Query/Impl/QueryOr.cs | 6 +- LiteDB/Query/Query.cs | 8 +- LiteDB/Shell/Commands/Collections/Drop.cs | 2 +- LiteDB/Shell/Commands/Collections/Exec.cs | 94 ---------- LiteDB/Shell/Commands/Others/Dump.cs | 22 +-- 41 files changed, 252 insertions(+), 882 deletions(-) delete mode 100644 LiteDB/Core/Collections/Drop.cs delete mode 100644 LiteDB/Core/Collections/Rename.cs rename LiteDB/{DataStructure => Engine}/Disks/FileDiskService.cs (100%) rename LiteDB/{DataStructure => Engine}/Disks/IDiskService.cs (100%) rename LiteDB/{Utils => Engine}/DumpDatabase.cs (100%) create mode 100644 LiteDB/Engine/LiteEngine.cs rename LiteDB/{DataStructure => Engine}/Pages/BasePage.cs (100%) rename LiteDB/{DataStructure => Engine}/Pages/CollectionPage.cs (100%) rename LiteDB/{DataStructure => Engine}/Pages/DataPage.cs (100%) rename LiteDB/{DataStructure => Engine}/Pages/ExtendPage.cs (100%) rename LiteDB/{DataStructure => Engine}/Pages/HeaderPage.cs (100%) rename LiteDB/{DataStructure => Engine}/Pages/IndexPage.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/CacheService.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/CollectionService.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/DataService.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/IndexService.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/PageService.cs (100%) rename LiteDB/{DataStructure => Engine}/Services/TransactionService.cs (100%) rename LiteDB/{DataStructure => Engine}/Structures/CollectionIndex.cs (100%) rename LiteDB/{DataStructure => Engine}/Structures/DataBlock.cs (100%) rename LiteDB/{DataStructure => Engine}/Structures/IndexNode.cs (100%) rename LiteDB/{DataStructure => Engine}/Structures/IndexOptions.cs (100%) rename LiteDB/{DataStructure => Engine}/Structures/PageAddress.cs (100%) delete mode 100644 LiteDB/Shell/Commands/Collections/Exec.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 43305d6b1..06afc1192 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,7 +1,6 @@ # LiteDB v.2 - TODO -- Revisar transação - Remover possibilidade - - A transacao deve ficar sempre na operacao mais externa +- Revisar help do Shell - saiu varias coisas - Thread Safe - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private - Somente os metodos externos devem ter LOCK @@ -134,7 +133,7 @@ LiteEngine(IDiskService) { Insert(colName, myDocInBson) : id Update(colName, myId, myDocInBson): bool Delete(colName, query) : int - Query(colName, query) : IEnumerable + Find(colName, query) : IEnumerable Min\Max\Count\Exists(colName, query) EnsureIndex(colName, fieldName) : bool DropIndex(colName, fieldName) : bool diff --git a/LiteDB/Core/Collections/Aggregate.cs b/LiteDB/Core/Collections/Aggregate.cs index 456439ffb..52285bd1a 100644 --- a/LiteDB/Core/Collections/Aggregate.cs +++ b/LiteDB/Core/Collections/Aggregate.cs @@ -16,13 +16,7 @@ public partial class LiteCollection /// public int Count() { - this.Database.Transaction.AvoidDirtyRead(); - - var col = this.GetCollectionPage(false); - - if (col == null) return 0; - - return Convert.ToInt32(col.DocumentCount); + return _engine.Count(_name, null); } /// @@ -32,11 +26,7 @@ public int Count(Query query) { if (query == null) throw new ArgumentNullException("query"); - this.Database.Transaction.AvoidDirtyRead(); - - var nodes = query.Run(this); - - return nodes.Count(); + return _engine.Count(_name, query); } /// @@ -44,6 +34,8 @@ public int Count(Query query) /// public int Count(Expression> predicate) { + if (predicate == null) throw new ArgumentNullException("predicate"); + return this.Count(_visitor.Visit(predicate)); } @@ -54,11 +46,7 @@ public bool Exists(Query query) { if (query == null) throw new ArgumentNullException("query"); - this.Database.Transaction.AvoidDirtyRead(); - - var nodes = query.Run(this); - - return nodes.FirstOrDefault() != null; + return _engine.Exists(_name, query); } /// @@ -66,6 +54,8 @@ public bool Exists(Query query) /// public bool Exists(Expression> predicate) { + if (predicate == null) throw new ArgumentNullException("predicate"); + return this.Exists(_visitor.Visit(predicate)); } @@ -80,18 +70,7 @@ public BsonValue Min(string field) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - this.Database.Transaction.AvoidDirtyRead(); - - var index = this.GetOrCreateIndex(field); - - if (index == null) return BsonValue.MinValue; - - var head = this.Database.Indexer.GetNode(index.HeadNode); - var next = this.Database.Indexer.GetNode(head.Next[0]); - - if (next.IsHeadTail(index)) return BsonValue.MinValue; - - return next.Key; + return _engine.Min(_name, field); } /// @@ -107,6 +86,8 @@ public BsonValue Min() /// public BsonValue Min(Expression> property) { + if (property == null) throw new ArgumentNullException("property"); + var field = _visitor.GetBsonField(property); return this.Min(field); @@ -119,18 +100,7 @@ public BsonValue Max(string field) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - this.Database.Transaction.AvoidDirtyRead(); - - var index = this.GetOrCreateIndex(field); - - if (index == null) return BsonValue.MaxValue; - - var tail = this.Database.Indexer.GetNode(index.TailNode); - var prev = this.Database.Indexer.GetNode(tail.Prev[0]); - - if (prev.IsHeadTail(index)) return BsonValue.MaxValue; - - return prev.Key; + return _engine.Max(_name, field); } /// @@ -146,6 +116,8 @@ public BsonValue Max() /// public BsonValue Max(Expression> property) { + if (property == null) throw new ArgumentNullException("property"); + var field = _visitor.GetBsonField(property); return this.Max(field); diff --git a/LiteDB/Core/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs index 564cba19e..af0d7eb7b 100644 --- a/LiteDB/Core/Collections/Delete.cs +++ b/LiteDB/Core/Collections/Delete.cs @@ -16,25 +16,7 @@ public int Delete(Query query) { if(query == null) throw new ArgumentNullException("query"); - lock(_locker) - { - // start transaction - this.Database.Transaction.Begin(); - - try - { - var count = this.DeleteDocuments(query); - - this.Database.Transaction.Commit(); - - return count; - } - catch (Exception ex) - { - this.Database.Transaction.Rollback(); - throw ex; - } - } + return _engine.DeleteDocuments(_name, query); } /// @@ -54,40 +36,5 @@ public bool Delete(BsonValue id) return this.Delete(Query.EQ("_id", id)) > 0; } - - /// - /// Internal implementation to delete a document - no trans, no locks - /// - internal int DeleteDocuments(Query query) - { - var col = this.GetCollectionPage(false); - - // no collection, no document - abort trans - if (col == null) return 0; - - var count = 0; - - // find nodes - var nodes = query.Run(this); - - foreach (var node in nodes) - { - // read dataBlock - var dataBlock = this.Database.Data.Read(node.DataBlock, false); - - // lets remove all indexes that point to this in dataBlock - foreach (var index in col.GetIndexes(true)) - { - this.Database.Indexer.Delete(index, dataBlock.IndexRef[index.Slot]); - } - - // remove object data - this.Database.Data.Delete(col, node.DataBlock); - - count++; - } - - return count; - } } } diff --git a/LiteDB/Core/Collections/Drop.cs b/LiteDB/Core/Collections/Drop.cs deleted file mode 100644 index 08e64204a..000000000 --- a/LiteDB/Core/Collections/Drop.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteCollection - { - /// - /// Drop a collection deleting all documents and indexes - /// - public bool Drop() - { - lock(_locker) - { - // start transaction - this.Database.Transaction.Begin(); - - try - { - var col = this.GetCollectionPage(false); - - // if collection not exists, no drop - if (col == null) - { - this.Database.Transaction.Commit(); - return false; - } - - // delete all data pages + indexes pages - this.Database.Collections.Drop(col); - - _pageID = uint.MaxValue; - - this.Database.Transaction.Commit(); - - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - } - } -} diff --git a/LiteDB/Core/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs index e6c241747..10b9c5a72 100644 --- a/LiteDB/Core/Collections/Find.cs +++ b/LiteDB/Core/Collections/Find.cs @@ -18,20 +18,10 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) { if (query == null) throw new ArgumentNullException("query"); - this.Database.Transaction.AvoidDirtyRead(); + var docs = _engine.Find(_name, query, skip, limit); - var nodes = query.Run(this); - - if (skip > 0) nodes = nodes.Skip(skip); - - if (limit != int.MaxValue) nodes = nodes.Take(limit); - - foreach (var node in nodes) + foreach (var doc in docs) { - var dataBlock = this.Database.Data.Read(node.DataBlock, true); - - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - // executing all includes in BsonDocument foreach (var action in _includes) { @@ -39,7 +29,7 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) } // get object from BsonDocument - var obj = this.Database.Mapper.ToObject(doc); + var obj = _mapper.ToObject(doc); yield return obj; } diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index f4b07a46f..5788ab1c4 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -34,7 +34,7 @@ public LiteCollection Include(Expression> dbref) if(array.Count == 0) return; // all doc refs in an array must be same collection, lets take first only - var col = this.Database.GetCollection(array[0].AsDocument["$ref"]); + var col = new LiteCollection(array[0].AsDocument["$ref"], _engine, _mapper); for(var i = 0; i < array.Count; i++) { @@ -46,16 +46,15 @@ public LiteCollection Include(Expression> dbref) { // for BsonDocument, get property value e update with full object refence var doc = prop.AsDocument; - var col = this.Database.GetCollection(doc["$ref"]); + var col = new LiteCollection(doc["$ref"], _engine, _mapper); var obj = col.FindById(doc["$id"]); bson.Set(propPath, obj); } }; // cloning this collection and adding this include - var newcol = new LiteCollection(this.Database, this.Name); + var newcol = new LiteCollection(_name, _engine, _mapper); - newcol._pageID = _pageID; newcol._includes.AddRange(_includes); newcol._includes.Add(action); diff --git a/LiteDB/Core/Collections/Index.cs b/LiteDB/Core/Collections/Index.cs index bb5f3bc8b..ec06bdda5 100644 --- a/LiteDB/Core/Collections/Index.cs +++ b/LiteDB/Core/Collections/Index.cs @@ -24,45 +24,7 @@ public bool EnsureIndex(string field, IndexOptions options) if (!CollectionIndex.IndexPattern.IsMatch(field)) throw LiteException.InvalidFormat("IndexField", field); - lock (_locker) - { - // start transaction - this.Database.Transaction.Begin(); - - try - { - // do not create collection at this point - var col = this.GetCollectionPage(false); - - // check if index already exists (if collection exists) - if (col != null && col.GetIndex(field) != null) - { - this.Database.Transaction.Commit(); - return false; - } - - // if not exists collection yet, create a new now - if (col == null) - { - col = this.Database.Collections.Add(this.Name); - _pageID = col.PageID; - } - - // create index head - var index = this.Database.Indexer.CreateIndex(col); - - this.EnsureIndex(col, field, options); - - this.Database.Transaction.Commit(); - - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } + return _engine.EnsureIndex(_name, field, options); } /// @@ -104,91 +66,7 @@ public bool EnsureIndex(Expression> property, IndexOptions options /// public IEnumerable GetIndexes() { - this.Database.Transaction.AvoidDirtyRead(); - - var col = this.GetCollectionPage(false); - - if (col == null) yield break; - - foreach(var index in col.GetIndexes(true)) - { - yield return new BsonDocument() - .Add("slot", index.Slot) - .Add("field", index.Field) - .Add("options", this.Database.Mapper.Serialize(index.Options)); - } - } - - /// - /// Get an index or create a new one if not exists - if no collection, no need create an index - no lock, no trans - /// - internal CollectionIndex GetOrCreateIndex(string field) - { - // get collection page that contains all indexes - var col = this.GetCollectionPage(false); - - // no collection, no index - if(col == null) return null; - - // get index - var index = col.GetIndex(field); - - // if index not found, lets check if type T has [BsonIndex] with custom options - if (index == null && typeof(T) != typeof(BsonDocument)) - { - var options = this.Database.Mapper.GetIndexFromMapper(field); - - // create a new index using BsonIndex options - if (options != null) - { - index = this.EnsureIndex(col, field, options); - } - } - - // if no index, let's auto create an index with default index options - if (index == null) - { - index = this.EnsureIndex(col, field, new IndexOptions()); - } - - return index; - } - - /// - /// Internal implementation of create an index - no locks, no trans - /// - private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptions options) - { - // create index head - var index = this.Database.Indexer.CreateIndex(col); - - index.Field = field; - index.Options = options; - - // read all objects (read from PK index) - foreach (var node in new QueryAll("_id", Query.Ascending).Run(this)) - { - var dataBlock = this.Database.Data.Read(node.DataBlock, true); - - // read object - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - - // adding index - var key = doc.Get(field); - - var newNode = this.Database.Indexer.AddNode(index, key); - - // adding this new index Node to indexRef - dataBlock.IndexRef[index.Slot] = newNode.Position; - - // link index node to datablock - newNode.DataBlock = dataBlock.Position; - - // mark datablock page as dirty - this.Database.Pager.SetDirty(dataBlock.Page); - } - - return index; + return _engine.GetIndexes(_name); } /// @@ -197,53 +75,8 @@ private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptio public bool DropIndex(string field) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - if (field == "_id") throw LiteException.IndexDropId(); - - var col = this.GetCollectionPage(false); - - // no collection, no index - if(col == null) return false; - - // search for index reference - var index = col.GetIndex(field); - - // no index, no drop - if (index == null) return false; - - lock (_locker) - { - // start transaction - this.Database.Transaction.Begin(); - - try - { - this.DropIndex(col, index); - - this.Database.Transaction.Commit(); - - return true; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - } - - /// - /// Internal implementation of drop an index - no lock, no trans - /// - private void DropIndex(CollectionPage col, CollectionIndex index) - { - // delete all data pages + indexes pages - this.Database.Indexer.DropIndex(index); - - // clear index reference - index.Clear(); - // save collection page - this.Database.Pager.SetDirty(col); + return _engine.DropIndex(_name, field); } } } diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 21470ebaa..0eabad7d4 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -15,24 +15,11 @@ public BsonValue Insert(T document) { if (document == null) throw new ArgumentNullException("document"); - lock (_locker) - { - this.Database.Transaction.Begin(); - - try - { - var id = this.InsertDocument(document); + _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper)); - this.Database.Transaction.Commit(); + var doc = _mapper.ToDocument(document); - return id; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } + return _engine.InsertDocument(_name, doc); } /// @@ -44,84 +31,13 @@ public int Insert(IEnumerable docs) var count = 0; - lock (_locker) - { - this.Database.Transaction.Begin(); - - try - { - foreach (var doc in docs) - { - this.InsertDocument(doc); - count++; - } - - this.Database.Transaction.Commit(); - - return count; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - } - - /// - /// Internal implementation of Insert a document. Returns document _id - no trans, no locks - /// - private BsonValue InsertDocument(T document) - { - // set an id value if document object needs - this.Database.Mapper.SetAutoId(document, this.GetBsonCollection()); - - var doc = this.Database.Mapper.ToDocument(document); - - BsonValue id; - - // add ObjectId to _id if _id not found - if (!doc.RawValue.TryGetValue("_id", out id)) - { - id = doc["_id"] = ObjectId.NewObjectId(); - } - - // test if _id is a valid type - if (id.IsNull || id.IsMinValue || id.IsMaxValue) + foreach (var doc in docs) { - throw LiteException.InvalidDataType("_id", id); - } - - // serialize object - var bytes = BsonSerializer.Serialize(doc); - - var col = this.GetCollectionPage(true); - - // storage in data pages - returns dataBlock address - var dataBlock = this.Database.Data.Insert(col, bytes); - - // store id in a PK index [0 array] - var pk = this.Database.Indexer.AddNode(col.PK, id); - - // do links between index <-> data block - pk.DataBlock = dataBlock.Position; - dataBlock.IndexRef[0] = pk.Position; - - // for each index, insert new IndexNode - foreach (var index in col.GetIndexes(false)) - { - var key = doc.Get(index.Field); - - var node = this.Database.Indexer.AddNode(index, key); - - // point my index to data object - node.DataBlock = dataBlock.Position; - - // point my dataBlock - dataBlock.IndexRef[index.Slot] = node.Position; + this.Insert(doc); + count++; } - return id; + return count++; } } } diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs index 0188b9076..2bec94803 100644 --- a/LiteDB/Core/Collections/InsertBulk.cs +++ b/LiteDB/Core/Collections/InsertBulk.cs @@ -19,37 +19,22 @@ public int InsertBulk(IEnumerable docs, int buffer = 32768) var enumerator = docs.GetEnumerator(); var count = 0; - lock(_locker) + while (true) { - while (true) - { - var buff = buffer; - - this.Database.Transaction.Begin(); - - try - { - var more = true; + var buff = buffer; - while (buff > 0 && (more = enumerator.MoveNext())) - { - this.InsertDocument(enumerator.Current); - buff--; - count++; - } + var more = true; - this.Database.Transaction.Commit(); + while (buff > 0 && (more = enumerator.MoveNext())) + { + this.Insert(enumerator.Current); + buff--; + count++; + } - if (more == false) - { - return count; - } - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } + if (more == false) + { + return count; } } } diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 12af3a116..6e80fef9a 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -9,74 +9,24 @@ namespace LiteDB public sealed partial class LiteCollection where T : new() { - private uint _pageID; + private LiteEngine _engine; + private string _name; + private BsonMapper _mapper; private List> _includes; private QueryVisitor _visitor; - // Use a locker object (LiteDatabase) to thread safe - private object _locker; - /// /// Get collection name /// - public string Name { get; private set; } + public string Name { get { return _name; } } - /// - /// Gets database object reference - /// - public LiteDatabase Database { get; private set; } - - internal LiteCollection(LiteDatabase db, string name) + internal LiteCollection(string name, LiteEngine engine, BsonMapper mapper) { - this.Name = name; - this.Database = db; - _pageID = uint.MaxValue; - _visitor = new QueryVisitor(db.Mapper); + _name = name; + _engine = engine; + _mapper = mapper; + _visitor = new QueryVisitor(mapper); _includes = new List>(); - _locker = db; - } - - /// - /// Get the collection page only when nedded. Gets from cache always to garantee that wil be the last (in case of _clearCache will get a new one) - /// - internal CollectionPage GetCollectionPage(bool addIfNotExits) - { - // _pageID never change, even if data file was changed - if (_pageID == uint.MaxValue) - { - var col = this.Database.Collections.Get(this.Name); - - if (col == null) - { - // create a new collection only if - if (addIfNotExits) - { - col = this.Database.Collections.Add(this.Name); - } - else - { - return null; - } - } - - _pageID = col.PageID; - - return col; - } - - return this.Database.Pager.GetPage(_pageID); - } - - /// - /// Returns a new instance of this collection but using BsonDocument insted T - Copy _pageID to avoid new collection page search - /// - internal LiteCollection GetBsonCollection() - { - var col = new LiteCollection(this.Database, this.Name); - - col._pageID = _pageID; - - return col; } } } diff --git a/LiteDB/Core/Collections/Rename.cs b/LiteDB/Core/Collections/Rename.cs deleted file mode 100644 index 7ec784625..000000000 --- a/LiteDB/Core/Collections/Rename.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteCollection - { - /// - /// Drop a collection deleting all documents and indexes - /// - public bool Rename(string newName) - { - this.Database.Transaction.Begin(); - - try - { - // get collection page - var col = this.GetCollectionPage(false); - - if (col == null) - { - this.Database.Transaction.Commit(); - return false; - } - - // change collection name - col.CollectionName = newName; - - // set page as dirty - this.Database.Pager.SetDirty(col); - - this.Database.Transaction.Commit(); - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - - return true; - } - } -} diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index 0342419e8..12dff2310 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -17,30 +17,13 @@ public bool Update(T document) if (document == null) throw new ArgumentNullException("document"); // get BsonDocument from object - var doc = this.Database.Mapper.ToDocument(document); + var doc = _mapper.ToDocument(document); var id = doc["_id"]; if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - lock(_locker) - { - this.Database.Transaction.Begin(); - - try - { - var result = this.UpdateDocument(id, doc); - - this.Database.Transaction.Commit(); - - return result; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } + return _engine.UpdateDocument(_name, id, doc); } /// @@ -52,26 +35,9 @@ public bool Update(BsonValue id, T document) if (id == null || id.IsNull) throw new ArgumentNullException("id"); // get BsonDocument from object - var doc = this.Database.Mapper.ToDocument(document); + var doc = _mapper.ToDocument(document); - lock (_locker) - { - this.Database.Transaction.Begin(); - - try - { - var result = this.UpdateDocument(id, doc); - - this.Database.Transaction.Commit(); - - return result; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } + return _engine.UpdateDocument(_name, id, doc); } /// @@ -82,39 +48,24 @@ public int Update(Query query, Action action) if (query == null) throw new ArgumentNullException("query"); if (action == null) throw new ArgumentNullException("action"); - lock(_locker) - { - this.Database.Transaction.Begin(); - - try - { - var docs = this.Find(query).ToArray(); // used to avoid changes during Action - var count = 0; - - foreach (var doc in docs) - { - action(doc); + var docs = this.Find(query).ToArray(); // used to avoid changes during Action + var count = 0; - // get BsonDocument from object - var bson = this.Database.Mapper.ToDocument(doc); - - var id = bson["_id"]; + foreach (var doc in docs) + { + action(doc); - if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); + // get BsonDocument from object + var bson = _mapper.ToDocument(doc); - count += this.UpdateDocument(id, bson) ? 1 : 0; - } + var id = bson["_id"]; - this.Database.Transaction.Commit(); + if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - return count; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } + count += _engine.UpdateDocument(_name, id, bson) ? 1 : 0; } + + return count; } /// @@ -124,59 +75,5 @@ public void Update(Expression> predicate, Action action) { this.Update(_visitor.Visit(predicate), action); } - - /// - /// Internal implementation of Update a document (no lock, no transaction) - /// - private bool UpdateDocument(BsonValue id, BsonDocument doc) - { - // serialize object - var bytes = BsonSerializer.Serialize(doc); - - var col = this.GetCollectionPage(false); - - // if no collection, no updates - if (col == null) return false; - - // normalize id before find - var value = id.Normalize(col.PK.Options); - - // find indexNode from pk index - var indexNode = this.Database.Indexer.Find(col.PK, value, false, Query.Ascending); - - // if not found document, no updates - if (indexNode == null) return false; - - // update data storage - var dataBlock = this.Database.Data.Update(col, indexNode.DataBlock, bytes); - - // delete/insert indexes - do not touch on PK - foreach (var index in col.GetIndexes(false)) - { - var key = doc.Get(index.Field); - - var node = this.Database.Indexer.GetNode(dataBlock.IndexRef[index.Slot]); - - // check if my index node was changed - if (node.Key.CompareTo(key) != 0) - { - // remove old index node - this.Database.Indexer.Delete(index, node.Position); - - // and add a new one - var newNode = this.Database.Indexer.AddNode(index, key); - - // point my index to data object - newNode.DataBlock = dataBlock.Position; - - // point my dataBlock - dataBlock.IndexRef[index.Slot] = newNode.Position; - - this.Database.Pager.SetDirty(dataBlock.Page); - } - } - - return true; - } } } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index a1f83d4c4..53324d3f3 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -12,34 +12,16 @@ namespace LiteDB /// public partial class LiteDatabase : IDisposable { - #region Properties + Ctor + private LiteEngine _engine; - internal CacheService Cache { get; private set; } + private BsonMapper _mapper; - internal IDiskService Disk { get; private set; } - - internal PageService Pager { get; private set; } - - internal TransactionService Transaction { get; private set; } - - internal IndexService Indexer { get; private set; } - - internal DataService Data { get; private set; } - - internal CollectionService Collections { get; private set; } - - public BsonMapper Mapper { get; private set; } - - private LiteDatabase() - { - this.Mapper = BsonMapper.Global; - } + public BsonMapper Mapper { get { return _mapper; } } /// /// Starts LiteDB database using a connectionString /// public LiteDatabase(string connectionString) - : this() { var str = new ConnectionString(connectionString); @@ -51,9 +33,9 @@ public LiteDatabase(string connectionString) if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); - this.Disk = new FileDiskService(filename, journal, timeout, readOnly, password); - - this.Initialize(); + // initialize engine creating a new FileDiskService for data access + _engine = new LiteEngine(new FileDiskService(filename, journal, timeout, readOnly, password)); + _mapper = BsonMapper.Global; } /// @@ -61,37 +43,10 @@ public LiteDatabase(string connectionString) /// public LiteDatabase(IDiskService diskService, BsonMapper mapper) { - this.Disk = diskService; - this.Mapper = mapper; - } - - /// - /// Initialize database engine - starts all services and open datafile - /// - private void Initialize() - { - var isNew = this.Disk.Initialize(); - - if(isNew) - { - this.Disk.WritePage(0, new HeaderPage().WritePage()); - } - - this.Cache = new CacheService(); - - this.Pager = new PageService(this.Disk, this.Cache); - - this.Indexer = new IndexService(this.Pager); - - this.Data = new DataService(this.Pager); - - this.Collections = new CollectionService(this.Pager, this.Indexer, this.Data); - - this.Transaction = new TransactionService(this.Disk, this.Cache); + _engine = new LiteEngine(diskService); + _mapper = mapper; } - #endregion - #region Collections /// @@ -101,7 +56,7 @@ private void Initialize() public LiteCollection GetCollection(string name) where T : new() { - return new LiteCollection(this, name); + return new LiteCollection(name, _engine, _mapper); } /// @@ -110,7 +65,7 @@ public LiteCollection GetCollection(string name) /// Collection name (case insensitive) public LiteCollection GetCollection(string name) { - return new LiteCollection(this, name); + return new LiteCollection(name, _engine, _mapper); } /// @@ -118,9 +73,7 @@ public LiteCollection GetCollection(string name) /// public IEnumerable GetCollectionNames() { - this.Transaction.AvoidDirtyRead(); - - return this.Collections.GetAll().Select(x => x.CollectionName); + return _engine.GetCollectionNames(); } /// @@ -128,9 +81,7 @@ public IEnumerable GetCollectionNames() /// public bool CollectionExists(string name) { - this.Transaction.AvoidDirtyRead(); - - return this.Collections.Get(name) != null; + return _engine.GetCollectionNames().Contains(name); } /// @@ -138,7 +89,7 @@ public bool CollectionExists(string name) /// public bool DropCollection(string name) { - return this.GetCollection(name).Drop(); + return _engine.DropCollection(name); } /// @@ -146,7 +97,7 @@ public bool DropCollection(string name) /// public bool RenameCollection(string oldName, string newName) { - return this.GetCollection(oldName).Rename(newName); + return _engine.RenameCollection(oldName, newName); } #endregion @@ -185,8 +136,7 @@ public BsonValue RunCommand(string command) public void Dispose() { - this.Disk.Dispose(); - this.Cache.Dispose(); + _engine.Dispose(); } } } diff --git a/LiteDB/DataStructure/Disks/FileDiskService.cs b/LiteDB/Engine/Disks/FileDiskService.cs similarity index 100% rename from LiteDB/DataStructure/Disks/FileDiskService.cs rename to LiteDB/Engine/Disks/FileDiskService.cs diff --git a/LiteDB/DataStructure/Disks/IDiskService.cs b/LiteDB/Engine/Disks/IDiskService.cs similarity index 100% rename from LiteDB/DataStructure/Disks/IDiskService.cs rename to LiteDB/Engine/Disks/IDiskService.cs diff --git a/LiteDB/Utils/DumpDatabase.cs b/LiteDB/Engine/DumpDatabase.cs similarity index 100% rename from LiteDB/Utils/DumpDatabase.cs rename to LiteDB/Engine/DumpDatabase.cs diff --git a/LiteDB/Engine/LiteEngine.cs b/LiteDB/Engine/LiteEngine.cs new file mode 100644 index 000000000..3d7908eaf --- /dev/null +++ b/LiteDB/Engine/LiteEngine.cs @@ -0,0 +1,128 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + /// + /// A internal class that take care of all engine data structure access - it´s basic implementation of a NoSql database + /// Its isolated from complete solution - works on low level only + /// + internal partial class LiteEngine : IDisposable + { + #region Services instances + + private CacheService _cache; + + private IDiskService _disk; + + private PageService _pager; + + private TransactionService _transaction; + + private IndexService _indexer; + + private DataService _data; + + private CollectionService _collections; + + public LiteEngine(IDiskService disk) + { + _disk = disk; + + var isNew = _disk.Initialize(); + + if(isNew) + { + _disk.WritePage(0, new HeaderPage().WritePage()); + } + + _cache = new CacheService(); + _pager = new PageService(_disk, _cache); + _indexer = new IndexService(_pager); + _data = new DataService(_pager); + _collections = new CollectionService(_pager, _indexer, _data); + _transaction = new TransactionService(_disk, _cache); + } + + #endregion + + public BsonValue InsertDocument(string col, BsonDocument doc) + { + return null; + } + + public bool UpdateDocument(string col, BsonValue id, BsonDocument doc) + { + return true; + } + + public int DeleteDocuments(string col, Query query) + { + return 0; + } + + public IEnumerable Find(string col, Query query, int skip = 0, int limit = int.MaxValue) + { + return null; + } + + public BsonValue Min(string colName, string field) + { + return true; + } + + public BsonValue Max(string colName, string field) + { + return true; + } + + public int Count(string colName, Query query) + { + return 0; + } + + public bool Exists(string colName, Query query) + { + return true; + } + + public bool EnsureIndex(string col, string field, IndexOptions options) + { + return true; + } + + public IEnumerable GetIndexes(string colName) + { + return null; + } + + public bool DropIndex(string colName, string field) + { + return true; + } + + public IEnumerable GetCollectionNames() + { + return null; + } + + public bool DropCollection(string colName) + { + return true; + } + + public bool RenameCollection(string colName, string newName) + { + return true; + } + + public void Dispose() + { + _disk.Dispose(); + } + } +} diff --git a/LiteDB/DataStructure/Pages/BasePage.cs b/LiteDB/Engine/Pages/BasePage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/BasePage.cs rename to LiteDB/Engine/Pages/BasePage.cs diff --git a/LiteDB/DataStructure/Pages/CollectionPage.cs b/LiteDB/Engine/Pages/CollectionPage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/CollectionPage.cs rename to LiteDB/Engine/Pages/CollectionPage.cs diff --git a/LiteDB/DataStructure/Pages/DataPage.cs b/LiteDB/Engine/Pages/DataPage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/DataPage.cs rename to LiteDB/Engine/Pages/DataPage.cs diff --git a/LiteDB/DataStructure/Pages/ExtendPage.cs b/LiteDB/Engine/Pages/ExtendPage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/ExtendPage.cs rename to LiteDB/Engine/Pages/ExtendPage.cs diff --git a/LiteDB/DataStructure/Pages/HeaderPage.cs b/LiteDB/Engine/Pages/HeaderPage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/HeaderPage.cs rename to LiteDB/Engine/Pages/HeaderPage.cs diff --git a/LiteDB/DataStructure/Pages/IndexPage.cs b/LiteDB/Engine/Pages/IndexPage.cs similarity index 100% rename from LiteDB/DataStructure/Pages/IndexPage.cs rename to LiteDB/Engine/Pages/IndexPage.cs diff --git a/LiteDB/DataStructure/Services/CacheService.cs b/LiteDB/Engine/Services/CacheService.cs similarity index 100% rename from LiteDB/DataStructure/Services/CacheService.cs rename to LiteDB/Engine/Services/CacheService.cs diff --git a/LiteDB/DataStructure/Services/CollectionService.cs b/LiteDB/Engine/Services/CollectionService.cs similarity index 100% rename from LiteDB/DataStructure/Services/CollectionService.cs rename to LiteDB/Engine/Services/CollectionService.cs diff --git a/LiteDB/DataStructure/Services/DataService.cs b/LiteDB/Engine/Services/DataService.cs similarity index 100% rename from LiteDB/DataStructure/Services/DataService.cs rename to LiteDB/Engine/Services/DataService.cs diff --git a/LiteDB/DataStructure/Services/IndexService.cs b/LiteDB/Engine/Services/IndexService.cs similarity index 100% rename from LiteDB/DataStructure/Services/IndexService.cs rename to LiteDB/Engine/Services/IndexService.cs diff --git a/LiteDB/DataStructure/Services/PageService.cs b/LiteDB/Engine/Services/PageService.cs similarity index 100% rename from LiteDB/DataStructure/Services/PageService.cs rename to LiteDB/Engine/Services/PageService.cs diff --git a/LiteDB/DataStructure/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs similarity index 100% rename from LiteDB/DataStructure/Services/TransactionService.cs rename to LiteDB/Engine/Services/TransactionService.cs diff --git a/LiteDB/DataStructure/Structures/CollectionIndex.cs b/LiteDB/Engine/Structures/CollectionIndex.cs similarity index 100% rename from LiteDB/DataStructure/Structures/CollectionIndex.cs rename to LiteDB/Engine/Structures/CollectionIndex.cs diff --git a/LiteDB/DataStructure/Structures/DataBlock.cs b/LiteDB/Engine/Structures/DataBlock.cs similarity index 100% rename from LiteDB/DataStructure/Structures/DataBlock.cs rename to LiteDB/Engine/Structures/DataBlock.cs diff --git a/LiteDB/DataStructure/Structures/IndexNode.cs b/LiteDB/Engine/Structures/IndexNode.cs similarity index 100% rename from LiteDB/DataStructure/Structures/IndexNode.cs rename to LiteDB/Engine/Structures/IndexNode.cs diff --git a/LiteDB/DataStructure/Structures/IndexOptions.cs b/LiteDB/Engine/Structures/IndexOptions.cs similarity index 100% rename from LiteDB/DataStructure/Structures/IndexOptions.cs rename to LiteDB/Engine/Structures/IndexOptions.cs diff --git a/LiteDB/DataStructure/Structures/PageAddress.cs b/LiteDB/Engine/Structures/PageAddress.cs similarity index 100% rename from LiteDB/DataStructure/Structures/PageAddress.cs rename to LiteDB/Engine/Structures/PageAddress.cs diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 97698c453..528b3debe 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -51,14 +51,13 @@ - - + + - @@ -111,7 +110,6 @@ - @@ -126,33 +124,32 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - diff --git a/LiteDB/Query/Impl/QueryAnd.cs b/LiteDB/Query/Impl/QueryAnd.cs index 86bff697f..bb360447d 100644 --- a/LiteDB/Query/Impl/QueryAnd.cs +++ b/LiteDB/Query/Impl/QueryAnd.cs @@ -23,10 +23,10 @@ internal override IEnumerable ExecuteIndex(IndexService indexer, Coll throw new NotSupportedException(); } - internal override IEnumerable Run(LiteCollection collection) + internal override IEnumerable Run(IndexService indexer, CollectionIndex index) { - var left = _left.Run(collection); - var right = _right.Run(collection); + var left = _left.Run(indexer, index); + var right = _right.Run(indexer, index); return left.Intersect(right, new IndexNodeComparer()); } diff --git a/LiteDB/Query/Impl/QueryOr.cs b/LiteDB/Query/Impl/QueryOr.cs index 000c3f362..682a5067c 100644 --- a/LiteDB/Query/Impl/QueryOr.cs +++ b/LiteDB/Query/Impl/QueryOr.cs @@ -23,10 +23,10 @@ internal override IEnumerable ExecuteIndex(IndexService indexer, Coll throw new NotSupportedException(); } - internal override IEnumerable Run(LiteCollection collection) + internal override IEnumerable Run(IndexService indexer, CollectionIndex index) { - var left = _left.Run(collection); - var right = _right.Run(collection); + var left = _left.Run(indexer, index); + var right = _right.Run(indexer, index); return left.Union(right, new IndexNodeComparer()); } diff --git a/LiteDB/Query/Query.cs b/LiteDB/Query/Query.cs index ef82960db..afd5801fb 100644 --- a/LiteDB/Query/Query.cs +++ b/LiteDB/Query/Query.cs @@ -171,17 +171,13 @@ public static Query Or(Query left, Query right) /// /// Find witch index will be used and run Execute method /// - internal virtual IEnumerable Run(LiteCollection collection) - where T : new() + internal virtual IEnumerable Run(IndexService indexer, CollectionIndex index) { - // get or create new index for Field - var index = collection.GetOrCreateIndex(this.Field); - // no collection just returns an empty list of indexnode if (index == null) return new List(); // execute query to get all IndexNodes - return this.ExecuteIndex(collection.Database.Indexer, index); + return this.ExecuteIndex(indexer, index); } #endregion diff --git a/LiteDB/Shell/Commands/Collections/Drop.cs b/LiteDB/Shell/Commands/Collections/Drop.cs index 21d145513..2024bed2c 100644 --- a/LiteDB/Shell/Commands/Collections/Drop.cs +++ b/LiteDB/Shell/Commands/Collections/Drop.cs @@ -17,7 +17,7 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) { var col = this.ReadCollection(db, s); - return col.Drop(); + return db.DropCollection(col.Name); } } } diff --git a/LiteDB/Shell/Commands/Collections/Exec.cs b/LiteDB/Shell/Commands/Collections/Exec.cs deleted file mode 100644 index f4aa49be4..000000000 --- a/LiteDB/Shell/Commands/Collections/Exec.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Microsoft.CSharp; -using System; -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; - -namespace LiteDB.Shell.Commands -{ - internal class CollectionExec : BaseCollection, ILiteCommand - { - public bool IsCommand(StringScanner s) - { - return this.IsCollectionCommand(s, "exec"); - } - - public BsonValue Execute(LiteDatabase db, StringScanner s) - { - var col = this.ReadCollection(db, s); - var query = s.Match("{") ? Query.All() : this.ReadQuery(s); - var code = DynamicCode.GetCode(s); - - var docs = col.Find(query).ToArray(); - - try - { - db.Transaction.Begin(); - - foreach (var doc in docs) - { - code(doc["_id"], doc, col, db); - } - - db.Transaction.Commit(); - - return docs.Length; - } - catch (Exception ex) - { - db.Transaction.Rollback(); - throw ex; - } - } - } - - internal class DynamicCode - { - private const string CODE_TEMPLATE = @" -using System; -using LiteDB; - -public class Program { - public static void DoWork( - BsonValue id, - BsonDocument doc, - LiteCollection col, - LiteDatabase db) { [code] } -}"; - - public static Action, LiteDatabase> GetCode(StringScanner s) - { - var str = s.Scan(@"[\s\S]*"); - var code = CODE_TEMPLATE.Replace("[code]", str); - var provider = new CSharpCodeProvider(); - var parameters = new CompilerParameters(); - - parameters.ReferencedAssemblies.Add("LiteDB.dll"); - parameters.GenerateInMemory = true; - parameters.GenerateExecutable = false; - - var results = provider.CompileAssemblyFromSource(parameters, code); - - if (results.Errors.HasErrors) - { - var err = new StringBuilder(); - foreach (CompilerError error in results.Errors) - { - err.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)); - } - throw new InvalidOperationException(err.ToString().Trim()); - } - - var assembly = results.CompiledAssembly; - var program = assembly.GetType("Program"); - var method = program.GetMethod("DoWork"); - - return new Action, LiteDatabase>((id, doc, col, db) => - { - method.Invoke(null, new object[] { id, doc, col, db }); - }); - } - } -} diff --git a/LiteDB/Shell/Commands/Others/Dump.cs b/LiteDB/Shell/Commands/Others/Dump.cs index ab71fcef1..23cb48aee 100644 --- a/LiteDB/Shell/Commands/Others/Dump.cs +++ b/LiteDB/Shell/Commands/Others/Dump.cs @@ -17,19 +17,19 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) { var result = new StringBuilder(); - if (s.HasTerminated || s.Match("mem$")) - { - var mem = s.Match("mem$"); + //if (s.HasTerminated || s.Match("mem$")) + //{ + // var mem = s.Match("mem$"); - result = DumpDatabase.Pages(db, mem); - } - else - { - var col = s.Scan(@"[\w-]+"); - var field = s.Scan(@"\s+\w+").Trim(); + // result = DumpDatabase.Pages(db, mem); + //} + //else + //{ + // var col = s.Scan(@"[\w-]+"); + // var field = s.Scan(@"\s+\w+").Trim(); - result = DumpDatabase.Index(db, col, field); - } + // result = DumpDatabase.Index(db, col, field); + //} return result.ToString(); } From 19491cdc9002c0fd9c1d034f50cc2c3108212af6 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 17:10:46 -0200 Subject: [PATCH 42/91] Implement basic database layer --- LiteDB-TODO.txt | 1 + LiteDB.Tests/BigFileTest.cs | 2 +- LiteDB/Core/Collections/Insert.cs | 25 ++- LiteDB/Core/Collections/InsertBulk.cs | 42 ----- LiteDB/Core/Collections/Update.cs | 18 +-- LiteDB/Engine/Engine/Aggregate.cs | 32 ++++ LiteDB/Engine/Engine/Collection.cs | 27 ++++ LiteDB/Engine/Engine/Delete.cs | 64 ++++++++ LiteDB/Engine/Engine/Find.cs | 45 ++++++ LiteDB/Engine/Engine/Index.cs | 152 +++++++++++++++++++ LiteDB/Engine/Engine/Insert.cs | 84 ++++++++++ LiteDB/Engine/Engine/Update.cs | 89 +++++++++++ LiteDB/Engine/LiteEngine.cs | 89 ++++------- LiteDB/Engine/Services/TransactionService.cs | 7 +- LiteDB/LiteDB.csproj | 8 +- LiteDB/Query/Impl/QueryAnd.cs | 6 +- LiteDB/Query/Impl/QueryOr.cs | 6 +- LiteDB/Query/Query.cs | 9 +- LiteDB/Shell/Commands/Collections/Bulk.cs | 2 +- LiteDB/Utils/LiteException.cs | 4 +- 20 files changed, 571 insertions(+), 141 deletions(-) delete mode 100644 LiteDB/Core/Collections/InsertBulk.cs create mode 100644 LiteDB/Engine/Engine/Aggregate.cs create mode 100644 LiteDB/Engine/Engine/Collection.cs create mode 100644 LiteDB/Engine/Engine/Delete.cs create mode 100644 LiteDB/Engine/Engine/Find.cs create mode 100644 LiteDB/Engine/Engine/Index.cs create mode 100644 LiteDB/Engine/Engine/Insert.cs create mode 100644 LiteDB/Engine/Engine/Update.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 06afc1192..d7b236cff 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,6 +1,7 @@ # LiteDB v.2 - TODO - Revisar help do Shell - saiu varias coisas +- Mapper de indice está fora do ar - Thread Safe - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private - Somente os metodos externos devem ter LOCK diff --git a/LiteDB.Tests/BigFileTest.cs b/LiteDB.Tests/BigFileTest.cs index 6a7dd83d4..50c426ca2 100644 --- a/LiteDB.Tests/BigFileTest.cs +++ b/LiteDB.Tests/BigFileTest.cs @@ -25,7 +25,7 @@ public void BigFile_Test() { var col = db.GetCollection("col1"); - col.InsertBulk(GetDocs()); + col.Insert(GetDocs()); } } } diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 0eabad7d4..a105815b4 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -19,7 +19,9 @@ public BsonValue Insert(T document) var doc = _mapper.ToDocument(document); - return _engine.InsertDocument(_name, doc); + _engine.InsertDocuments(_name, new BsonDocument[] { doc }); + + return doc["_id"]; } /// @@ -29,15 +31,24 @@ public int Insert(IEnumerable docs) { if (docs == null) throw new ArgumentNullException("docs"); - var count = 0; + return _engine.InsertDocuments(_name, this.GetBsonDocs(docs)); + } - foreach (var doc in docs) + /// + /// Convert each T document in a BsonDocument, setting autoId for each one + /// + /// + /// + private IEnumerable GetBsonDocs(IEnumerable docs) + { + foreach (var document in docs) { - this.Insert(doc); - count++; - } + _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper)); - return count++; + var doc = _mapper.ToDocument(document); + + yield return doc; + } } } } diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs deleted file mode 100644 index 2bec94803..000000000 --- a/LiteDB/Core/Collections/InsertBulk.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteCollection - { - /// - /// Bulk documents to a collection - use data chunks for most efficient insert - /// - public int InsertBulk(IEnumerable docs, int buffer = 32768) - { - if (docs == null) throw new ArgumentNullException("docs"); - if (buffer < 100) throw new ArgumentException("buffer must be bigger than 100"); - - var enumerator = docs.GetEnumerator(); - var count = 0; - - while (true) - { - var buff = buffer; - - var more = true; - - while (buff > 0 && (more = enumerator.MoveNext())) - { - this.Insert(enumerator.Current); - buff--; - count++; - } - - if (more == false) - { - return count; - } - } - } - } -} diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index 12dff2310..d79c10e6a 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -19,11 +19,7 @@ public bool Update(T document) // get BsonDocument from object var doc = _mapper.ToDocument(document); - var id = doc["_id"]; - - if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - - return _engine.UpdateDocument(_name, id, doc); + return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }) > 0; } /// @@ -37,7 +33,10 @@ public bool Update(BsonValue id, T document) // get BsonDocument from object var doc = _mapper.ToDocument(document); - return _engine.UpdateDocument(_name, id, doc); + // set document _id using id parameter + doc["_id"] = id; + + return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }) > 0; } /// @@ -58,11 +57,8 @@ public int Update(Query query, Action action) // get BsonDocument from object var bson = _mapper.ToDocument(doc); - var id = bson["_id"]; - - if (id.IsNull || id.IsMinValue || id.IsMaxValue) throw LiteException.InvalidDataType("_id", id); - - count += _engine.UpdateDocument(_name, id, bson) ? 1 : 0; + throw new NotImplementedException(); + //count += _engine.UpdateDocuments(_name, id, bson) ? 1 : 0; } return count; diff --git a/LiteDB/Engine/Engine/Aggregate.cs b/LiteDB/Engine/Engine/Aggregate.cs new file mode 100644 index 000000000..37798f611 --- /dev/null +++ b/LiteDB/Engine/Engine/Aggregate.cs @@ -0,0 +1,32 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + public BsonValue Min(string colName, string field) + { + return true; + } + + public BsonValue Max(string colName, string field) + { + return true; + } + + public int Count(string colName, Query query) + { + return 0; + } + + public bool Exists(string colName, Query query) + { + return true; + } + } +} diff --git a/LiteDB/Engine/Engine/Collection.cs b/LiteDB/Engine/Engine/Collection.cs new file mode 100644 index 000000000..88918099f --- /dev/null +++ b/LiteDB/Engine/Engine/Collection.cs @@ -0,0 +1,27 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + public IEnumerable GetCollectionNames() + { + return null; + } + + public bool DropCollection(string colName) + { + return true; + } + + public bool RenameCollection(string colName, string newName) + { + return true; + } + } +} diff --git a/LiteDB/Engine/Engine/Delete.cs b/LiteDB/Engine/Engine/Delete.cs new file mode 100644 index 000000000..929499fa0 --- /dev/null +++ b/LiteDB/Engine/Engine/Delete.cs @@ -0,0 +1,64 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + /// + /// Implements delete based on a query result + /// + public int DeleteDocuments(string colName, Query query) + { + try + { + // start new transaction + _transaction.Begin(); + + var col = this.GetCollectionPage(colName, false); + + // no collection, no document - abort trans + if (col == null) + { + _transaction.Commit(); + return 0; + } + + var count = 0; + + // find nodes + var nodes = query.Run(col, _indexer); + + foreach (var node in nodes) + { + // read dataBlock (do not read all extend pages, i will not use) + var dataBlock = _data.Read(node.DataBlock, false); + + // lets remove all indexes that point to this in dataBlock + foreach (var index in col.GetIndexes(true)) + { + _indexer.Delete(index, dataBlock.IndexRef[index.Slot]); + } + + // remove object data + _data.Delete(col, node.DataBlock); + + count++; + } + + _transaction.Commit(); + + return count; + } + catch + { + _transaction.Rollback(); + throw; + } + } + } +} diff --git a/LiteDB/Engine/Engine/Find.cs b/LiteDB/Engine/Engine/Find.cs new file mode 100644 index 000000000..8953f46b9 --- /dev/null +++ b/LiteDB/Engine/Engine/Find.cs @@ -0,0 +1,45 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + public IEnumerable Find(string colName, Query query, int skip = 0, int limit = int.MaxValue) + { + lock(_locker) + { + _transaction.AvoidDirtyRead(); + + // get my collection page + var col = this.GetCollectionPage(colName, false); + + // no collection, no documents + if(col == null) yield break; + + // get nodes from query executor to get all IndexNodes + var nodes = query.Run(col, _indexer); + + // skip first N nodes + if (skip > 0) nodes = nodes.Skip(skip); + + // limit in M nodes + if (limit != int.MaxValue) nodes = nodes.Take(limit); + + // for each document, read data and deserialize as document + foreach (var node in nodes) + { + var dataBlock = _data.Read(node.DataBlock, true); + + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + + yield return doc; + } + } + } + } +} diff --git a/LiteDB/Engine/Engine/Index.cs b/LiteDB/Engine/Engine/Index.cs new file mode 100644 index 000000000..6458c8a68 --- /dev/null +++ b/LiteDB/Engine/Engine/Index.cs @@ -0,0 +1,152 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + /// + /// Create a new index (or do nothing if already exisits) to a collection/field + /// + public bool EnsureIndex(string colName, string field, IndexOptions options) + { + lock (_locker) + try + { + // start transaction + _transaction.Begin(); + + // do not create collection at this point + var col = this.GetCollectionPage(colName, true); + + // check if index already exists (if collection exists) + if (col != null && col.GetIndex(field) != null) + { + _transaction.Commit(); + return false; + } + + this.EnsureIndex(col, field, options); + + _transaction.Commit(); + + return true; + } + catch + { + _transaction.Rollback(); + throw; + } + } + + /// + /// Internal implementation of create an index - no locks, no trans + /// + private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptions options) + { + // create index head + var index = _indexer.CreateIndex(col); + + index.Field = field; + index.Options = options; + + // read all objects (read from PK index) + foreach (var node in new QueryAll("_id", Query.Ascending).Run(col, _indexer)) + { + var dataBlock = _data.Read(node.DataBlock, true); + + // read object + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + + // adding index + var key = doc.Get(field); + + var newNode = _indexer.AddNode(index, key); + + // adding this new index Node to indexRef + dataBlock.IndexRef[index.Slot] = newNode.Position; + + // link index node to datablock + newNode.DataBlock = dataBlock.Position; + + // mark datablock page as dirty + _pager.SetDirty(dataBlock.Page); + } + + return index; + } + + /// + /// List all indexes inside a collection + /// + public IEnumerable GetIndexes(string colName) + { + _transaction.AvoidDirtyRead(); + + var col = this.GetCollectionPage(colName, false); + + if (col == null) yield break; + + foreach (var index in col.GetIndexes(true)) + { + yield return new BsonDocument() + .Add("slot", index.Slot) + .Add("field", index.Field) + .Add("unique", index.Options.Unique); + } + } + + public bool DropIndex(string colName, string field) + { + if (field == "_id") throw LiteException.IndexDropId(); + + lock (_locker) + try + { + // start transaction + _transaction.Begin(); + + var col = this.GetCollectionPage(colName, false); + + // no collection, no index + if (col == null) + { + _transaction.Commit(); + return false; + } + + // search for index reference + var index = col.GetIndex(field); + + // no index, no drop + if (index == null) + { + _transaction.Commit(); + return false; + } + + // delete all data pages + indexes pages + _indexer.DropIndex(index); + + // clear index reference + index.Clear(); + + // save collection page + _pager.SetDirty(col); + + _transaction.Commit(); + + return true; + } + catch + { + _transaction.Rollback(); + throw; + } + } + } +} diff --git a/LiteDB/Engine/Engine/Insert.cs b/LiteDB/Engine/Engine/Insert.cs new file mode 100644 index 000000000..867e89e3c --- /dev/null +++ b/LiteDB/Engine/Engine/Insert.cs @@ -0,0 +1,84 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + /// + /// Implements insert documents in a collection + /// + public int InsertDocuments(string colName, IEnumerable docs) + { + lock(_locker) + try + { + // start transaction + _transaction.Begin(); + + var count = 0; + var col = this.GetCollectionPage(colName, true); + + foreach (var doc in docs) + { + BsonValue id; + + // add ObjectId to _id if _id not found + if (!doc.RawValue.TryGetValue("_id", out id)) + { + id = doc["_id"] = ObjectId.NewObjectId(); + } + + // test if _id is a valid type + if (id.IsNull || id.IsMinValue || id.IsMaxValue) + { + _transaction.Rollback(); + throw LiteException.InvalidDataType("_id", id); + } + + // serialize object + var bytes = BsonSerializer.Serialize(doc); + + // storage in data pages - returns dataBlock address + var dataBlock = _data.Insert(col, bytes); + + // store id in a PK index [0 array] + var pk = _indexer.AddNode(col.PK, id); + + // do links between index <-> data block + pk.DataBlock = dataBlock.Position; + dataBlock.IndexRef[0] = pk.Position; + + // for each index, insert new IndexNode + foreach (var index in col.GetIndexes(false)) + { + var key = doc.Get(index.Field); + + var node = _indexer.AddNode(index, key); + + // point my index to data object + node.DataBlock = dataBlock.Position; + + // point my dataBlock + dataBlock.IndexRef[index.Slot] = node.Position; + } + + count++; + } + + _transaction.Commit(); + + return count; + } + catch + { + _transaction.Rollback(); + throw; + } + } + } +} diff --git a/LiteDB/Engine/Engine/Update.cs b/LiteDB/Engine/Engine/Update.cs new file mode 100644 index 000000000..48a7b5b22 --- /dev/null +++ b/LiteDB/Engine/Engine/Update.cs @@ -0,0 +1,89 @@ +using LiteDB.Shell; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + internal partial class LiteEngine : IDisposable + { + /// + /// Implement update command to a document inside a collection + /// + public int UpdateDocuments(string colName, IEnumerable docs) + { + lock (_locker) + try + { + _transaction.Begin(); + + var count = 0; + var col = this.GetCollectionPage(colName, false); + + // if no collection, no updates + if (col == null) + { + _transaction.Commit(); + return 0; + } + + foreach (var doc in docs) + { + // normalize id before find + var id = doc["_id"].Normalize(col.PK.Options); + + // find indexNode from pk index + var indexNode = _indexer.Find(col.PK, id, false, Query.Ascending); + + // if not found document, no updates + if (indexNode == null) continue; + + // serialize document in bytes + var bytes = BsonSerializer.Serialize(doc); + + // update data storage + var dataBlock = _data.Update(col, indexNode.DataBlock, bytes); + + // delete/insert indexes - do not touch on PK + foreach (var index in col.GetIndexes(false)) + { + var key = doc.Get(index.Field); + + var node = _indexer.GetNode(dataBlock.IndexRef[index.Slot]); + + // check if my index node was changed + if (node.Key.CompareTo(key) != 0) + { + // remove old index node + _indexer.Delete(index, node.Position); + + // and add a new one + var newNode = _indexer.AddNode(index, key); + + // point my index to data object + newNode.DataBlock = dataBlock.Position; + + // point my dataBlock + dataBlock.IndexRef[index.Slot] = newNode.Position; + + _pager.SetDirty(dataBlock.Page); + } + } + + count++; + } + + _transaction.Commit(); + + return count; + } + catch + { + _transaction.Rollback(); + throw; + } + } + } +} diff --git a/LiteDB/Engine/LiteEngine.cs b/LiteDB/Engine/LiteEngine.cs index 3d7908eaf..09482b351 100644 --- a/LiteDB/Engine/LiteEngine.cs +++ b/LiteDB/Engine/LiteEngine.cs @@ -29,6 +29,10 @@ internal partial class LiteEngine : IDisposable private CollectionService _collections; + private Dictionary _collectionPages = new Dictionary(); + + private object _locker = new object(); + public LiteEngine(IDiskService disk) { _disk = disk; @@ -50,76 +54,39 @@ public LiteEngine(IDiskService disk) #endregion - public BsonValue InsertDocument(string col, BsonDocument doc) - { - return null; - } - - public bool UpdateDocument(string col, BsonValue id, BsonDocument doc) - { - return true; - } - - public int DeleteDocuments(string col, Query query) - { - return 0; - } - - public IEnumerable Find(string col, Query query, int skip = 0, int limit = int.MaxValue) - { - return null; - } - - public BsonValue Min(string colName, string field) + /// + /// Get the collection page only when nedded. Gets from pager always to garantee that wil be the last (in case of clear cache will get a new one - pageID never changes) + /// + internal CollectionPage GetCollectionPage(string name, bool addIfNotExits) { - return true; - } + uint pageID; - public BsonValue Max(string colName, string field) - { - return true; - } - - public int Count(string colName, Query query) - { - return 0; - } + // check if pageID is in my dictionary + if(_collectionPages.TryGetValue(name, out pageID)) + { + return _pager.GetPage(pageID); + } + else + { + // search my page on collection service + var col = _collections.Get(name); - public bool Exists(string colName, Query query) - { - return true; - } + if(col == null && addIfNotExits) + { + col = _collections.Add(name); + } - public bool EnsureIndex(string col, string field, IndexOptions options) - { - return true; - } - - public IEnumerable GetIndexes(string colName) - { - return null; - } + if(col != null) + { + _collectionPages.Add(name, col.PageID); - public bool DropIndex(string colName, string field) - { - return true; - } + return _pager.GetPage(col.PageID); + } + } - public IEnumerable GetCollectionNames() - { return null; } - public bool DropCollection(string colName) - { - return true; - } - - public bool RenameCollection(string colName, string newName) - { - return true; - } - public void Dispose() { _disk.Dispose(); diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs index 3a3b3ce3c..d36716212 100644 --- a/LiteDB/Engine/Services/TransactionService.cs +++ b/LiteDB/Engine/Services/TransactionService.cs @@ -62,11 +62,6 @@ public void Commit() { //Console.WriteLine("save page " + page.PageID); _disk.WritePage(page.PageID, page.WritePage()); - - if(page.PageID == 800) - { - Console.WriteLine("parar"); - } } // delete journal file - datafile is consist here @@ -84,7 +79,7 @@ public void Commit() public void Rollback() { - if (_trans == false) throw new SystemException("Rollback transaction"); + if (_trans == false) return; // clear all pages from memory (return true if has dirty pages on cache) if (_cache.Clear()) diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 528b3debe..e62eb6a2b 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -52,13 +52,19 @@ + + + + + + + - diff --git a/LiteDB/Query/Impl/QueryAnd.cs b/LiteDB/Query/Impl/QueryAnd.cs index bb360447d..94245e06e 100644 --- a/LiteDB/Query/Impl/QueryAnd.cs +++ b/LiteDB/Query/Impl/QueryAnd.cs @@ -23,10 +23,10 @@ internal override IEnumerable ExecuteIndex(IndexService indexer, Coll throw new NotSupportedException(); } - internal override IEnumerable Run(IndexService indexer, CollectionIndex index) + internal override IEnumerable Run(CollectionPage col, IndexService indexer) { - var left = _left.Run(indexer, index); - var right = _right.Run(indexer, index); + var left = _left.Run(col, indexer); + var right = _right.Run(col, indexer); return left.Intersect(right, new IndexNodeComparer()); } diff --git a/LiteDB/Query/Impl/QueryOr.cs b/LiteDB/Query/Impl/QueryOr.cs index 682a5067c..8d01bba18 100644 --- a/LiteDB/Query/Impl/QueryOr.cs +++ b/LiteDB/Query/Impl/QueryOr.cs @@ -23,10 +23,10 @@ internal override IEnumerable ExecuteIndex(IndexService indexer, Coll throw new NotSupportedException(); } - internal override IEnumerable Run(IndexService indexer, CollectionIndex index) + internal override IEnumerable Run(CollectionPage col, IndexService indexer) { - var left = _left.Run(indexer, index); - var right = _right.Run(indexer, index); + var left = _left.Run(col, indexer); + var right = _right.Run(col, indexer); return left.Union(right, new IndexNodeComparer()); } diff --git a/LiteDB/Query/Query.cs b/LiteDB/Query/Query.cs index afd5801fb..4f9415c71 100644 --- a/LiteDB/Query/Query.cs +++ b/LiteDB/Query/Query.cs @@ -171,10 +171,13 @@ public static Query Or(Query left, Query right) /// /// Find witch index will be used and run Execute method /// - internal virtual IEnumerable Run(IndexService indexer, CollectionIndex index) + internal virtual IEnumerable Run(CollectionPage col, IndexService indexer) { - // no collection just returns an empty list of indexnode - if (index == null) return new List(); + // get index for this query + var index = col.GetIndex(this.Field); + + // no index? throw an exception + if (index == null) throw LiteException.IndexNotFound(col.CollectionName, this.Field); // execute query to get all IndexNodes return this.ExecuteIndex(indexer, index); diff --git a/LiteDB/Shell/Commands/Collections/Bulk.cs b/LiteDB/Shell/Commands/Collections/Bulk.cs index 1d27f8415..400c8900f 100644 --- a/LiteDB/Shell/Commands/Collections/Bulk.cs +++ b/LiteDB/Shell/Commands/Collections/Bulk.cs @@ -23,7 +23,7 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) { var docs = JsonSerializer.DeserializeArray(sr); - return col.InsertBulk(docs.Select(x => x.AsDocument)); + return col.Insert(docs.Select(x => x.AsDocument)); } } } diff --git a/LiteDB/Utils/LiteException.cs b/LiteDB/Utils/LiteException.cs index 4d8e01a81..041ecdf0b 100644 --- a/LiteDB/Utils/LiteException.cs +++ b/LiteDB/Utils/LiteException.cs @@ -87,9 +87,9 @@ public static LiteException IndexKeyTooLong() return new LiteException(111, "Index key must be less than {0} bytes", IndexService.MAX_INDEX_LENGTH); } - public static LiteException IndexNotFound(string name) + public static LiteException IndexNotFound(string colName, string field) { - return new LiteException(112, "Index not found on '{0}'", name); + return new LiteException(112, "Index not found on '{0}.{1}'", colName, field); } public static LiteException LockTimeout(TimeSpan ts) From 22ddf68f2dfdac997594cfd1e9b1dcf91c1c0d86 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 17:46:41 -0200 Subject: [PATCH 43/91] Implement collection engine actions --- LiteDB-TODO.txt | 1 + LiteDB/Engine/Engine/Aggregate.cs | 68 ++++++++++++++++-- LiteDB/Engine/Engine/Collection.cs | 51 ++++++++++++- LiteDB/Engine/Engine/Delete.cs | 1 + LiteDB/Engine/Engine/Find.cs | 2 - LiteDB/Engine/Engine/Index.cs | 76 +++++++++----------- LiteDB/Engine/LiteEngine.cs | 6 ++ LiteDB/Engine/Services/TransactionService.cs | 11 +-- LiteDB/Engine/Structures/IndexOptions.cs | 5 -- 9 files changed, 161 insertions(+), 60 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index d7b236cff..d9f243f31 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,6 +2,7 @@ - Revisar help do Shell - saiu varias coisas - Mapper de indice está fora do ar +- No metodos do engine, a primeria coisa q faco é o GetCollecionPage()/BeginTrans? Usar avoid lá dentro? - Thread Safe - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private - Somente os metodos externos devem ter LOCK diff --git a/LiteDB/Engine/Engine/Aggregate.cs b/LiteDB/Engine/Engine/Aggregate.cs index 37798f611..145e92baf 100644 --- a/LiteDB/Engine/Engine/Aggregate.cs +++ b/LiteDB/Engine/Engine/Aggregate.cs @@ -9,24 +9,84 @@ namespace LiteDB { internal partial class LiteEngine : IDisposable { + /// + /// Returns first value from an index (first is min value) + /// public BsonValue Min(string colName, string field) { - return true; + // get collection page (no col, no min) + var col = this.GetCollectionPage(colName, false); + + if(col == null) return BsonValue.MinValue; + + // get index (no index, no min) + var index = col.GetIndex(field); + + if (index == null) return BsonValue.MinValue; + + var head = _indexer.GetNode(index.HeadNode); + var next = _indexer.GetNode(head.Next[0]); + + if (next.IsHeadTail(index)) return BsonValue.MinValue; + + return next.Key; } + /// + /// Returns last value from an index (last is max value) + /// public BsonValue Max(string colName, string field) { - return true; + // get collection page (no col, no max) + var col = this.GetCollectionPage(colName, false); + + if (col == null) return BsonValue.MaxValue; + + // get index (no index, no max) + var index = col.GetIndex(field); + + if (index == null) return BsonValue.MaxValue; + + var tail = _indexer.GetNode(index.TailNode); + var prev = _indexer.GetNode(tail.Prev[0]); + + if (prev.IsHeadTail(index)) return BsonValue.MaxValue; + + return prev.Key; } + /// + /// Count all nodes from a query execution - do not deserialize documents to count + /// public int Count(string colName, Query query) { - return 0; + // get collection page (no col, returns 0) + var col = this.GetCollectionPage(colName, false); + + if (col == null) return 0; + + // run query in this collection + var nodes = query.Run(col, _indexer); + + // count all nodes + return nodes.Count(); } + /// + /// Check if has at least one node in a query execution - do not deserialize documents to check + /// public bool Exists(string colName, Query query) { - return true; + // get collection page (no col, not exists) + var col = this.GetCollectionPage(colName, false); + + if (col == null) return false; + + // run query in this collection + var nodes = query.Run(col, _indexer); + + // check if has at least first + return nodes.FirstOrDefault() != null; } } } diff --git a/LiteDB/Engine/Engine/Collection.cs b/LiteDB/Engine/Engine/Collection.cs index 88918099f..af9a87be3 100644 --- a/LiteDB/Engine/Engine/Collection.cs +++ b/LiteDB/Engine/Engine/Collection.cs @@ -9,19 +9,66 @@ namespace LiteDB { internal partial class LiteEngine : IDisposable { + /// + /// Returns all collection inside datafile + /// public IEnumerable GetCollectionNames() { - return null; + _transaction.AvoidDirtyRead(); + + return _collections.GetAll().Select(x => x.CollectionName); } + /// + /// Drop collection including all documents, indexes and extended pages + /// public bool DropCollection(string colName) { + // get collection page + var col = this.GetCollectionPage(colName, false); + + if(col == null) return false; + + _collections.Drop(col); + return true; } + /// + /// Rename a collection + /// public bool RenameCollection(string colName, string newName) { - return true; + lock(_locker) + try + { + _transaction.Begin(); + + // get my collection - no col, no rename + var col = GetCollectionPage(colName, false); + + if(col == null) + { + _transaction.Commit(); + return false; + } + + // change collection name and save + col.CollectionName = newName; + _pager.SetDirty(col); + + // remove from collection cache + _collectionPages.Remove(colName); + + _transaction.Commit(); + + return true; + } + catch + { + _transaction.Rollback(); + throw; + } } } } diff --git a/LiteDB/Engine/Engine/Delete.cs b/LiteDB/Engine/Engine/Delete.cs index 929499fa0..e6d6f1821 100644 --- a/LiteDB/Engine/Engine/Delete.cs +++ b/LiteDB/Engine/Engine/Delete.cs @@ -14,6 +14,7 @@ internal partial class LiteEngine : IDisposable /// public int DeleteDocuments(string colName, Query query) { + lock(_locker) try { // start new transaction diff --git a/LiteDB/Engine/Engine/Find.cs b/LiteDB/Engine/Engine/Find.cs index 8953f46b9..5fe38644a 100644 --- a/LiteDB/Engine/Engine/Find.cs +++ b/LiteDB/Engine/Engine/Find.cs @@ -13,8 +13,6 @@ public IEnumerable Find(string colName, Query query, int skip = 0, { lock(_locker) { - _transaction.AvoidDirtyRead(); - // get my collection page var col = this.GetCollectionPage(colName, false); diff --git a/LiteDB/Engine/Engine/Index.cs b/LiteDB/Engine/Engine/Index.cs index 6458c8a68..d3103198e 100644 --- a/LiteDB/Engine/Engine/Index.cs +++ b/LiteDB/Engine/Engine/Index.cs @@ -23,61 +23,51 @@ public bool EnsureIndex(string colName, string field, IndexOptions options) // do not create collection at this point var col = this.GetCollectionPage(colName, true); - // check if index already exists (if collection exists) - if (col != null && col.GetIndex(field) != null) + // check if index already exists + if (col.GetIndex(field) != null) { _transaction.Commit(); return false; } - this.EnsureIndex(col, field, options); + // create index head + var index = _indexer.CreateIndex(col); - _transaction.Commit(); + index.Field = field; + index.Options = options; - return true; - } - catch - { - _transaction.Rollback(); - throw; - } - } - - /// - /// Internal implementation of create an index - no locks, no trans - /// - private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptions options) - { - // create index head - var index = _indexer.CreateIndex(col); + // read all objects (read from PK index) + foreach (var node in new QueryAll("_id", Query.Ascending).Run(col, _indexer)) + { + var dataBlock = _data.Read(node.DataBlock, true); - index.Field = field; - index.Options = options; + // read object + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - // read all objects (read from PK index) - foreach (var node in new QueryAll("_id", Query.Ascending).Run(col, _indexer)) - { - var dataBlock = _data.Read(node.DataBlock, true); + // adding index + var key = doc.Get(field); - // read object - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + var newNode = _indexer.AddNode(index, key); - // adding index - var key = doc.Get(field); + // adding this new index Node to indexRef + dataBlock.IndexRef[index.Slot] = newNode.Position; - var newNode = _indexer.AddNode(index, key); + // link index node to datablock + newNode.DataBlock = dataBlock.Position; - // adding this new index Node to indexRef - dataBlock.IndexRef[index.Slot] = newNode.Position; + // mark datablock page as dirty + _pager.SetDirty(dataBlock.Page); + } - // link index node to datablock - newNode.DataBlock = dataBlock.Position; + _transaction.Commit(); - // mark datablock page as dirty - _pager.SetDirty(dataBlock.Page); + return true; + } + catch + { + _transaction.Rollback(); + throw; } - - return index; } /// @@ -85,8 +75,6 @@ private CollectionIndex EnsureIndex(CollectionPage col, string field, IndexOptio /// public IEnumerable GetIndexes(string colName) { - _transaction.AvoidDirtyRead(); - var col = this.GetCollectionPage(colName, false); if (col == null) yield break; @@ -96,7 +84,11 @@ public IEnumerable GetIndexes(string colName) yield return new BsonDocument() .Add("slot", index.Slot) .Add("field", index.Field) - .Add("unique", index.Options.Unique); + .Add("unique", index.Options.Unique) + .Add("ignoreCase", index.Options.IgnoreCase) + .Add("removeAccents", index.Options.RemoveAccents) + .Add("trimWhitespace", index.Options.TrimWhitespace) + .Add("emptyStringToNull", index.Options.EmptyStringToNull); } } diff --git a/LiteDB/Engine/LiteEngine.cs b/LiteDB/Engine/LiteEngine.cs index 09482b351..3083e00cf 100644 --- a/LiteDB/Engine/LiteEngine.cs +++ b/LiteDB/Engine/LiteEngine.cs @@ -61,6 +61,12 @@ internal CollectionPage GetCollectionPage(string name, bool addIfNotExits) { uint pageID; + // read if data file was changed by another process - if changed, reset my collection pageId cache + if(_transaction.AvoidDirtyRead()) + { + _collectionPages = new Dictionary(); + } + // check if pageID is in my dictionary if(_collectionPages.TryGetValue(name, out pageID)) { diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs index d36716212..4d8ced026 100644 --- a/LiteDB/Engine/Services/TransactionService.cs +++ b/LiteDB/Engine/Services/TransactionService.cs @@ -29,8 +29,6 @@ public void Begin() { if(_trans == true) throw new SystemException("Begin transaction"); - this.AvoidDirtyRead(); - // lock (or try to) datafile _disk.Lock(); @@ -98,16 +96,16 @@ public void Rollback() /// This method must be called before read/write operation to avoid dirty reads. /// It's occurs when my cache contains pages that was changed in another process /// - public void AvoidDirtyRead() + public bool AvoidDirtyRead() { lock (_disk) { // if is in transaction pages are not dirty (begin trans was checked) - if (_trans == true) return; + if (_trans == true) return false; var cache = _cache.GetPage(0); - if (cache == null) return; + if (cache == null) return false; // read change direct from disk var change = _disk.GetChangeID(); @@ -117,7 +115,10 @@ public void AvoidDirtyRead() { //Console.WriteLine("Datafile changed, clear cache"); _cache.Clear(); + return true; } + + return false; } } } diff --git a/LiteDB/Engine/Structures/IndexOptions.cs b/LiteDB/Engine/Structures/IndexOptions.cs index 73b716ec7..a6b3e4757 100644 --- a/LiteDB/Engine/Structures/IndexOptions.cs +++ b/LiteDB/Engine/Structures/IndexOptions.cs @@ -15,31 +15,26 @@ public class IndexOptions : IEquatable /// /// Unique keys? /// - [BsonField("unique")] public bool Unique { get; set; } /// /// Ignore case? (convert all strings to lowercase) /// - [BsonField("ignoreCase")] public bool IgnoreCase { get; set; } /// /// Remove all whitespace on start/end string? /// - [BsonField("trimWhitespace")] public bool TrimWhitespace { get; set; } /// /// Convert all empty string to null? /// - [BsonField("emptyStringToNull")] public bool EmptyStringToNull { get; set; } /// /// Removing accents on string? /// - [BsonField("removeAccents")] public bool RemoveAccents { get; set; } public IndexOptions() From 4cfd01e14944dd0b093947a573c6e99dcefa9064 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 17:47:14 -0200 Subject: [PATCH 44/91] Change dir name --- LiteDB/Engine/{Engine => Actions}/Aggregate.cs | 0 LiteDB/Engine/{Engine => Actions}/Collection.cs | 0 LiteDB/Engine/{Engine => Actions}/Delete.cs | 0 LiteDB/Engine/{Engine => Actions}/Find.cs | 0 LiteDB/Engine/{Engine => Actions}/Index.cs | 0 LiteDB/Engine/{Engine => Actions}/Insert.cs | 0 LiteDB/Engine/{Engine => Actions}/Update.cs | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename LiteDB/Engine/{Engine => Actions}/Aggregate.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Collection.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Delete.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Find.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Index.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Insert.cs (100%) rename LiteDB/Engine/{Engine => Actions}/Update.cs (100%) diff --git a/LiteDB/Engine/Engine/Aggregate.cs b/LiteDB/Engine/Actions/Aggregate.cs similarity index 100% rename from LiteDB/Engine/Engine/Aggregate.cs rename to LiteDB/Engine/Actions/Aggregate.cs diff --git a/LiteDB/Engine/Engine/Collection.cs b/LiteDB/Engine/Actions/Collection.cs similarity index 100% rename from LiteDB/Engine/Engine/Collection.cs rename to LiteDB/Engine/Actions/Collection.cs diff --git a/LiteDB/Engine/Engine/Delete.cs b/LiteDB/Engine/Actions/Delete.cs similarity index 100% rename from LiteDB/Engine/Engine/Delete.cs rename to LiteDB/Engine/Actions/Delete.cs diff --git a/LiteDB/Engine/Engine/Find.cs b/LiteDB/Engine/Actions/Find.cs similarity index 100% rename from LiteDB/Engine/Engine/Find.cs rename to LiteDB/Engine/Actions/Find.cs diff --git a/LiteDB/Engine/Engine/Index.cs b/LiteDB/Engine/Actions/Index.cs similarity index 100% rename from LiteDB/Engine/Engine/Index.cs rename to LiteDB/Engine/Actions/Index.cs diff --git a/LiteDB/Engine/Engine/Insert.cs b/LiteDB/Engine/Actions/Insert.cs similarity index 100% rename from LiteDB/Engine/Engine/Insert.cs rename to LiteDB/Engine/Actions/Insert.cs diff --git a/LiteDB/Engine/Engine/Update.cs b/LiteDB/Engine/Actions/Update.cs similarity index 100% rename from LiteDB/Engine/Engine/Update.cs rename to LiteDB/Engine/Actions/Update.cs From 812ba7a7035d33b9c1cfc4260880c6d4681041bb Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 18:18:57 -0200 Subject: [PATCH 45/91] Transaction code block --- LiteDB/Core/Collections/Include.cs | 14 +-- LiteDB/Engine/Actions/Collection.cs | 23 +---- LiteDB/Engine/Actions/Delete.cs | 23 +---- LiteDB/Engine/Actions/Find.cs | 35 ++++--- LiteDB/Engine/Actions/Index.cs | 92 ++++++------------- LiteDB/Engine/Actions/Insert.cs | 19 +--- LiteDB/Engine/Actions/Update.cs | 21 +---- LiteDB/Engine/LiteEngine.cs | 27 +++++- LiteDB/Engine/Services/TransactionService.cs | 4 +- LiteDB/LiteDB.csproj | 14 +-- .../Shell/Commands/Collections/EnsureIndex.cs | 2 +- 11 files changed, 99 insertions(+), 175 deletions(-) diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index 5788ab1c4..f32334624 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -19,18 +19,18 @@ public LiteCollection Include(Expression> dbref) { if (dbref == null) throw new ArgumentNullException("dbref"); - var propPath = dbref.GetPath(); + var path = dbref.GetPath(); Action action = (bson) => { - var prop = bson.Get(propPath); + var value = bson.Get(path); - if(prop.IsNull) return; + if(value.IsNull) return; // if property value is an array, populate all values - if(prop.IsArray) + if(value.IsArray) { - var array = prop.AsArray; + var array = value.AsArray; if(array.Count == 0) return; // all doc refs in an array must be same collection, lets take first only @@ -45,10 +45,10 @@ public LiteCollection Include(Expression> dbref) else { // for BsonDocument, get property value e update with full object refence - var doc = prop.AsDocument; + var doc = value.AsDocument; var col = new LiteCollection(doc["$ref"], _engine, _mapper); var obj = col.FindById(doc["$id"]); - bson.Set(propPath, obj); + bson.Set(path, obj); } }; diff --git a/LiteDB/Engine/Actions/Collection.cs b/LiteDB/Engine/Actions/Collection.cs index af9a87be3..bf3bb8c76 100644 --- a/LiteDB/Engine/Actions/Collection.cs +++ b/LiteDB/Engine/Actions/Collection.cs @@ -39,19 +39,9 @@ public bool DropCollection(string colName) /// public bool RenameCollection(string colName, string newName) { - lock(_locker) - try + return this.Transaction(colName, false, (col) => { - _transaction.Begin(); - - // get my collection - no col, no rename - var col = GetCollectionPage(colName, false); - - if(col == null) - { - _transaction.Commit(); - return false; - } + if(col == null) return false; // change collection name and save col.CollectionName = newName; @@ -60,15 +50,8 @@ public bool RenameCollection(string colName, string newName) // remove from collection cache _collectionPages.Remove(colName); - _transaction.Commit(); - return true; - } - catch - { - _transaction.Rollback(); - throw; - } + }); } } } diff --git a/LiteDB/Engine/Actions/Delete.cs b/LiteDB/Engine/Actions/Delete.cs index e6d6f1821..970a6a6be 100644 --- a/LiteDB/Engine/Actions/Delete.cs +++ b/LiteDB/Engine/Actions/Delete.cs @@ -14,20 +14,10 @@ internal partial class LiteEngine : IDisposable /// public int DeleteDocuments(string colName, Query query) { - lock(_locker) - try + return this.Transaction(colName, false, (col) => { - // start new transaction - _transaction.Begin(); - - var col = this.GetCollectionPage(colName, false); - // no collection, no document - abort trans - if (col == null) - { - _transaction.Commit(); - return 0; - } + if (col == null) return 0; var count = 0; @@ -51,15 +41,8 @@ public int DeleteDocuments(string colName, Query query) count++; } - _transaction.Commit(); - return count; - } - catch - { - _transaction.Rollback(); - throw; - } + }); } } } diff --git a/LiteDB/Engine/Actions/Find.cs b/LiteDB/Engine/Actions/Find.cs index 5fe38644a..6c28aa6c6 100644 --- a/LiteDB/Engine/Actions/Find.cs +++ b/LiteDB/Engine/Actions/Find.cs @@ -11,32 +11,29 @@ internal partial class LiteEngine : IDisposable { public IEnumerable Find(string colName, Query query, int skip = 0, int limit = int.MaxValue) { - lock(_locker) - { - // get my collection page - var col = this.GetCollectionPage(colName, false); + // get my collection page + var col = this.GetCollectionPage(colName, false); - // no collection, no documents - if(col == null) yield break; + // no collection, no documents + if(col == null) yield break; - // get nodes from query executor to get all IndexNodes - var nodes = query.Run(col, _indexer); + // get nodes from query executor to get all IndexNodes + var nodes = query.Run(col, _indexer); - // skip first N nodes - if (skip > 0) nodes = nodes.Skip(skip); + // skip first N nodes + if (skip > 0) nodes = nodes.Skip(skip); - // limit in M nodes - if (limit != int.MaxValue) nodes = nodes.Take(limit); + // limit in M nodes + if (limit != int.MaxValue) nodes = nodes.Take(limit); - // for each document, read data and deserialize as document - foreach (var node in nodes) - { - var dataBlock = _data.Read(node.DataBlock, true); + // for each document, read data and deserialize as document + foreach (var node in nodes) + { + var dataBlock = _data.Read(node.DataBlock, true); - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - yield return doc; - } + yield return doc; } } } diff --git a/LiteDB/Engine/Actions/Index.cs b/LiteDB/Engine/Actions/Index.cs index d3103198e..ad40f9ab0 100644 --- a/LiteDB/Engine/Actions/Index.cs +++ b/LiteDB/Engine/Actions/Index.cs @@ -14,21 +14,10 @@ internal partial class LiteEngine : IDisposable /// public bool EnsureIndex(string colName, string field, IndexOptions options) { - lock (_locker) - try + return this.Transaction(colName, true, (col) => { - // start transaction - _transaction.Begin(); - - // do not create collection at this point - var col = this.GetCollectionPage(colName, true); - // check if index already exists - if (col.GetIndex(field) != null) - { - _transaction.Commit(); - return false; - } + if (col.GetIndex(field) != null) return false; // create index head var index = _indexer.CreateIndex(col); @@ -59,67 +48,27 @@ public bool EnsureIndex(string colName, string field, IndexOptions options) _pager.SetDirty(dataBlock.Page); } - _transaction.Commit(); - return true; - } - catch - { - _transaction.Rollback(); - throw; - } + }); } /// - /// List all indexes inside a collection + /// Drop an index from a collection /// - public IEnumerable GetIndexes(string colName) - { - var col = this.GetCollectionPage(colName, false); - - if (col == null) yield break; - - foreach (var index in col.GetIndexes(true)) - { - yield return new BsonDocument() - .Add("slot", index.Slot) - .Add("field", index.Field) - .Add("unique", index.Options.Unique) - .Add("ignoreCase", index.Options.IgnoreCase) - .Add("removeAccents", index.Options.RemoveAccents) - .Add("trimWhitespace", index.Options.TrimWhitespace) - .Add("emptyStringToNull", index.Options.EmptyStringToNull); - } - } - public bool DropIndex(string colName, string field) { if (field == "_id") throw LiteException.IndexDropId(); - lock (_locker) - try + return this.Transaction(colName, false, (col) => { - // start transaction - _transaction.Begin(); - - var col = this.GetCollectionPage(colName, false); - // no collection, no index - if (col == null) - { - _transaction.Commit(); - return false; - } + if (col == null) return false; // search for index reference var index = col.GetIndex(field); // no index, no drop - if (index == null) - { - _transaction.Commit(); - return false; - } + if (index == null) return false; // delete all data pages + indexes pages _indexer.DropIndex(index); @@ -130,14 +79,29 @@ public bool DropIndex(string colName, string field) // save collection page _pager.SetDirty(col); - _transaction.Commit(); - return true; - } - catch + }); + } + + /// + /// List all indexes inside a collection + /// + public IEnumerable GetIndexes(string colName) + { + var col = this.GetCollectionPage(colName, false); + + if (col == null) yield break; + + foreach (var index in col.GetIndexes(true)) { - _transaction.Rollback(); - throw; + yield return new BsonDocument() + .Add("slot", index.Slot) + .Add("field", index.Field) + .Add("unique", index.Options.Unique) + .Add("ignoreCase", index.Options.IgnoreCase) + .Add("removeAccents", index.Options.RemoveAccents) + .Add("trimWhitespace", index.Options.TrimWhitespace) + .Add("emptyStringToNull", index.Options.EmptyStringToNull); } } } diff --git a/LiteDB/Engine/Actions/Insert.cs b/LiteDB/Engine/Actions/Insert.cs index 867e89e3c..a5589ca1d 100644 --- a/LiteDB/Engine/Actions/Insert.cs +++ b/LiteDB/Engine/Actions/Insert.cs @@ -14,14 +14,9 @@ internal partial class LiteEngine : IDisposable /// public int InsertDocuments(string colName, IEnumerable docs) { - lock(_locker) - try + return this.Transaction(colName, true, (col) => { - // start transaction - _transaction.Begin(); - var count = 0; - var col = this.GetCollectionPage(colName, true); foreach (var doc in docs) { @@ -36,7 +31,6 @@ public int InsertDocuments(string colName, IEnumerable docs) // test if _id is a valid type if (id.IsNull || id.IsMinValue || id.IsMaxValue) { - _transaction.Rollback(); throw LiteException.InvalidDataType("_id", id); } @@ -70,15 +64,8 @@ public int InsertDocuments(string colName, IEnumerable docs) count++; } - _transaction.Commit(); - - return count; - } - catch - { - _transaction.Rollback(); - throw; - } + return count++; + }); } } } diff --git a/LiteDB/Engine/Actions/Update.cs b/LiteDB/Engine/Actions/Update.cs index 48a7b5b22..c7fc5bb39 100644 --- a/LiteDB/Engine/Actions/Update.cs +++ b/LiteDB/Engine/Actions/Update.cs @@ -14,20 +14,12 @@ internal partial class LiteEngine : IDisposable /// public int UpdateDocuments(string colName, IEnumerable docs) { - lock (_locker) - try + return this.Transaction(colName, false, (col) => { - _transaction.Begin(); - var count = 0; - var col = this.GetCollectionPage(colName, false); // if no collection, no updates - if (col == null) - { - _transaction.Commit(); - return 0; - } + if (col == null) return 0; foreach (var doc in docs) { @@ -75,15 +67,8 @@ public int UpdateDocuments(string colName, IEnumerable docs) count++; } - _transaction.Commit(); - return count; - } - catch - { - _transaction.Rollback(); - throw; - } + }); } } } diff --git a/LiteDB/Engine/LiteEngine.cs b/LiteDB/Engine/LiteEngine.cs index 3083e00cf..28ad36bc8 100644 --- a/LiteDB/Engine/LiteEngine.cs +++ b/LiteDB/Engine/LiteEngine.cs @@ -57,7 +57,7 @@ public LiteEngine(IDiskService disk) /// /// Get the collection page only when nedded. Gets from pager always to garantee that wil be the last (in case of clear cache will get a new one - pageID never changes) /// - internal CollectionPage GetCollectionPage(string name, bool addIfNotExits) + private CollectionPage GetCollectionPage(string name, bool addIfNotExits) { uint pageID; @@ -93,6 +93,31 @@ internal CollectionPage GetCollectionPage(string name, bool addIfNotExits) return null; } + /// + /// Encapsulate all transaction commands in same data structure + /// + private T Transaction(string colName, bool addIfNotExists, Func action) + { + lock(_locker) + try + { + _transaction.Begin(); + + var col = this.GetCollectionPage(colName, addIfNotExists); + + var result = action(col); + + _transaction.Commit(); + + return result; + } + catch + { + _transaction.Rollback(); + throw; + } + } + public void Dispose() { _disk.Dispose(); diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/Engine/Services/TransactionService.cs index 4d8ced026..563341c8d 100644 --- a/LiteDB/Engine/Services/TransactionService.cs +++ b/LiteDB/Engine/Services/TransactionService.cs @@ -27,7 +27,7 @@ internal TransactionService(IDiskService disk, CacheService cache) /// public void Begin() { - if(_trans == true) throw new SystemException("Begin transaction"); + if(_trans == true) throw new SystemException("Begin transaction already exists"); // lock (or try to) datafile _disk.Lock(); @@ -40,7 +40,7 @@ public void Begin() /// public void Commit() { - if (_trans == false) throw new SystemException("Commit transaction"); + if (_trans == false) throw new SystemException("No begin transaction"); if (_cache.HasDirtyPages) { diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index e62eb6a2b..e65bd1ec3 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -52,13 +52,13 @@ - - - - - - - + + + + + + + diff --git a/LiteDB/Shell/Commands/Collections/EnsureIndex.cs b/LiteDB/Shell/Commands/Collections/EnsureIndex.cs index 932026dfd..022e70c74 100644 --- a/LiteDB/Shell/Commands/Collections/EnsureIndex.cs +++ b/LiteDB/Shell/Commands/Collections/EnsureIndex.cs @@ -21,7 +21,7 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) if (doc.IsNull) { - return col.EnsureIndex(field, false); + return col.EnsureIndex(field); } else if (doc.IsBoolean) { From e1307295e8ed19e78cabcb98f2557f988014dbb7 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 18:19:32 -0200 Subject: [PATCH 46/91] Update my todo --- LiteDB-TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index d9f243f31..ca817dc1b 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,6 +2,7 @@ - Revisar help do Shell - saiu varias coisas - Mapper de indice está fora do ar +- A criação de indice esta sempre como Unique - No metodos do engine, a primeria coisa q faco é o GetCollecionPage()/BeginTrans? Usar avoid lá dentro? - Thread Safe - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private @@ -36,6 +37,7 @@ - Avaliar uso do ConcurrentDictionary na cache + - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() From 6c40eb3a3301de00eb91941ee80f6b5a52e44e46 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 19:36:36 -0200 Subject: [PATCH 47/91] Write binary bug --- LiteDB/Document/BsonValue.cs | 3 +-- LiteDB/Utils/ByteWriter.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/LiteDB/Document/BsonValue.cs b/LiteDB/Document/BsonValue.cs index d7b4315f7..5c3c01e6f 100644 --- a/LiteDB/Document/BsonValue.cs +++ b/LiteDB/Document/BsonValue.cs @@ -625,8 +625,7 @@ private int GetBytesCountElement(string key, BsonValue value, bool recalc) Encoding.UTF8.GetByteCount(key) + // CString 1 + // CString 0x00 value.GetBytesCount(recalc) + - (value.Type == BsonType.String || value.Type == BsonType.Binary || value.Type == BsonType.Guid ? 5 : 0) // bytes.Length + 0x?? - ; + (value.Type == BsonType.String || value.Type == BsonType.Binary || value.Type == BsonType.Guid ? 5 : 0); // bytes.Length + 0x?? } /// diff --git a/LiteDB/Utils/ByteWriter.cs b/LiteDB/Utils/ByteWriter.cs index 6d585d8ce..bf84ffb5e 100644 --- a/LiteDB/Utils/ByteWriter.cs +++ b/LiteDB/Utils/ByteWriter.cs @@ -38,7 +38,7 @@ public void Write(Byte value) public void Write(Boolean value) { - _buffer[_pos] = value ? (byte)1 : (byte)1; + _buffer[_pos] = value ? (byte)1 : (byte)0; _pos++; } From 8ecfb4be764245c9f751599901a2cf108ce11ccf Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 19:45:49 -0200 Subject: [PATCH 48/91] Small fixes --- LiteDB-TODO.txt | 11 ++--------- LiteDB/Utils/LiteException.cs | 7 +------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index ca817dc1b..be7f2f156 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,15 +2,9 @@ - Revisar help do Shell - saiu varias coisas - Mapper de indice está fora do ar -- A criação de indice esta sempre como Unique -- No metodos do engine, a primeria coisa q faco é o GetCollecionPage()/BeginTrans? Usar avoid lá dentro? +- FileStorage deveria usar metodos do LiteEngine (DbEngine) +- Implementar novamente o BulkInsert - Thread Safe - - Deve ter metodos internos para fazer a operacao SEM lock nos metodos private - - Somente os metodos externos devem ter LOCK - - Somente os metodos externos devem ter AvoidDirtyRead - - E transacao? Fica aonde? No Externo, sempre. No interno não - - `lock` pode ter inner lock do mesmo objeto na mesma thread - - A BeginTrans() deve ser aberto antes de fazer consultas no banco - para garantir o AvoidDirtyRead - O "lock" deve estar por fora do BeginTrans() lock(this.Database) { @@ -32,7 +26,6 @@ } - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - - Insert/Update/Delete/EnsureIndex/DropIndex/Rename - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache diff --git a/LiteDB/Utils/LiteException.cs b/LiteDB/Utils/LiteException.cs index 041ecdf0b..c85cf2283 100644 --- a/LiteDB/Utils/LiteException.cs +++ b/LiteDB/Utils/LiteException.cs @@ -32,11 +32,6 @@ public static LiteException NoDatabase() return new LiteException(100, "There is no database"); } - public static LiteException InvalidTransaction() - { - return new LiteException(101, "This operation doesn't support open transaction."); - } - public static LiteException FileNotFound(string fileId) { return new LiteException(102, "File '{0}' not found", fileId); @@ -113,7 +108,7 @@ public static LiteException InvalidFormat(string field, string format) public static LiteException DocumentMaxDepth(int depth) { - return new LiteException(201, "Document has more than {0} nested documents. Check for circular references", depth); + return new LiteException(201, "Document has more than {0} nested documents. Check for circular references (use DbRef)", depth); } public static LiteException InvalidCtor(Type type) From af6e159a7801755b91fa094c073e4c0f26804dee Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 22:42:48 -0200 Subject: [PATCH 49/91] New FileStorage using DbEngine --- LiteDB/Core/Collections/LiteCollection.cs | 4 +- LiteDB/Core/LiteDatabase.cs | 8 +-- .../{Engine => DbEngine}/Actions/Aggregate.cs | 2 +- .../Actions/Collection.cs | 2 +- LiteDB/{Engine => DbEngine}/Actions/Delete.cs | 2 +- LiteDB/{Engine => DbEngine}/Actions/Find.cs | 2 +- LiteDB/{Engine => DbEngine}/Actions/Index.cs | 2 +- LiteDB/{Engine => DbEngine}/Actions/Insert.cs | 2 +- LiteDB/{Engine => DbEngine}/Actions/Update.cs | 2 +- .../LiteEngine.cs => DbEngine/DbEngine.cs} | 4 +- .../Disks/FileDiskService.cs | 0 .../Disks/IDiskService.cs | 0 LiteDB/{Engine => DbEngine}/DumpDatabase.cs | 0 LiteDB/{Engine => DbEngine}/Pages/BasePage.cs | 0 .../Pages/CollectionPage.cs | 0 LiteDB/{Engine => DbEngine}/Pages/DataPage.cs | 0 .../{Engine => DbEngine}/Pages/ExtendPage.cs | 0 .../{Engine => DbEngine}/Pages/HeaderPage.cs | 0 .../{Engine => DbEngine}/Pages/IndexPage.cs | 0 .../Services/CacheService.cs | 2 +- .../Services/CollectionService.cs | 0 .../Services/DataService.cs | 0 .../Services/IndexService.cs | 0 .../Services/PageService.cs | 0 .../Services/TransactionService.cs | 0 .../Structures/CollectionIndex.cs | 0 .../Structures/DataBlock.cs | 0 .../Structures/IndexNode.cs | 0 .../Structures/IndexOptions.cs | 0 .../Structures/PageAddress.cs | 0 LiteDB/FileStorage/LiteFileInfo.cs | 14 ++--- LiteDB/FileStorage/LiteFileStorage.cs | 51 +++++++++--------- LiteDB/FileStorage/LiteFileStream.cs | 12 ++--- LiteDB/LiteDB.csproj | 54 +++++++++---------- 34 files changed, 82 insertions(+), 81 deletions(-) rename LiteDB/{Engine => DbEngine}/Actions/Aggregate.cs (98%) rename LiteDB/{Engine => DbEngine}/Actions/Collection.cs (96%) rename LiteDB/{Engine => DbEngine}/Actions/Delete.cs (96%) rename LiteDB/{Engine => DbEngine}/Actions/Find.cs (95%) rename LiteDB/{Engine => DbEngine}/Actions/Index.cs (98%) rename LiteDB/{Engine => DbEngine}/Actions/Insert.cs (97%) rename LiteDB/{Engine => DbEngine}/Actions/Update.cs (97%) rename LiteDB/{Engine/LiteEngine.cs => DbEngine/DbEngine.cs} (97%) rename LiteDB/{Engine => DbEngine}/Disks/FileDiskService.cs (100%) rename LiteDB/{Engine => DbEngine}/Disks/IDiskService.cs (100%) rename LiteDB/{Engine => DbEngine}/DumpDatabase.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/BasePage.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/CollectionPage.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/DataPage.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/ExtendPage.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/HeaderPage.cs (100%) rename LiteDB/{Engine => DbEngine}/Pages/IndexPage.cs (100%) rename LiteDB/{Engine => DbEngine}/Services/CacheService.cs (97%) rename LiteDB/{Engine => DbEngine}/Services/CollectionService.cs (100%) rename LiteDB/{Engine => DbEngine}/Services/DataService.cs (100%) rename LiteDB/{Engine => DbEngine}/Services/IndexService.cs (100%) rename LiteDB/{Engine => DbEngine}/Services/PageService.cs (100%) rename LiteDB/{Engine => DbEngine}/Services/TransactionService.cs (100%) rename LiteDB/{Engine => DbEngine}/Structures/CollectionIndex.cs (100%) rename LiteDB/{Engine => DbEngine}/Structures/DataBlock.cs (100%) rename LiteDB/{Engine => DbEngine}/Structures/IndexNode.cs (100%) rename LiteDB/{Engine => DbEngine}/Structures/IndexOptions.cs (100%) rename LiteDB/{Engine => DbEngine}/Structures/PageAddress.cs (100%) diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 6e80fef9a..f8950e5d7 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -9,7 +9,7 @@ namespace LiteDB public sealed partial class LiteCollection where T : new() { - private LiteEngine _engine; + private DbEngine _engine; private string _name; private BsonMapper _mapper; private List> _includes; @@ -20,7 +20,7 @@ public sealed partial class LiteCollection /// public string Name { get { return _name; } } - internal LiteCollection(string name, LiteEngine engine, BsonMapper mapper) + internal LiteCollection(string name, DbEngine engine, BsonMapper mapper) { _name = name; _engine = engine; diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 53324d3f3..8ee307311 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -12,7 +12,7 @@ namespace LiteDB /// public partial class LiteDatabase : IDisposable { - private LiteEngine _engine; + private DbEngine _engine; private BsonMapper _mapper; @@ -34,7 +34,7 @@ public LiteDatabase(string connectionString) if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); // initialize engine creating a new FileDiskService for data access - _engine = new LiteEngine(new FileDiskService(filename, journal, timeout, readOnly, password)); + _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password)); _mapper = BsonMapper.Global; } @@ -43,7 +43,7 @@ public LiteDatabase(string connectionString) /// public LiteDatabase(IDiskService diskService, BsonMapper mapper) { - _engine = new LiteEngine(diskService); + _engine = new DbEngine(diskService); _mapper = mapper; } @@ -111,7 +111,7 @@ public bool RenameCollection(string oldName, string newName) /// public LiteFileStorage FileStorage { - get { return _fs ?? (_fs = new LiteFileStorage(this)); } + get { return _fs ?? (_fs = new LiteFileStorage(_engine)); } } #endregion diff --git a/LiteDB/Engine/Actions/Aggregate.cs b/LiteDB/DbEngine/Actions/Aggregate.cs similarity index 98% rename from LiteDB/Engine/Actions/Aggregate.cs rename to LiteDB/DbEngine/Actions/Aggregate.cs index 145e92baf..f0736b164 100644 --- a/LiteDB/Engine/Actions/Aggregate.cs +++ b/LiteDB/DbEngine/Actions/Aggregate.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Returns first value from an index (first is min value) diff --git a/LiteDB/Engine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs similarity index 96% rename from LiteDB/Engine/Actions/Collection.cs rename to LiteDB/DbEngine/Actions/Collection.cs index bf3bb8c76..eb77f4202 100644 --- a/LiteDB/Engine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Returns all collection inside datafile diff --git a/LiteDB/Engine/Actions/Delete.cs b/LiteDB/DbEngine/Actions/Delete.cs similarity index 96% rename from LiteDB/Engine/Actions/Delete.cs rename to LiteDB/DbEngine/Actions/Delete.cs index 970a6a6be..c5793cd6d 100644 --- a/LiteDB/Engine/Actions/Delete.cs +++ b/LiteDB/DbEngine/Actions/Delete.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Implements delete based on a query result diff --git a/LiteDB/Engine/Actions/Find.cs b/LiteDB/DbEngine/Actions/Find.cs similarity index 95% rename from LiteDB/Engine/Actions/Find.cs rename to LiteDB/DbEngine/Actions/Find.cs index 6c28aa6c6..913634236 100644 --- a/LiteDB/Engine/Actions/Find.cs +++ b/LiteDB/DbEngine/Actions/Find.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { public IEnumerable Find(string colName, Query query, int skip = 0, int limit = int.MaxValue) { diff --git a/LiteDB/Engine/Actions/Index.cs b/LiteDB/DbEngine/Actions/Index.cs similarity index 98% rename from LiteDB/Engine/Actions/Index.cs rename to LiteDB/DbEngine/Actions/Index.cs index ad40f9ab0..f9c27c123 100644 --- a/LiteDB/Engine/Actions/Index.cs +++ b/LiteDB/DbEngine/Actions/Index.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Create a new index (or do nothing if already exisits) to a collection/field diff --git a/LiteDB/Engine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs similarity index 97% rename from LiteDB/Engine/Actions/Insert.cs rename to LiteDB/DbEngine/Actions/Insert.cs index a5589ca1d..3ab507b99 100644 --- a/LiteDB/Engine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Implements insert documents in a collection diff --git a/LiteDB/Engine/Actions/Update.cs b/LiteDB/DbEngine/Actions/Update.cs similarity index 97% rename from LiteDB/Engine/Actions/Update.cs rename to LiteDB/DbEngine/Actions/Update.cs index c7fc5bb39..7d9e05aa0 100644 --- a/LiteDB/Engine/Actions/Update.cs +++ b/LiteDB/DbEngine/Actions/Update.cs @@ -7,7 +7,7 @@ namespace LiteDB { - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { /// /// Implement update command to a document inside a collection diff --git a/LiteDB/Engine/LiteEngine.cs b/LiteDB/DbEngine/DbEngine.cs similarity index 97% rename from LiteDB/Engine/LiteEngine.cs rename to LiteDB/DbEngine/DbEngine.cs index 28ad36bc8..de1fd2a53 100644 --- a/LiteDB/Engine/LiteEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -11,7 +11,7 @@ namespace LiteDB /// A internal class that take care of all engine data structure access - it´s basic implementation of a NoSql database /// Its isolated from complete solution - works on low level only /// - internal partial class LiteEngine : IDisposable + internal partial class DbEngine : IDisposable { #region Services instances @@ -33,7 +33,7 @@ internal partial class LiteEngine : IDisposable private object _locker = new object(); - public LiteEngine(IDiskService disk) + public DbEngine(IDiskService disk) { _disk = disk; diff --git a/LiteDB/Engine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs similarity index 100% rename from LiteDB/Engine/Disks/FileDiskService.cs rename to LiteDB/DbEngine/Disks/FileDiskService.cs diff --git a/LiteDB/Engine/Disks/IDiskService.cs b/LiteDB/DbEngine/Disks/IDiskService.cs similarity index 100% rename from LiteDB/Engine/Disks/IDiskService.cs rename to LiteDB/DbEngine/Disks/IDiskService.cs diff --git a/LiteDB/Engine/DumpDatabase.cs b/LiteDB/DbEngine/DumpDatabase.cs similarity index 100% rename from LiteDB/Engine/DumpDatabase.cs rename to LiteDB/DbEngine/DumpDatabase.cs diff --git a/LiteDB/Engine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs similarity index 100% rename from LiteDB/Engine/Pages/BasePage.cs rename to LiteDB/DbEngine/Pages/BasePage.cs diff --git a/LiteDB/Engine/Pages/CollectionPage.cs b/LiteDB/DbEngine/Pages/CollectionPage.cs similarity index 100% rename from LiteDB/Engine/Pages/CollectionPage.cs rename to LiteDB/DbEngine/Pages/CollectionPage.cs diff --git a/LiteDB/Engine/Pages/DataPage.cs b/LiteDB/DbEngine/Pages/DataPage.cs similarity index 100% rename from LiteDB/Engine/Pages/DataPage.cs rename to LiteDB/DbEngine/Pages/DataPage.cs diff --git a/LiteDB/Engine/Pages/ExtendPage.cs b/LiteDB/DbEngine/Pages/ExtendPage.cs similarity index 100% rename from LiteDB/Engine/Pages/ExtendPage.cs rename to LiteDB/DbEngine/Pages/ExtendPage.cs diff --git a/LiteDB/Engine/Pages/HeaderPage.cs b/LiteDB/DbEngine/Pages/HeaderPage.cs similarity index 100% rename from LiteDB/Engine/Pages/HeaderPage.cs rename to LiteDB/DbEngine/Pages/HeaderPage.cs diff --git a/LiteDB/Engine/Pages/IndexPage.cs b/LiteDB/DbEngine/Pages/IndexPage.cs similarity index 100% rename from LiteDB/Engine/Pages/IndexPage.cs rename to LiteDB/DbEngine/Pages/IndexPage.cs diff --git a/LiteDB/Engine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs similarity index 97% rename from LiteDB/Engine/Services/CacheService.cs rename to LiteDB/DbEngine/Services/CacheService.cs index 0bef37f33..7696275b1 100644 --- a/LiteDB/Engine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -38,7 +38,7 @@ public T GetPage(uint pageID) // if a need a specific page but has a BasePage, returns null if (page != null && page.GetType() == typeof(BasePage) && typeof(T) != typeof(BasePage)) { - if(page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); + //if(page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); var specificPage = new T(); diff --git a/LiteDB/Engine/Services/CollectionService.cs b/LiteDB/DbEngine/Services/CollectionService.cs similarity index 100% rename from LiteDB/Engine/Services/CollectionService.cs rename to LiteDB/DbEngine/Services/CollectionService.cs diff --git a/LiteDB/Engine/Services/DataService.cs b/LiteDB/DbEngine/Services/DataService.cs similarity index 100% rename from LiteDB/Engine/Services/DataService.cs rename to LiteDB/DbEngine/Services/DataService.cs diff --git a/LiteDB/Engine/Services/IndexService.cs b/LiteDB/DbEngine/Services/IndexService.cs similarity index 100% rename from LiteDB/Engine/Services/IndexService.cs rename to LiteDB/DbEngine/Services/IndexService.cs diff --git a/LiteDB/Engine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs similarity index 100% rename from LiteDB/Engine/Services/PageService.cs rename to LiteDB/DbEngine/Services/PageService.cs diff --git a/LiteDB/Engine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs similarity index 100% rename from LiteDB/Engine/Services/TransactionService.cs rename to LiteDB/DbEngine/Services/TransactionService.cs diff --git a/LiteDB/Engine/Structures/CollectionIndex.cs b/LiteDB/DbEngine/Structures/CollectionIndex.cs similarity index 100% rename from LiteDB/Engine/Structures/CollectionIndex.cs rename to LiteDB/DbEngine/Structures/CollectionIndex.cs diff --git a/LiteDB/Engine/Structures/DataBlock.cs b/LiteDB/DbEngine/Structures/DataBlock.cs similarity index 100% rename from LiteDB/Engine/Structures/DataBlock.cs rename to LiteDB/DbEngine/Structures/DataBlock.cs diff --git a/LiteDB/Engine/Structures/IndexNode.cs b/LiteDB/DbEngine/Structures/IndexNode.cs similarity index 100% rename from LiteDB/Engine/Structures/IndexNode.cs rename to LiteDB/DbEngine/Structures/IndexNode.cs diff --git a/LiteDB/Engine/Structures/IndexOptions.cs b/LiteDB/DbEngine/Structures/IndexOptions.cs similarity index 100% rename from LiteDB/Engine/Structures/IndexOptions.cs rename to LiteDB/DbEngine/Structures/IndexOptions.cs diff --git a/LiteDB/Engine/Structures/PageAddress.cs b/LiteDB/DbEngine/Structures/PageAddress.cs similarity index 100% rename from LiteDB/Engine/Structures/PageAddress.cs rename to LiteDB/DbEngine/Structures/PageAddress.cs diff --git a/LiteDB/FileStorage/LiteFileInfo.cs b/LiteDB/FileStorage/LiteFileInfo.cs index b07ae8630..691a4d537 100644 --- a/LiteDB/FileStorage/LiteFileInfo.cs +++ b/LiteDB/FileStorage/LiteFileInfo.cs @@ -32,7 +32,7 @@ public class LiteFileInfo public DateTime UploadDate { get; internal set; } public BsonDocument Metadata { get; set; } - private LiteDatabase _db; + private DbEngine _engine; public LiteFileInfo(string id) : this(id, id) @@ -51,9 +51,9 @@ public LiteFileInfo(string id, string filename) this.Metadata = new BsonDocument(); } - internal LiteFileInfo(LiteDatabase db, BsonDocument doc) + internal LiteFileInfo(DbEngine engine, BsonDocument doc) { - _db = db; + _engine = engine; this.Id = doc["_id"].AsString; this.Filename = doc["filename"].AsString; @@ -97,7 +97,7 @@ internal IEnumerable CreateChunks(Stream stream) if (read != CHUNK_SIZE) { var bytes = new byte[read]; - Array.Copy(buffer, bytes, read); + Buffer.BlockCopy(buffer, 0, bytes, 0, read); chunk["data"] = bytes; } else @@ -124,9 +124,9 @@ internal static string GetChunckId(string fileId, int index) /// public LiteFileStream OpenRead() { - if (_db == null) throw LiteException.NoDatabase(); + if (_engine == null) throw LiteException.NoDatabase(); - return new LiteFileStream(_db, this); + return new LiteFileStream(_engine, this); } /// @@ -134,7 +134,7 @@ public LiteFileStream OpenRead() /// public void SaveAs(string filename, bool overwritten = true) { - if (_db == null) throw LiteException.NoDatabase(); + if (_engine == null) throw LiteException.NoDatabase(); using (var file = new FileStream(filename, overwritten ? FileMode.Create : FileMode.CreateNew)) { diff --git a/LiteDB/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs index 234e818e4..30377a2f5 100644 --- a/LiteDB/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -11,15 +11,14 @@ namespace LiteDB /// public partial class LiteFileStorage { - public LiteCollection Files { get; private set; } - public LiteCollection Chunks { get; private set; } - public LiteDatabase Database { get; private set; } + internal const string FILES = "_files"; + internal const string CHUNKS = "_chunks"; - internal LiteFileStorage(LiteDatabase db) + private DbEngine _engine; + + internal LiteFileStorage(DbEngine engine) { - this.Database = db; - this.Files = this.Database.GetCollection("_files"); - this.Chunks = this.Database.GetCollection("_chunks"); + _engine = engine; } #region Upload @@ -35,16 +34,16 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) file.UploadDate = DateTime.Now; // insert file in _files collections with 0 file length - this.Files.Insert(file.AsDocument); + _engine.InsertDocuments(FILES, new BsonDocument[] { file.AsDocument }); // for each chunk, insert as a chunk document foreach (var chunk in file.CreateChunks(stream)) { - this.Chunks.Insert(chunk); + _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }); } // update fileLength to confirm full file length stored in disk - this.Files.Update(file.AsDocument); + _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }); return file; } @@ -81,7 +80,7 @@ public bool SetMetadata(string id, BsonDocument metadata) var file = this.FindById(id); if (file == null) return false; file.Metadata = metadata; - this.Files.Update(file.AsDocument); + _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }); return true; } @@ -110,7 +109,7 @@ public LiteFileStream OpenRead(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id"); - var doc = this.Files.FindById(id); + var doc = _engine.Find(FILES, Query.EQ("_id", id)).FirstOrDefault(); if (doc == null) return null; @@ -124,7 +123,7 @@ internal LiteFileStream OpenRead(LiteFileInfo entry) { if (entry == null) throw new ArgumentNullException("entry"); - return new LiteFileStream(this.Database, entry); + return new LiteFileStream(_engine, entry); } #endregion @@ -138,11 +137,11 @@ public LiteFileInfo FindById(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id"); - var doc = this.Files.FindById(id); + var doc = _engine.Find(FILES, Query.EQ("_id", id)).FirstOrDefault(); if (doc == null) return null; - return new LiteFileInfo(this.Database, doc); + return new LiteFileInfo(_engine, doc); } /// @@ -150,13 +149,15 @@ public LiteFileInfo FindById(string id) /// public IEnumerable Find(string startsWith) { - var result = string.IsNullOrEmpty(startsWith) ? - this.Files.Find(Query.All()) : - this.Files.Find(Query.StartsWith("_id", startsWith)); + var query = string.IsNullOrEmpty(startsWith) ? + Query.All() : + Query.StartsWith("_id", startsWith); + + var docs = _engine.Find(FILES, query); - foreach (var doc in result) + foreach (var doc in docs) { - yield return new LiteFileInfo(this.Database, doc); + yield return new LiteFileInfo(_engine, doc); } } @@ -165,7 +166,7 @@ public IEnumerable Find(string startsWith) /// public bool Exists(string id) { - return this.Files.Exists(Query.EQ("_id", id)); + return _engine.Exists(FILES, Query.EQ("_id", id)); } /// @@ -188,18 +189,18 @@ public bool Delete(string id) if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id"); // remove file reference in _files - var d = this.Files.Delete(id); + var d = _engine.DeleteDocuments(FILES, Query.EQ("_id", id)); // if not found, just return false - if (d == false) return false; + if (d == 0) return false; var index = 0; while (true) { - var del = Chunks.Delete(LiteFileInfo.GetChunckId(id, index++)); + var del = _engine.DeleteDocuments(CHUNKS, Query.EQ("_id", LiteFileInfo.GetChunckId(id, index++))); - if (del == false) break; + if (del == 0) break; } return true; diff --git a/LiteDB/FileStorage/LiteFileStream.cs b/LiteDB/FileStorage/LiteFileStream.cs index edbe0e63a..245ff385d 100644 --- a/LiteDB/FileStorage/LiteFileStream.cs +++ b/LiteDB/FileStorage/LiteFileStream.cs @@ -9,7 +9,7 @@ namespace LiteDB { public class LiteFileStream : Stream { - private LiteDatabase _db; + private DbEngine _engine; private LiteFileInfo _file; private readonly long _streamLength = 0; @@ -19,9 +19,9 @@ public class LiteFileStream : Stream private byte[] _currentChunkData = null; private int _positionInChunk = 0; - internal LiteFileStream(LiteDatabase db, LiteFileInfo file) + internal LiteFileStream(DbEngine engine, LiteFileInfo file) { - _db = db; + _engine = engine; _file = file; if (file.Length == 0) @@ -78,9 +78,9 @@ public override int Read(byte[] buffer, int offset, int count) private byte[] GetChunkData(int index) { // check if there is no more chunks in this file - var chunks = _db.GetCollection("_chunks"); - - var chunk = chunks.FindById(LiteFileInfo.GetChunckId(_file.Id, index)); + var chunk = _engine + .Find(LiteFileStorage.CHUNKS, Query.EQ("_id", LiteFileInfo.GetChunckId(_file.Id, index))) + .FirstOrDefault(); // if chunk is null there is no more chunks return chunk == null ? null : chunk["data"].AsBinary; diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index e65bd1ec3..6ab9a0706 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -51,15 +51,15 @@ - - - - - - - - - + + + + + + + + + @@ -130,29 +130,29 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + From a9b6b57baac84313bf7d63023b660663cd918cf4 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 22:51:29 -0200 Subject: [PATCH 50/91] Add chunk in FileInfo --- LiteDB-TODO.txt | 59 ++------------------------- LiteDB/FileStorage/LiteFileInfo.cs | 5 +++ LiteDB/FileStorage/LiteFileStorage.cs | 2 +- 3 files changed, 10 insertions(+), 56 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index be7f2f156..6c9ea95b8 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -2,35 +2,11 @@ - Revisar help do Shell - saiu varias coisas - Mapper de indice está fora do ar -- FileStorage deveria usar metodos do LiteEngine (DbEngine) - Implementar novamente o BulkInsert - Thread Safe - - O "lock" deve estar por fora do BeginTrans() - lock(this.Database) - { - this.Database.Transaction.Begin(); - - try - { - var result = this.UpdateDocument(id, doc); - - this.Database.Transaction.Commit(); - - return result; - } - catch - { - this.Database.Transaction.Rollback(); - throw; - } - } - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache - - - - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache - Implementar StreamDiskService() @@ -112,37 +88,10 @@ e) Leitura conforme indice (retorno poucos registros) - Logotipo + Nome - Atualizar codeproject -## Vamos fazer??? Vai ser legal e meio insano - quem sabe em uma outra branch? - -LiteEngine - - Só trabalha com BsonDocument - - Query sem Linq (só bsonDocument) - - Totalmente interna - - Só faz o minimo de acesso a engine - - Cuida das transacoes, locks?! A pensar - - Inclui a QueryEngine - - A solução final pode não ficar tão eficiente quando que temos hoje, mas acho que há uma melhora na redução de bugs e complexidade - - Não pode ter estado (begintrans/commit) - a execução de um metodo deve sempre ser igual a outra - -LiteEngine(IDiskService) { - // AllServices - - Insert(colName, myDocInBson) : id - Update(colName, myId, myDocInBson): bool - Delete(colName, query) : int - Find(colName, query) : IEnumerable - Min\Max\Count\Exists(colName, query) - EnsureIndex(colName, fieldName) : bool - DropIndex(colName, fieldName) : bool - RenameCollection(colName, newName) : bool - - GetIndexes(colName) ? para que? só se for pro Shell?! - -} - -## Modulos -- Engine - - QueryEngine +## Modulos do LiteDB +- Core +- DbEngine +- QueryEngine - Mapper - Utils - Linq, shell, fileStorage, fts diff --git a/LiteDB/FileStorage/LiteFileInfo.cs b/LiteDB/FileStorage/LiteFileInfo.cs index 691a4d537..7d777b2c0 100644 --- a/LiteDB/FileStorage/LiteFileInfo.cs +++ b/LiteDB/FileStorage/LiteFileInfo.cs @@ -29,6 +29,7 @@ public class LiteFileInfo public string Filename { get; set; } public string MimeType { get; set; } public long Length { get; private set; } + public int Chunks { get; private set; } public DateTime UploadDate { get; internal set; } public BsonDocument Metadata { get; set; } @@ -47,6 +48,7 @@ public LiteFileInfo(string id, string filename) this.Filename = Path.GetFileName(filename); this.MimeType = MimeTypeConverter.GetMimeType(this.Filename); this.Length = 0; + this.Chunks = 0; this.UploadDate = DateTime.Now; this.Metadata = new BsonDocument(); } @@ -59,6 +61,7 @@ internal LiteFileInfo(DbEngine engine, BsonDocument doc) this.Filename = doc["filename"].AsString; this.MimeType = doc["mimeType"].AsString; this.Length = doc["length"].AsInt64; + this.Chunks = doc["chunks"].AsInt32; this.UploadDate = doc["uploadDate"].AsDateTime; this.Metadata = doc["metadata"].AsDocument; } @@ -73,6 +76,7 @@ public BsonDocument AsDocument doc["filename"] = this.Filename; doc["mimeType"] = this.MimeType; doc["length"] = this.Length; + doc["chunks"] = this.Chunks; doc["uploadDate"] = this.UploadDate; doc["metadata"] = this.Metadata ?? new BsonDocument(); @@ -89,6 +93,7 @@ internal IEnumerable CreateChunks(Stream stream) while ((read = stream.Read(buffer, 0, LiteFileInfo.CHUNK_SIZE)) > 0) { this.Length += (long)read; + this.Chunks++; var chunk = new BsonDocument(); diff --git a/LiteDB/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs index 30377a2f5..e354e3598 100644 --- a/LiteDB/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -42,7 +42,7 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }); } - // update fileLength to confirm full file length stored in disk + // update fileLength/chunks to confirm full file length stored in disk _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }); return file; From 4ca7992426fa66d90f157189bdd94aec1ef20ff0 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 11 Nov 2015 23:18:34 -0200 Subject: [PATCH 51/91] Found bugs on cache service --- LiteDB/DbEngine/Services/CacheService.cs | 29 +++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 7696275b1..529321f89 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -40,16 +40,17 @@ public T GetPage(uint pageID) { //if(page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); - var specificPage = new T(); - - specificPage.ReadPage(page.DiskData); - - lock(_cache) - { - _cache[pageID] = specificPage; - } - - return specificPage; + //var specificPage = new T(); + // + //specificPage.ReadPage(page.DiskData); + // + //lock(_cache) + //{ + // _cache[pageID] = specificPage; + //} + // + //return specificPage; + return null; } return (T)page; @@ -64,7 +65,7 @@ public void AddPage(BasePage page, bool dirty = false) lock(_cache) { // do not cache extend page - never will be reused - if (page.PageType != PageType.Extend) + //if (page.PageType != PageType.Extend) { _cache[page.PageID] = page; } @@ -74,7 +75,7 @@ public void AddPage(BasePage page, bool dirty = false) { page.IsDirty = true; _dirty[page.PageID] = page; - _cache[page.PageID] = page; + //_cache[page.PageID] = page; // if page is new (not exits on datafile), there is no journal for them if(page.DiskData.Length > 0) @@ -114,12 +115,14 @@ public void ClearDirty() page.IsDirty = false; // remove all non-header-collection pages (this can be optional in future) - if (page.PageType != PageType.Header && page.PageType != PageType.Collection) + //if (page.PageType != PageType.Header && page.PageType != PageType.Collection) + if (page.PageType == PageType.Extend) { _cache.Remove(page.PageID); } } + //_cache.Clear(); _dirty.Clear(); } } From 61c6ecd7780b5d2ed75fc71cef413f09cbba9d70 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 12 Nov 2015 11:25:50 -0200 Subject: [PATCH 52/91] Working in cache bugs --- LiteDB/DbEngine/Actions/Collection.cs | 12 ++-- LiteDB/DbEngine/DbEngine.cs | 2 +- LiteDB/DbEngine/Pages/BasePage.cs | 68 ++++++++++-------- LiteDB/DbEngine/Services/CacheService.cs | 70 +++++++++++-------- LiteDB/DbEngine/Services/PageService.cs | 6 +- .../DbEngine/Services/TransactionService.cs | 8 ++- 6 files changed, 95 insertions(+), 71 deletions(-) diff --git a/LiteDB/DbEngine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs index eb77f4202..01a3fda2c 100644 --- a/LiteDB/DbEngine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -24,14 +24,14 @@ public IEnumerable GetCollectionNames() /// public bool DropCollection(string colName) { - // get collection page - var col = this.GetCollectionPage(colName, false); - - if(col == null) return false; + return this.Transaction(colName, false, (col) => + { + if(col == null) return false; - _collections.Drop(col); + _collections.Drop(col); - return true; + return true; + }); } /// diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index de1fd2a53..c48869294 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -49,7 +49,7 @@ public DbEngine(IDiskService disk) _indexer = new IndexService(_pager); _data = new DataService(_pager); _collections = new CollectionService(_pager, _indexer, _data); - _transaction = new TransactionService(_disk, _cache); + _transaction = new TransactionService(_disk, _pager, _cache); } #endregion diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index cfc6dd96c..446fc7578 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -102,7 +102,7 @@ public virtual void Clear() this.PageType = PageType.Empty; this.FreeBytes = PAGE_AVAILABLE_BYTES; this.ItemCount = 0; - this.DiskData = new byte[0]; + this.DiskData = new byte[BasePage.PAGE_SIZE]; } /// @@ -140,6 +140,42 @@ public T CopyTo() #region Read/Write page + /// + /// Read a page from byte array + /// + public void ReadPage(byte[] buffer) + { + var reader = new ByteReader(buffer); + + this.ReadHeader(reader); + + if (this.PageType != LiteDB.PageType.Empty) + { + this.ReadContent(reader); + } + + this.DiskData = buffer; + } + + /// + /// Write a page to byte array + /// + public byte[] WritePage() + { + var writer = new ByteWriter(BasePage.PAGE_SIZE); + + this.WriteHeader(writer); + + if (this.PageType != LiteDB.PageType.Empty) + { + this.WriteContent(writer); + } + + this.DiskData = writer.Buffer; + + return writer.Buffer; + } + public virtual void ReadHeader(ByteReader reader) { this.PageID = reader.ReadUInt32(); @@ -170,36 +206,6 @@ public virtual void WriteContent(ByteWriter writer) { } - public void ReadPage(byte[] buffer) - { - var reader = new ByteReader(buffer); - - this.ReadHeader(reader); - - if (this.PageType != LiteDB.PageType.Empty) - { - this.ReadContent(reader); - } - - this.DiskData = buffer; - } - - public byte[] WritePage() - { - var writer = new ByteWriter(BasePage.PAGE_SIZE); - - WriteHeader(writer); - - if (this.PageType != LiteDB.PageType.Empty) - { - WriteContent(writer); - } - - this.DiskData = writer.Buffer; - - return writer.Buffer; - } - #endregion } } diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 529321f89..13ba0f185 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -38,19 +38,21 @@ public T GetPage(uint pageID) // if a need a specific page but has a BasePage, returns null if (page != null && page.GetType() == typeof(BasePage) && typeof(T) != typeof(BasePage)) { - //if(page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); - - //var specificPage = new T(); - // - //specificPage.ReadPage(page.DiskData); - // - //lock(_cache) - //{ - // _cache[pageID] = specificPage; - //} - // - //return specificPage; return null; + + if (page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); + + var specificPage = new T(); + + specificPage.ReadPage(page.DiskData); + + lock(_cache) + { + _cache[pageID] = specificPage; + } + + return specificPage; + //return null; } return (T)page; @@ -60,7 +62,7 @@ public T GetPage(uint pageID) /// Add a page to cache. if this page is in cache, override (except if is basePage - in this case, copy header) /// If set is dirty, add in a second list /// - public void AddPage(BasePage page, bool dirty = false) + public void AddPage(BasePage page) { lock(_cache) { @@ -69,20 +71,30 @@ public void AddPage(BasePage page, bool dirty = false) { _cache[page.PageID] = page; } + } + } - // page is dirty? add in a special list too and mark as dirty (all type of pages) - if (dirty && !page.IsDirty) - { - page.IsDirty = true; - _dirty[page.PageID] = page; - //_cache[page.PageID] = page; + /// + /// Add a page as dirty page + /// + public void SetPageDirty(BasePage page) + { + _cache[page.PageID] = page; - // if page is new (not exits on datafile), there is no journal for them - if(page.DiskData.Length > 0) - { - // call action passing dirty page - used for journal file writes - MarkAsDirtyAction(page); - } + // if page already dirty, do nothing + if (page.IsDirty) return; + + lock(_dirty) + { + // mark as dirty and add to dirty list + page.IsDirty = true; + _dirty[page.PageID] = page; + + // if page is new (not exits on datafile), there is no journal for them + if (page.DiskData.Length > 0) + { + // call action passing dirty page - used for journal file writes + MarkAsDirtyAction(page); } } } @@ -115,14 +127,16 @@ public void ClearDirty() page.IsDirty = false; // remove all non-header-collection pages (this can be optional in future) - //if (page.PageType != PageType.Header && page.PageType != PageType.Collection) - if (page.PageType == PageType.Extend) + if (page.PageType == PageType.Extend || + page.PageType == PageType.Empty || + page.PageType == PageType.Index || + page.PageType == PageType.Data) { _cache.Remove(page.PageID); } } - //_cache.Clear(); + _cache.Clear(); _dirty.Clear(); } } diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 134714332..a6cf3f227 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -36,7 +36,7 @@ public T GetPage(uint pageID) page.ReadPage(buffer); - //Console.WriteLine("read " + pageID + " (" + typeof(T).Name + ")"); + Console.WriteLine("read disk " + pageID + " (" + typeof(T).Name + ")"); _cache.AddPage(page); } @@ -50,7 +50,7 @@ public T GetPage(uint pageID) /// public void SetDirty(BasePage page) { - _cache.AddPage(page, true); + _cache.SetPageDirty(page); } public HeaderPage Header { get { return this.GetPage(0); } } @@ -125,6 +125,8 @@ public void DeletePage(uint pageID, bool addSequence = false) { // update page to mark as completly empty page page.Clear(); + + // mark page as dirty this.SetDirty(page); // add to empty free list diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 563341c8d..7042cfa83 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -12,12 +12,14 @@ namespace LiteDB internal class TransactionService { private IDiskService _disk; + private PageService _pager; private CacheService _cache; private bool _trans = false; - internal TransactionService(IDiskService disk, CacheService cache) + internal TransactionService(IDiskService disk, PageService pager, CacheService cache) { _disk = disk; + _pager = pager; _cache = cache; _cache.MarkAsDirtyAction = (page) => _disk.WriteJournal(page.PageID, page.DiskData); } @@ -44,13 +46,13 @@ public void Commit() if (_cache.HasDirtyPages) { - var header = _cache.GetPage(0); + var header = _pager.GetPage(0); // increase file changeID (back to 0 when overflow) header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); // add header page as dirty to cache - _cache.AddPage(header, true); + _pager.SetDirty(header); // commit journal file - it will be used if write operation fails _disk.CommitJournal((header.LastPageID + 1) * BasePage.PAGE_SIZE); From d90ca740e5a7f2f55466b309c8f63920b2856e54 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 12 Nov 2015 18:40:30 -0200 Subject: [PATCH 53/91] New pages structure --- LiteDB/DbEngine/Disks/FileDiskService.cs | 34 +++--- LiteDB/DbEngine/Pages/BasePage.cs | 115 ++++++++---------- LiteDB/DbEngine/Pages/CollectionPage.cs | 23 +++- LiteDB/DbEngine/Pages/DataPage.cs | 23 ++-- LiteDB/DbEngine/Pages/EmptyPage.cs | 47 +++++++ LiteDB/DbEngine/Pages/ExtendPage.cs | 23 ++-- LiteDB/DbEngine/Pages/HeaderPage.cs | 22 +++- LiteDB/DbEngine/Pages/IndexPage.cs | 23 ++-- LiteDB/DbEngine/Services/CacheService.cs | 57 +++------ LiteDB/DbEngine/Services/CollectionService.cs | 18 ++- LiteDB/DbEngine/Services/PageService.cs | 55 +++++---- .../DbEngine/Services/TransactionService.cs | 6 +- LiteDB/LiteDB.csproj | 1 + 13 files changed, 246 insertions(+), 201 deletions(-) create mode 100644 LiteDB/DbEngine/Pages/EmptyPage.cs diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index 2d866837e..e9c3d58a1 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -159,7 +159,7 @@ public void WriteJournal(uint pageID, byte[] data) }); } - //Console.WriteLine("journal write " + pageID); + Console.WriteLine("journal write " + pageID); // just write original bytes in order that are changed _journal.Write(data, 0, BasePage.PAGE_SIZE); @@ -169,12 +169,15 @@ public void CommitJournal(long fileSize) { if (_journalEnabled == false) return; - // write a mark (byte 1) to know when journal is finish - // after that, if found a non-exclusive-open journal file, must be recovery - _journal.WriteByte(JOURNAL_FINISH_POSITION, 1); + if(_journal != null) + { + // write a mark (byte 1) to know when journal is finish + // after that, if found a non-exclusive-open journal file, must be recovery + _journal.WriteByte(JOURNAL_FINISH_POSITION, 1); - // flush all journal file data to disk - _journal.Flush(); + // flush all journal file data to disk + _journal.Flush(); + } // fileSize parameter tell me final size of data file - helpful to extend first datafile _stream.SetLength(fileSize); @@ -184,12 +187,15 @@ public void DeleteJournal() { if (_journalEnabled == false) return; - // close journal stream and delete file - _journal.Dispose(); - _journal = null; + if(_journal != null) + { + // close journal stream and delete file + _journal.Dispose(); + _journal = null; - // remove journal file - File.Delete(_journalFilename); + // remove journal file + File.Delete(_journalFilename); + } } public void Dispose() @@ -242,12 +248,12 @@ private void Recovery(FileStream journal) // if header, read all byte (to get original filesize) if(pageID == 0) { - var header = new HeaderPage(); - header.ReadPage(buffer); + var header = (HeaderPage)BasePage.ReadPage(buffer); + fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } - //Console.WriteLine("recovery " + pageID); + Console.WriteLine("recovery " + pageID); // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index 446fc7578..abdc875d4 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -8,7 +8,7 @@ namespace LiteDB { internal enum PageType { Empty = 0, Header = 1, Collection = 2, Index = 3, Data = 4, Extend = 5 } - internal class BasePage + internal abstract class BasePage { #region Page Constants @@ -45,9 +45,9 @@ internal class BasePage public uint NextPageID { get; set; } /// - /// Indicate the page type [1 byte] + /// Indicate the page type [1 byte] - Must be implemented for each page type /// - public PageType PageType { get; set; } + public abstract PageType PageType { get; } /// /// Used for all pages to count itens inside this page(bytes, nodes, blocks, ...) @@ -72,11 +72,11 @@ internal class BasePage /// public byte[] DiskData { get; private set; } - public BasePage() + public BasePage(uint pageID) { + this.PageID = pageID; this.PrevPageID = uint.MaxValue; this.NextPageID = uint.MaxValue; - this.PageType = LiteDB.PageType.Empty; this.ItemCount = 0; this.FreeBytes = PAGE_AVAILABLE_BYTES; this.DiskData = new byte[0]; @@ -86,75 +86,61 @@ public BasePage() /// Every page must imeplement this ItemCount + FreeBytes /// Must be called after Items are updates (insert/deletes) to keep variables ItemCount and FreeBytes synced /// - public virtual void UpdateItemCount() - { - this.ItemCount = 0; - this.FreeBytes = PAGE_AVAILABLE_BYTES; - } + public abstract void UpdateItemCount(); + + #region Read/Write page /// - /// Clear page content (using when delete a page) + /// Create a new instance of page based on T type /// - public virtual void Clear() + public static T CreateInstance(uint pageID) + where T : BasePage { - this.PrevPageID = uint.MaxValue; - this.NextPageID = uint.MaxValue; - this.PageType = PageType.Empty; - this.FreeBytes = PAGE_AVAILABLE_BYTES; - this.ItemCount = 0; - this.DiskData = new byte[BasePage.PAGE_SIZE]; + var type = typeof(T); + + // why I need cast to BasePage before cast to T?? if you know, please tell me :) + if (type == typeof(HeaderPage)) return (T)(BasePage)(new HeaderPage()); + if (type == typeof(CollectionPage)) return (T)(BasePage)(new CollectionPage(pageID)); + if (type == typeof(IndexPage)) return (T)(BasePage)(new IndexPage(pageID)); + if (type == typeof(DataPage)) return (T)(BasePage)(new DataPage(pageID)); + if (type == typeof(ExtendPage)) return (T)(BasePage)(new ExtendPage(pageID)); + if (type == typeof(EmptyPage)) return (T)(BasePage)(new EmptyPage(pageID)); + + throw new SystemException("Invalid base page type T"); } /// - /// Convert a BasePage to a specific page keeping same page header vars and re-loading disk content + /// Create a new instance of page based on PageType /// - public T CopyTo() - where T : BasePage, new() + public static BasePage CreateInstance(uint pageID, PageType pageType) { - if (this.DiskData.Length == 0) throw new SystemException("No diskdata in this page"); - - var page = new T(); - page.PageID = this.PageID; - page.PrevPageID = this.PrevPageID; - page.NextPageID = this.NextPageID; - // page.PageType = this.PageType; - page.ItemCount = this.ItemCount; - page.FreeBytes = this.FreeBytes; - page.IsDirty = this.IsDirty; - page.DiskData = new byte[BasePage.PAGE_SIZE]; - - Buffer.BlockCopy(this.DiskData, 0, page.DiskData, 0, BasePage.PAGE_SIZE); - - var reader = new ByteReader(this.DiskData); - - // skip header - i copyed from "this" instance (including possible changes) - reader.ReadBytes(BasePage.PAGE_HEADER_SIZE); - - if (page.PageType != LiteDB.PageType.Empty) + switch (pageType) { - this.ReadContent(reader); + case PageType.Header: return new HeaderPage(); + case PageType.Collection: return new CollectionPage(pageID); + case PageType.Index: return new IndexPage(pageID); + case PageType.Data: return new DataPage(pageID); + case PageType.Extend: return new ExtendPage(pageID); + case PageType.Empty: return new EmptyPage(pageID); + default: throw new SystemException("Invalid pageType"); } - - return page; } - #region Read/Write page - /// - /// Read a page from byte array + /// Read a page with correct instance page object /// - public void ReadPage(byte[] buffer) + public static BasePage ReadPage(byte[] buffer) { var reader = new ByteReader(buffer); - this.ReadHeader(reader); + var pageID = reader.ReadUInt32(); + var pageType = (PageType)reader.ReadByte(); + var page = CreateInstance(pageID, pageType); - if (this.PageType != LiteDB.PageType.Empty) - { - this.ReadContent(reader); - } + page.ReadHeader(reader); + page.ReadContent(reader); - this.DiskData = buffer; + return page; } /// @@ -176,35 +162,34 @@ public byte[] WritePage() return writer.Buffer; } - public virtual void ReadHeader(ByteReader reader) + private void ReadHeader(ByteReader reader) { - this.PageID = reader.ReadUInt32(); + // first 5 bytes (pageID + pageType) was readed before class create + // this.PageID + // this.PageType + this.PrevPageID = reader.ReadUInt32(); this.NextPageID = reader.ReadUInt32(); - this.PageType = (PageType)reader.ReadByte(); this.ItemCount = reader.ReadUInt16(); this.FreeBytes = reader.ReadUInt16(); reader.ReadBytes(3); // reserved 3 bytes } - public virtual void WriteHeader(ByteWriter writer) + private void WriteHeader(ByteWriter writer) { writer.Write(this.PageID); + writer.Write((byte)this.PageType); + writer.Write(this.PrevPageID); writer.Write(this.NextPageID); - writer.Write((byte)this.PageType); writer.Write((UInt16)this.ItemCount); writer.Write((UInt16)this.FreeBytes); writer.Write(new byte[3]); // reserved 3 bytes } - public virtual void ReadContent(ByteReader reader) - { - } + protected abstract void ReadContent(ByteReader reader); - public virtual void WriteContent(ByteWriter writer) - { - } + protected abstract void WriteContent(ByteWriter writer); #endregion } diff --git a/LiteDB/DbEngine/Pages/CollectionPage.cs b/LiteDB/DbEngine/Pages/CollectionPage.cs index 5a6831842..0f67edfe8 100644 --- a/LiteDB/DbEngine/Pages/CollectionPage.cs +++ b/LiteDB/DbEngine/Pages/CollectionPage.cs @@ -16,6 +16,11 @@ internal class CollectionPage : BasePage public static Regex NamePattern = new Regex(@"^[\w-]{1,30}$"); + /// + /// Page type = Collection + /// + public override PageType PageType { get { return PageType.Collection; } } + /// /// Name of collection /// @@ -37,10 +42,9 @@ internal class CollectionPage : BasePage /// public CollectionIndex[] Indexes { get; set; } - public CollectionPage() - : base() + public CollectionPage(uint pageID) + : base(pageID) { - this.PageType = PageType.Collection; this.FreeDataPageID = uint.MaxValue; this.DocumentCount = 0; this.ItemCount = 1; // fixed for CollectionPage @@ -53,9 +57,18 @@ public CollectionPage() } } + /// + /// Update freebytes + items count + /// + public override void UpdateItemCount() + { + this.ItemCount = 1; // fixed for CollectionPage + this.FreeBytes = 0; // no free bytes on collection-page - only one collection per page + } + #region Read/Write pages - public override void ReadContent(ByteReader reader) + protected override void ReadContent(ByteReader reader) { this.CollectionName = reader.ReadString(); this.FreeDataPageID = reader.ReadUInt32(); @@ -75,7 +88,7 @@ public override void ReadContent(ByteReader reader) } } - public override void WriteContent(ByteWriter writer) + protected override void WriteContent(ByteWriter writer) { writer.Write(this.CollectionName); writer.Write(this.FreeDataPageID); diff --git a/LiteDB/DbEngine/Pages/DataPage.cs b/LiteDB/DbEngine/Pages/DataPage.cs index 6b986e2de..963760ca4 100644 --- a/LiteDB/DbEngine/Pages/DataPage.cs +++ b/LiteDB/DbEngine/Pages/DataPage.cs @@ -11,6 +11,11 @@ namespace LiteDB /// internal class DataPage : BasePage { + /// + /// Page type = Extend + /// + public override PageType PageType { get { return PageType.Data; } } + /// /// If a Data Page has less that free space, it's considered full page for new items. Can be used only for update (DataPage) ~ 50% PAGE_AVAILABLE_BYTES /// This value is used for minimize @@ -22,19 +27,9 @@ internal class DataPage : BasePage /// public Dictionary DataBlocks { get; set; } - public DataPage() - : base() - { - this.PageType = PageType.Data; - this.DataBlocks = new Dictionary(); - } - - /// - /// Clear page content - dataBlocks - /// - public override void Clear() + public DataPage(uint pageID) + : base(pageID) { - base.Clear(); this.DataBlocks = new Dictionary(); } @@ -49,7 +44,7 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(ByteReader reader) + protected override void ReadContent(ByteReader reader) { this.DataBlocks = new Dictionary(ItemCount); @@ -73,7 +68,7 @@ public override void ReadContent(ByteReader reader) } } - public override void WriteContent(ByteWriter writer) + protected override void WriteContent(ByteWriter writer) { foreach (var block in this.DataBlocks.Values) { diff --git a/LiteDB/DbEngine/Pages/EmptyPage.cs b/LiteDB/DbEngine/Pages/EmptyPage.cs new file mode 100644 index 000000000..8e8f10b12 --- /dev/null +++ b/LiteDB/DbEngine/Pages/EmptyPage.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + /// + /// Represent a empty page (reused) + /// + internal class EmptyPage : BasePage + { + /// + /// Page type = Empty + /// + public override PageType PageType { get { return PageType.Empty; } } + + public EmptyPage(uint pageID) + : base(pageID) + { + this.ItemCount = 0; + this.FreeBytes = PAGE_AVAILABLE_BYTES; + } + + /// + /// Update freebytes + items count + /// + public override void UpdateItemCount() + { + this.ItemCount = 0; + this.FreeBytes = PAGE_AVAILABLE_BYTES; + } + + #region Read/Write pages + + protected override void ReadContent(ByteReader reader) + { + } + + protected override void WriteContent(ByteWriter writer) + { + } + + #endregion + } +} diff --git a/LiteDB/DbEngine/Pages/ExtendPage.cs b/LiteDB/DbEngine/Pages/ExtendPage.cs index 192d995a9..8be02392f 100644 --- a/LiteDB/DbEngine/Pages/ExtendPage.cs +++ b/LiteDB/DbEngine/Pages/ExtendPage.cs @@ -13,23 +13,18 @@ namespace LiteDB internal class ExtendPage : BasePage { /// - /// Represent the part or full of the object - if this page has NextPageID the object is bigger than this page + /// Page type = Extend /// - public Byte[] Data { get; set; } - - public ExtendPage() - : base() - { - this.PageType = PageType.Extend; - this.Data = new byte[0]; - } + public override PageType PageType { get { return PageType.Extend; } } /// - /// Clear page content - Data byte array + /// Represent the part or full of the object - if this page has NextPageID the object is bigger than this page /// - public override void Clear() + public Byte[] Data { get; set; } + + public ExtendPage(uint pageID) + : base(pageID) { - base.Clear(); this.Data = new byte[0]; } @@ -44,12 +39,12 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(ByteReader reader) + protected override void ReadContent(ByteReader reader) { this.Data = reader.ReadBytes(this.ItemCount); } - public override void WriteContent(ByteWriter writer) + protected override void WriteContent(ByteWriter writer) { writer.Write(this.Data); } diff --git a/LiteDB/DbEngine/Pages/HeaderPage.cs b/LiteDB/DbEngine/Pages/HeaderPage.cs index c67ce2c2e..ecd17b53d 100644 --- a/LiteDB/DbEngine/Pages/HeaderPage.cs +++ b/LiteDB/DbEngine/Pages/HeaderPage.cs @@ -8,6 +8,11 @@ namespace LiteDB { internal class HeaderPage : BasePage { + /// + /// Page type = Header + /// + public override PageType PageType { get { return PageType.Header; } } + /// /// ChangeID in file position (can be calc?) /// @@ -44,10 +49,8 @@ internal class HeaderPage : BasePage public uint FirstCollectionPageID; public HeaderPage() - : base() + : base(0) { - this.PageID = 0; - this.PageType = PageType.Header; this.FreeEmptyPageID = uint.MaxValue; this.FirstCollectionPageID = uint.MaxValue; this.ChangeID = 0; @@ -56,9 +59,18 @@ public HeaderPage() this.FreeBytes = 0; // no free bytes on header } + /// + /// Update freebytes + items count + /// + public override void UpdateItemCount() + { + this.ItemCount = 1; // fixed for header + this.FreeBytes = 0; // no free bytes on header + } + #region Read/Write pages - public override void ReadContent(ByteReader reader) + protected override void ReadContent(ByteReader reader) { var info = reader.ReadString(); var ver = reader.ReadByte(); @@ -71,7 +83,7 @@ public override void ReadContent(ByteReader reader) if (ver != FILE_VERSION) throw LiteException.InvalidDatabaseVersion(ver); } - public override void WriteContent(ByteWriter writer) + protected override void WriteContent(ByteWriter writer) { writer.Write(HEADER_INFO); writer.Write(FILE_VERSION); diff --git a/LiteDB/DbEngine/Pages/IndexPage.cs b/LiteDB/DbEngine/Pages/IndexPage.cs index db8b28832..6df54101a 100644 --- a/LiteDB/DbEngine/Pages/IndexPage.cs +++ b/LiteDB/DbEngine/Pages/IndexPage.cs @@ -8,6 +8,11 @@ namespace LiteDB { internal class IndexPage : BasePage { + /// + /// Page type = Index + /// + public override PageType PageType { get { return PageType.Index; } } + /// /// If a Index Page has less that this free space, it's considered full page for new items. /// @@ -15,19 +20,9 @@ internal class IndexPage : BasePage public Dictionary Nodes { get; set; } - public IndexPage() - : base() - { - this.PageType = PageType.Index; - this.Nodes = new Dictionary(); - } - - /// - /// Clear page content - nodes - /// - public override void Clear() + public IndexPage(uint pageID) + : base(pageID) { - base.Clear(); this.Nodes = new Dictionary(); } @@ -42,7 +37,7 @@ public override void UpdateItemCount() #region Read/Write pages - public override void ReadContent(ByteReader reader) + protected override void ReadContent(ByteReader reader) { this.Nodes = new Dictionary(this.ItemCount); @@ -69,7 +64,7 @@ public override void ReadContent(ByteReader reader) } } - public override void WriteContent(ByteWriter writer) + protected override void WriteContent(ByteWriter writer) { foreach (var node in this.Nodes.Values) { diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 13ba0f185..72652f523 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -27,47 +27,23 @@ public CacheService() } /// - /// Get a page inside cache system. Returns null if page not existis. - /// If T is more specific than page that I have in cache, returns null (eg. Page 2 is BasePage in cache and this method call for IndexPage PageId 2) + /// Get a page in my cache, first check if is in my dirty list. If not, check in my cache list. Returns null if not found /// public T GetPage(uint pageID) - where T : BasePage, new() + where T : BasePage { - var page = _cache.GetOrDefault(pageID, null); - - // if a need a specific page but has a BasePage, returns null - if (page != null && page.GetType() == typeof(BasePage) && typeof(T) != typeof(BasePage)) - { - return null; - - if (page.IsDirty) throw new SystemException("Can convert page - page is dirty?"); - - var specificPage = new T(); - - specificPage.ReadPage(page.DiskData); - - lock(_cache) - { - _cache[pageID] = specificPage; - } - - return specificPage; - //return null; - } - - return (T)page; + return (T)_dirty.GetOrDefault(pageID, null) ?? (T)_cache.GetOrDefault(pageID, null); } /// - /// Add a page to cache. if this page is in cache, override (except if is basePage - in this case, copy header) - /// If set is dirty, add in a second list + /// Add a page to cache. if this page is in cache, override /// public void AddPage(BasePage page) { lock(_cache) { // do not cache extend page - never will be reused - //if (page.PageType != PageType.Extend) + if (page.PageType != PageType.Extend) { _cache[page.PageID] = page; } @@ -79,17 +55,22 @@ public void AddPage(BasePage page) /// public void SetPageDirty(BasePage page) { - _cache[page.PageID] = page; - - // if page already dirty, do nothing - if (page.IsDirty) return; - lock(_dirty) { - // mark as dirty and add to dirty list - page.IsDirty = true; + // add page to my dirty list + cache list + //_cache[page.PageID] = page; _dirty[page.PageID] = page; + if(page.PageID == 0 && page.PageType != PageType.Header) + { + Console.WriteLine("wait"); + } + + // if page already dirty + if (page.IsDirty) return; + + page.IsDirty = true; + // if page is new (not exits on datafile), there is no journal for them if (page.DiskData.Length > 0) { @@ -122,11 +103,11 @@ public void ClearDirty() { lock(_cache) { - foreach(var page in _dirty.Values) + foreach (var page in _dirty.Values) { page.IsDirty = false; - // remove all non-header-collection pages (this can be optional in future) + // remove all non-header/collection pages (this can be optional in future) if (page.PageType == PageType.Extend || page.PageType == PageType.Empty || page.PageType == PageType.Index || diff --git a/LiteDB/DbEngine/Services/CollectionService.cs b/LiteDB/DbEngine/Services/CollectionService.cs index b2a08bf5e..3b0ff83a0 100644 --- a/LiteDB/DbEngine/Services/CollectionService.cs +++ b/LiteDB/DbEngine/Services/CollectionService.cs @@ -27,9 +27,11 @@ public CollectionPage Get(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); - if (_pager.Header.FirstCollectionPageID == uint.MaxValue) return null; + var header = _pager.GetPage(0); - var pages = _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); + if (header.FirstCollectionPageID == uint.MaxValue) return null; + + var pages = _pager.GetSeqPages(header.FirstCollectionPageID); var col = pages.FirstOrDefault(x => x.CollectionName.Equals(name, StringComparison.InvariantCultureIgnoreCase)); @@ -44,8 +46,10 @@ public CollectionPage Add(string name) if(string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); if(!CollectionPage.NamePattern.IsMatch(name)) throw LiteException.InvalidFormat("CollectionName", name); + var header = _pager.GetPage(0); + // test collection limit - var pages = _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); + var pages = _pager.GetSeqPages(header.FirstCollectionPageID); if (pages.Count() >= CollectionPage.MAX_COLLECTIONS) { @@ -55,7 +59,7 @@ public CollectionPage Add(string name) var col = _pager.NewPage(); // add page in collection list - _pager.AddOrRemoveToFreeList(true, col, _pager.Header, ref _pager.Header.FirstCollectionPageID); + _pager.AddOrRemoveToFreeList(true, col, header, ref header.FirstCollectionPageID); col.CollectionName = name; _pager.SetDirty(col); @@ -74,7 +78,7 @@ public CollectionPage Add(string name) /// public IEnumerable GetAll() { - return _pager.GetSeqPages(_pager.Header.FirstCollectionPageID); + return _pager.GetSeqPages(_pager.GetPage(0).FirstCollectionPageID); } /// @@ -118,8 +122,10 @@ public void Drop(CollectionPage col) _pager.DeletePage(pageID); } + var header = _pager.GetPage(0); + // remove page from collection list - _pager.AddOrRemoveToFreeList(false, col, _pager.Header, ref _pager.Header.FirstCollectionPageID); + _pager.AddOrRemoveToFreeList(false, col, header, ref header.FirstCollectionPageID); _pager.DeletePage(col.PageID, false); } diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index a6cf3f227..95bb0bb64 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -22,21 +22,25 @@ public PageService(IDiskService disk, CacheService cache) /// Get a page from cache or from disk (and put on cache) /// public T GetPage(uint pageID) - where T : BasePage, new() + where T : BasePage { + if(pageID == uint.MaxValue) + { + Console.Write("wait"); + } + + var page = _cache.GetPage(pageID); if (page == null) { - page = new T(); - lock(_disk) { var buffer = _disk.ReadPage(pageID); - page.ReadPage(buffer); + page = (T)BasePage.ReadPage(buffer); - Console.WriteLine("read disk " + pageID + " (" + typeof(T).Name + ")"); + Console.WriteLine("read page " + pageID + " (" + page.PageType + ")"); _cache.AddPage(page); } @@ -53,13 +57,11 @@ public void SetDirty(BasePage page) _cache.SetPageDirty(page); } - public HeaderPage Header { get { return this.GetPage(0); } } - /// /// Read all sequences pages from a start pageID (using NextPageID) /// public IEnumerable GetSeqPages(uint firstPageID) - where T : BasePage, new() + where T : BasePage { var pageID = firstPageID; @@ -77,25 +79,28 @@ public IEnumerable GetSeqPages(uint firstPageID) /// Get a new empty page - can be a reused page (EmptyPage) or a clean one (extend datafile) /// public T NewPage(BasePage prevPage = null) - where T : BasePage, new() + where T : BasePage { - var page = new T(); + var header = this.GetPage(0); + var pageID = (uint)0; // try get page from Empty free list - if(this.Header.FreeEmptyPageID != uint.MaxValue) + if(header.FreeEmptyPageID != uint.MaxValue) { - var free = this.GetPage(this.Header.FreeEmptyPageID); + var free = this.GetPage(header.FreeEmptyPageID); // remove page from empty list - this.AddOrRemoveToFreeList(false, free, this.Header, ref this.Header.FreeEmptyPageID); + this.AddOrRemoveToFreeList(false, free, header, ref header.FreeEmptyPageID); - page.PageID = free.PageID; + pageID = free.PageID; } else { - page.PageID = ++this.Header.LastPageID; + pageID = ++header.LastPageID; } + var page = BasePage.CreateInstance(pageID); + // if there a page before, just fix NextPageID pointer if (prevPage != null) { @@ -106,9 +111,7 @@ public T NewPage(BasePage prevPage = null) // mark header and this new page as dirty, and then add to cache this.SetDirty(page); - this.SetDirty(this.Header); - - _cache.AddPage(page); + this.SetDirty(header); return page; } @@ -118,19 +121,23 @@ public T NewPage(BasePage prevPage = null) /// public void DeletePage(uint pageID, bool addSequence = false) { + // get all pages in sequence or a single one var pages = addSequence ? this.GetSeqPages(pageID).ToArray() : new BasePage[] { this.GetPage(pageID) }; - // Adding all pages to FreeList + // get my header page + var header = this.GetPage(0); + + // adding all pages to FreeList foreach (var page in pages) { - // update page to mark as completly empty page - page.Clear(); + // create a new empty page based on a normal page + var empty = new EmptyPage(page.PageID); // mark page as dirty - this.SetDirty(page); + this.SetDirty(empty); // add to empty free list - this.AddOrRemoveToFreeList(true, page, this.Header, ref this.Header.FreeEmptyPageID); + this.AddOrRemoveToFreeList(true, empty, header, ref header.FreeEmptyPageID); } } @@ -138,7 +145,7 @@ public void DeletePage(uint pageID, bool addSequence = false) /// Returns a page that contains space enouth to data to insert new object - if not exits, create a new Page /// public T GetFreePage(uint startPageID, int size) - where T : BasePage, new() + where T : BasePage { if(startPageID != uint.MaxValue) { diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 7042cfa83..5c8b246a2 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -60,7 +60,9 @@ public void Commit() // write all dirty pages in data file foreach (var page in _cache.GetDirtyPages()) { - //Console.WriteLine("save page " + page.PageID); + page.UpdateItemCount(); + + Console.WriteLine("write page " + page.PageID + " (" + page.PageType + ")"); _disk.WritePage(page.PageID, page.WritePage()); } @@ -115,7 +117,7 @@ public bool AvoidDirtyRead() // if changeID was changed, file was changed by another process - clear all cache if (cache.ChangeID != change) { - //Console.WriteLine("Datafile changed, clear cache"); + Console.WriteLine("Datafile changed, clear cache"); _cache.Clear(); return true; } diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 6ab9a0706..e39528dcb 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -60,6 +60,7 @@ + From 90ac3a914fe79df431f9566b07de4ad77e8853c1 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 13 Nov 2015 11:01:51 -0200 Subject: [PATCH 54/91] Getting crazy with this bug --- LiteDB/Core/LiteDatabase.cs | 14 ++++ LiteDB/DbEngine/Actions/Collection.cs | 4 +- LiteDB/DbEngine/Actions/Delete.cs | 2 +- .../{DumpDatabase.cs => Actions/Dump.cs} | 66 ++++++++----------- LiteDB/DbEngine/DbEngine.cs | 35 ++-------- LiteDB/DbEngine/Pages/BasePage.cs | 14 ++-- LiteDB/DbEngine/Services/PageService.cs | 1 - LiteDB/LiteDB.csproj | 1 + LiteDB/Shell/Commands/Others/Dump.cs | 26 +++----- 9 files changed, 67 insertions(+), 96 deletions(-) rename LiteDB/DbEngine/{DumpDatabase.cs => Actions/Dump.cs} (76%) diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 8ee307311..71e1ef077 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -134,6 +134,20 @@ public BsonValue RunCommand(string command) #endregion + #region Dump + + internal string DumpPages() + { + return _engine.DumpPages().ToString(); + } + + internal string DumpIndex(string colName, string field) + { + return _engine.DumpIndex(colName, field).ToString(); + } + + #endregion + public void Dispose() { _engine.Dispose(); diff --git a/LiteDB/DbEngine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs index 01a3fda2c..670204fd5 100644 --- a/LiteDB/DbEngine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -45,10 +45,8 @@ public bool RenameCollection(string colName, string newName) // change collection name and save col.CollectionName = newName; - _pager.SetDirty(col); - // remove from collection cache - _collectionPages.Remove(colName); + _pager.SetDirty(col); return true; }); diff --git a/LiteDB/DbEngine/Actions/Delete.cs b/LiteDB/DbEngine/Actions/Delete.cs index c5793cd6d..fa1acb055 100644 --- a/LiteDB/DbEngine/Actions/Delete.cs +++ b/LiteDB/DbEngine/Actions/Delete.cs @@ -22,7 +22,7 @@ public int DeleteDocuments(string colName, Query query) var count = 0; // find nodes - var nodes = query.Run(col, _indexer); + var nodes = query.Run(col, _indexer).ToArray(); foreach (var node in nodes) { diff --git a/LiteDB/DbEngine/DumpDatabase.cs b/LiteDB/DbEngine/Actions/Dump.cs similarity index 76% rename from LiteDB/DbEngine/DumpDatabase.cs rename to LiteDB/DbEngine/Actions/Dump.cs index ed933be96..92d2f3b9d 100644 --- a/LiteDB/DbEngine/DumpDatabase.cs +++ b/LiteDB/DbEngine/Actions/Dump.cs @@ -7,22 +7,23 @@ namespace LiteDB { - /// - /// A debugger class to show how pages are storaged. Used to debug pages in shell/tests - /// - internal class DumpDatabase + internal partial class DbEngine : IDisposable { - public static StringBuilder Pages(LiteDatabase db, bool mem) + /// + /// Dump all pages into a string - debug purpose only + /// + public StringBuilder DumpPages() { var sb = new StringBuilder(); - sb.AppendLine("Dump - " + (mem ? "Cache/Disk" : "Only Disk")); + sb.AppendLine("Dump database"); + sb.AppendLine("============="); + sb.AppendLine(); - for (uint i = 0; i <= db.Pager.GetPage(0).LastPageID; i++) + for (uint i = 0; i <= _pager.GetPage(0).LastPageID; i++) { - var p = i == 0 ? - ReadPage(db, i, mem) : - ReadPage(db, i, mem); + var buffer = _disk.ReadPage(i); + var p = BasePage.ReadPage(buffer); sb.AppendFormat("{0} <{1},{2}> [{3}] {4}{5} | ", p.PageID.Dump(), @@ -32,11 +33,6 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) p.FreeBytes.ToString("0000"), p.IsDirty ? "d" : " "); - if (p.PageType == PageType.Collection) p = ReadPage(db, i, mem); - if (p.PageType == PageType.Data) p = ReadPage(db, i, mem); - if (p.PageType == PageType.Extend) p = ReadPage(db, i, mem); - if (p.PageType == PageType.Index) p = ReadPage(db, i, mem); - p.Dump(sb); sb.AppendLine(); } @@ -44,28 +40,14 @@ public static StringBuilder Pages(LiteDatabase db, bool mem) return sb; } - private static T ReadPage(LiteDatabase db, uint pageID, bool mem) - where T : BasePage, new() - { - if (mem && pageID == 0) return (T)(object)db.Cache.GetPage(0); - - if(mem) - { - return db.Pager.GetPage(pageID); - } - else - { - var page = new T(); - page.ReadPage(db.Disk.ReadPage(pageID)); - return page; - } - } - - public static StringBuilder Index(LiteDatabase db, string collection, string field, int size = 5) + /// + /// Dump skip list to a human reable format - debug purpose only + /// + public StringBuilder DumpIndex(string colName, string field, int size = 5) { var sbs = new StringBuilder[IndexNode.MAX_LEVEL_LENGTH + 1]; - var col = db.GetCollection(collection).GetCollectionPage(false); + var col = this.GetCollectionPage(colName, false); if (col == null) throw new ArgumentException("Invalid collection name"); var index = col.GetIndex(field); @@ -80,7 +62,7 @@ public static StringBuilder Index(LiteDatabase db, string collection, string fie while (!cur.IsEmpty) { - var page = db.Pager.GetPage(cur.PageID); + var page = _pager.GetPage(cur.PageID); var node = page.Nodes[cur.Index]; sbs[0].Append((Limit(node.Key.ToString(), size)).PadBoth(1 + (2 * size))); @@ -95,13 +77,13 @@ public static StringBuilder Index(LiteDatabase db, string collection, string fie { if (!node.Prev[i].IsEmpty) { - var pprev = db.Pager.GetPage(node.Prev[i].PageID); + var pprev = _pager.GetPage(node.Prev[i].PageID); var pnode = pprev.Nodes[node.Prev[i].Index]; p = pnode.Key.ToString(); } if (!node.Next[i].IsEmpty) { - var pnext = db.Pager.GetPage(node.Next[i].PageID); + var pnext = _pager.GetPage(node.Next[i].PageID); var pnode = pnext.Nodes[node.Next[i].Index]; n = pnode.Key.ToString(); } @@ -114,6 +96,9 @@ public static StringBuilder Index(LiteDatabase db, string collection, string fie } var s = new StringBuilder(); + s.AppendFormat("Dump index {0}.{1}\n", col, field); + s.AppendLine("=============================="); + s.AppendLine(); for (var i = sbs.Length - 1; i >= 0; i--) { @@ -128,7 +113,6 @@ private static string Limit(string text, int size) if (string.IsNullOrEmpty(text)) return ""; return text.Length > size ? text.Substring(0, size) : text; } - } #region Dump Extensions @@ -162,6 +146,7 @@ public static void Dump(this BasePage page, StringBuilder sb) if (page is IndexPage) Dump((IndexPage)page, sb); if (page is DataPage) Dump((DataPage)page, sb); if (page is ExtendPage) Dump((ExtendPage)page, sb); + if (page is EmptyPage) Dump((EmptyPage)page, sb); } public static void Dump(this HeaderPage page, StringBuilder sb) @@ -217,6 +202,11 @@ public static void Dump(this ExtendPage page, StringBuilder sb) sb.AppendFormat("BytesUsed: {0}", page.Data.Length); } + public static void Dump(this EmptyPage page, StringBuilder sb) + { + sb.AppendFormat("(empty)"); + } + public static string PadBoth(this string str, int length) { int spaces = length - str.Length; diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index c48869294..29399301d 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -29,8 +29,6 @@ internal partial class DbEngine : IDisposable private CollectionService _collections; - private Dictionary _collectionPages = new Dictionary(); - private object _locker = new object(); public DbEngine(IDiskService disk) @@ -59,38 +57,15 @@ public DbEngine(IDiskService disk) /// private CollectionPage GetCollectionPage(string name, bool addIfNotExits) { - uint pageID; + // search my page on collection service + var col = _collections.Get(name); - // read if data file was changed by another process - if changed, reset my collection pageId cache - if(_transaction.AvoidDirtyRead()) + if(col == null && addIfNotExits) { - _collectionPages = new Dictionary(); - } - - // check if pageID is in my dictionary - if(_collectionPages.TryGetValue(name, out pageID)) - { - return _pager.GetPage(pageID); - } - else - { - // search my page on collection service - var col = _collections.Get(name); - - if(col == null && addIfNotExits) - { - col = _collections.Add(name); - } - - if(col != null) - { - _collectionPages.Add(name, col.PageID); - - return _pager.GetPage(col.PageID); - } + col = _collections.Add(name); } - return null; + return col; } /// diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index abdc875d4..b297fabf4 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -34,6 +34,11 @@ internal abstract class BasePage /// public uint PageID { get; set; } + /// + /// Indicate the page type [1 byte] - Must be implemented for each page type + /// + public abstract PageType PageType { get; } + /// /// Represent the previous page. Used for page-sequences - MaxValue represent that has NO previous page [4 bytes] /// @@ -45,18 +50,13 @@ internal abstract class BasePage public uint NextPageID { get; set; } /// - /// Indicate the page type [1 byte] - Must be implemented for each page type - /// - public abstract PageType PageType { get; } - - /// - /// Used for all pages to count itens inside this page(bytes, nodes, blocks, ...) + /// Used for all pages to count itens inside this page(bytes, nodes, blocks, ...) [2 bytes] /// Its Int32 but writes in UInt16 /// public int ItemCount { get; set; } /// - /// Used to find a free page using only header search [used in FreeList] + /// Used to find a free page using only header search [used in FreeList] [2 bytes] /// Its Int32 but writes in UInt16 /// Its updated when a page modify content length (add/remove items) /// diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 95bb0bb64..d2bd396c8 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -29,7 +29,6 @@ public T GetPage(uint pageID) Console.Write("wait"); } - var page = _cache.GetPage(pageID); if (page == null) diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index e39528dcb..66e7a4d2b 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -51,6 +51,7 @@ + diff --git a/LiteDB/Shell/Commands/Others/Dump.cs b/LiteDB/Shell/Commands/Others/Dump.cs index 23cb48aee..f777554bd 100644 --- a/LiteDB/Shell/Commands/Others/Dump.cs +++ b/LiteDB/Shell/Commands/Others/Dump.cs @@ -15,23 +15,17 @@ public bool IsCommand(StringScanner s) public BsonValue Execute(LiteDatabase db, StringScanner s) { - var result = new StringBuilder(); + if (s.HasTerminated) + { + return db.DumpPages(); + } + else + { + var col = s.Scan(@"[\w-]+"); + var field = s.Scan(@"\s+\w+").Trim(); - //if (s.HasTerminated || s.Match("mem$")) - //{ - // var mem = s.Match("mem$"); - - // result = DumpDatabase.Pages(db, mem); - //} - //else - //{ - // var col = s.Scan(@"[\w-]+"); - // var field = s.Scan(@"\s+\w+").Trim(); - - // result = DumpDatabase.Index(db, col, field); - //} - - return result.ToString(); + return db.DumpIndex(col, field); + } } } } From 8849d662b3f4e69f02435d64a673dfea9b3b6386 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 13 Nov 2015 14:54:57 -0200 Subject: [PATCH 55/91] Adding lock file to all file length --- LiteDB.Shell/Commands/Spool.cs | 2 +- LiteDB/DbEngine/Disks/FileDiskService.cs | 15 ++++++--------- LiteDB/DbEngine/Pages/BasePage.cs | 2 +- LiteDB/DbEngine/Pages/EmptyPage.cs | 15 +++++++++++++++ LiteDB/DbEngine/Services/PageService.cs | 2 +- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/LiteDB.Shell/Commands/Spool.cs b/LiteDB.Shell/Commands/Spool.cs index 01e354f12..51d1b593c 100644 --- a/LiteDB.Shell/Commands/Spool.cs +++ b/LiteDB.Shell/Commands/Spool.cs @@ -30,7 +30,7 @@ public override void Execute(LiteShell shell, StringScanner s, Display display, { if (shell.Database == null) throw LiteException.NoDatabase(); - var path = Path.GetFullPath(string.Format("LiteDB-spool-{1:yyyy-MM-dd-HH-mm}.txt", DateTime.Now)); + var path = Path.GetFullPath(string.Format("LiteDB-spool-{0:yyyy-MM-dd-HH-mm}.txt", DateTime.Now)); _writer = File.CreateText(path); diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index e9c3d58a1..e52b7807a 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -10,11 +10,6 @@ namespace LiteDB { internal class FileDiskService : IDiskService { - /// - /// Lock data file position - /// - private const int LOCK_POSITION = 0; - /// /// Position on disk to write a mark to know when journal is finish and valid (byte 19 is free header area) /// @@ -22,6 +17,7 @@ internal class FileDiskService : IDiskService private FileStream _stream; private string _filename; + private long _lockLength; private FileStream _journal; private string _journalFilename; @@ -74,9 +70,10 @@ public bool Initialize() /// public void Lock() { - this.TryExec(() => - _stream.Lock(LOCK_POSITION, 1) - ); + this.TryExec(() => { + _lockLength = _stream.Length; + _stream.Lock(0, _lockLength); + }); } /// @@ -84,7 +81,7 @@ public void Lock() /// public void Unlock() { - _stream.Unlock(LOCK_POSITION, 1); + _stream.Unlock(0, _lockLength); } #endregion diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index b297fabf4..07d50b8fd 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -70,7 +70,7 @@ internal abstract class BasePage /// /// This is the data when read first from disk - used to journal operations (IDiskService only will use) /// - public byte[] DiskData { get; private set; } + public byte[] DiskData { get; protected set; } public BasePage(uint pageID) { diff --git a/LiteDB/DbEngine/Pages/EmptyPage.cs b/LiteDB/DbEngine/Pages/EmptyPage.cs index 8e8f10b12..9a33bce46 100644 --- a/LiteDB/DbEngine/Pages/EmptyPage.cs +++ b/LiteDB/DbEngine/Pages/EmptyPage.cs @@ -23,6 +23,21 @@ public EmptyPage(uint pageID) this.FreeBytes = PAGE_AVAILABLE_BYTES; } + public EmptyPage(BasePage page) + : this(page.PageID) + { + // copy prev/next links + this.NextPageID = page.NextPageID; + this.PrevPageID = page.PrevPageID; + + // if page is not dirty but it´s changing to empty, lets copy disk content to add in journal + if(!page.IsDirty) + { + this.DiskData = new byte[BasePage.PAGE_SIZE]; + Buffer.BlockCopy(page.DiskData, 0, this.DiskData, 0, BasePage.PAGE_SIZE); + } + } + /// /// Update freebytes + items count /// diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index d2bd396c8..9c90f8e65 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -130,7 +130,7 @@ public void DeletePage(uint pageID, bool addSequence = false) foreach (var page in pages) { // create a new empty page based on a normal page - var empty = new EmptyPage(page.PageID); + var empty = new EmptyPage(page); // mark page as dirty this.SetDirty(empty); From b8f0e72f77600a9fdff7b11d36880f954fc9f9d6 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 13 Nov 2015 16:06:01 -0200 Subject: [PATCH 56/91] Fix delete bug :) --- LiteDB-TODO.txt | 3 ++- LiteDB/Core/LiteDatabase.cs | 4 ++-- LiteDB/DbEngine/Actions/Delete.cs | 2 +- LiteDB/DbEngine/Actions/Dump.cs | 11 +++++++---- LiteDB/DbEngine/Pages/EmptyPage.cs | 6 +----- LiteDB/DbEngine/Services/CacheService.cs | 14 +++----------- LiteDB/Shell/Commands/Others/Dump.cs | 10 ++++++++-- 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 6c9ea95b8..d490a3f6b 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,9 +1,10 @@ # LiteDB v.2 - TODO - Revisar help do Shell - saiu varias coisas -- Mapper de indice está fora do ar +- Mapper de indice está fora do ar - "indexesDef" => IDictionary passar no methodo Find - Implementar novamente o BulkInsert - Thread Safe + - Thread safe no BsonMapper static methods (Reflection) - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 71e1ef077..c52b6ad41 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -136,9 +136,9 @@ public BsonValue RunCommand(string command) #region Dump - internal string DumpPages() + internal string DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) { - return _engine.DumpPages().ToString(); + return _engine.DumpPages(startPage, endPage).ToString(); } internal string DumpIndex(string colName, string field) diff --git a/LiteDB/DbEngine/Actions/Delete.cs b/LiteDB/DbEngine/Actions/Delete.cs index fa1acb055..c5793cd6d 100644 --- a/LiteDB/DbEngine/Actions/Delete.cs +++ b/LiteDB/DbEngine/Actions/Delete.cs @@ -22,7 +22,7 @@ public int DeleteDocuments(string colName, Query query) var count = 0; // find nodes - var nodes = query.Run(col, _indexer).ToArray(); + var nodes = query.Run(col, _indexer); foreach (var node in nodes) { diff --git a/LiteDB/DbEngine/Actions/Dump.cs b/LiteDB/DbEngine/Actions/Dump.cs index 92d2f3b9d..39b327bcd 100644 --- a/LiteDB/DbEngine/Actions/Dump.cs +++ b/LiteDB/DbEngine/Actions/Dump.cs @@ -12,7 +12,7 @@ internal partial class DbEngine : IDisposable /// /// Dump all pages into a string - debug purpose only /// - public StringBuilder DumpPages() + public StringBuilder DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) { var sb = new StringBuilder(); @@ -20,10 +20,13 @@ public StringBuilder DumpPages() sb.AppendLine("============="); sb.AppendLine(); - for (uint i = 0; i <= _pager.GetPage(0).LastPageID; i++) + var header = (HeaderPage)BasePage.ReadPage(_disk.ReadPage(0)); + + for (uint i = startPage; i <= endPage; i++) { - var buffer = _disk.ReadPage(i); - var p = BasePage.ReadPage(buffer); + if(i > header.LastPageID) break; + + var p = BasePage.ReadPage(_disk.ReadPage(i)); sb.AppendFormat("{0} <{1},{2}> [{3}] {4}{5} | ", p.PageID.Dump(), diff --git a/LiteDB/DbEngine/Pages/EmptyPage.cs b/LiteDB/DbEngine/Pages/EmptyPage.cs index 9a33bce46..72d174941 100644 --- a/LiteDB/DbEngine/Pages/EmptyPage.cs +++ b/LiteDB/DbEngine/Pages/EmptyPage.cs @@ -26,12 +26,8 @@ public EmptyPage(uint pageID) public EmptyPage(BasePage page) : this(page.PageID) { - // copy prev/next links - this.NextPageID = page.NextPageID; - this.PrevPageID = page.PrevPageID; - // if page is not dirty but it´s changing to empty, lets copy disk content to add in journal - if(!page.IsDirty) + if(!page.IsDirty && page.DiskData.Length > 0) { this.DiskData = new byte[BasePage.PAGE_SIZE]; Buffer.BlockCopy(page.DiskData, 0, this.DiskData, 0, BasePage.PAGE_SIZE); diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 72652f523..6b9ecacad 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -57,18 +57,11 @@ public void SetPageDirty(BasePage page) { lock(_dirty) { - // add page to my dirty list + cache list - //_cache[page.PageID] = page; - _dirty[page.PageID] = page; - - if(page.PageID == 0 && page.PageType != PageType.Header) - { - Console.WriteLine("wait"); - } - - // if page already dirty + // if page already dirty do nothing, not even add in _dirty dictionary (page type can be changed when delete a page) if (page.IsDirty) return; + _dirty[page.PageID] = page; + page.IsDirty = true; // if page is new (not exits on datafile), there is no journal for them @@ -117,7 +110,6 @@ public void ClearDirty() } } - _cache.Clear(); _dirty.Clear(); } } diff --git a/LiteDB/Shell/Commands/Others/Dump.cs b/LiteDB/Shell/Commands/Others/Dump.cs index f777554bd..3f3dbcd65 100644 --- a/LiteDB/Shell/Commands/Others/Dump.cs +++ b/LiteDB/Shell/Commands/Others/Dump.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace LiteDB.Shell.Commands { @@ -15,9 +16,14 @@ public bool IsCommand(StringScanner s) public BsonValue Execute(LiteDatabase db, StringScanner s) { - if (s.HasTerminated) + if (s.HasTerminated || s.Match(@"\d+")) { - return db.DumpPages(); + var start = s.Scan(@"\d*").Trim(); + var end = s.Scan(@"\s*\d*").Trim(); + + return db.DumpPages( + start.Length == 0 ? 0 : Convert.ToUInt32(start), + end.Length == 0 ? uint.MaxValue : Convert.ToUInt32(end)); } else { From 59f6112263999d89fccc692948953eee9e35e8f7 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 13 Nov 2015 16:41:55 -0200 Subject: [PATCH 57/91] Clean up debuggers --- LiteDB/DbEngine/Disks/FileDiskService.cs | 4 ---- LiteDB/DbEngine/Services/PageService.cs | 7 ------- LiteDB/DbEngine/Services/TransactionService.cs | 4 ---- 3 files changed, 15 deletions(-) diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index e52b7807a..4a73b7535 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -156,8 +156,6 @@ public void WriteJournal(uint pageID, byte[] data) }); } - Console.WriteLine("journal write " + pageID); - // just write original bytes in order that are changed _journal.Write(data, 0, BasePage.PAGE_SIZE); } @@ -250,8 +248,6 @@ private void Recovery(FileStream journal) fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } - Console.WriteLine("recovery " + pageID); - // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); _stream.Write(buffer, 0, BasePage.PAGE_SIZE); diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 9c90f8e65..421dd2940 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -24,11 +24,6 @@ public PageService(IDiskService disk, CacheService cache) public T GetPage(uint pageID) where T : BasePage { - if(pageID == uint.MaxValue) - { - Console.Write("wait"); - } - var page = _cache.GetPage(pageID); if (page == null) @@ -38,8 +33,6 @@ public T GetPage(uint pageID) var buffer = _disk.ReadPage(pageID); page = (T)BasePage.ReadPage(buffer); - - Console.WriteLine("read page " + pageID + " (" + page.PageType + ")"); _cache.AddPage(page); } diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 5c8b246a2..9f24d4775 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -60,9 +60,6 @@ public void Commit() // write all dirty pages in data file foreach (var page in _cache.GetDirtyPages()) { - page.UpdateItemCount(); - - Console.WriteLine("write page " + page.PageID + " (" + page.PageType + ")"); _disk.WritePage(page.PageID, page.WritePage()); } @@ -117,7 +114,6 @@ public bool AvoidDirtyRead() // if changeID was changed, file was changed by another process - clear all cache if (cache.ChangeID != change) { - Console.WriteLine("Datafile changed, clear cache"); _cache.Clear(); return true; } From 6ca68172a3e6dc52ba69368cb80b264e0cea8b3b Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 13 Nov 2015 18:15:48 -0200 Subject: [PATCH 58/91] Fix serialization --- LiteDB-TODO.txt | 29 ++++++------------- LiteDB/Core/Collections/Find.cs | 19 +++++++++++- LiteDB/DbEngine/Actions/Dump.cs | 4 +-- LiteDB/DbEngine/Disks/FileDiskService.cs | 2 +- LiteDB/DbEngine/Pages/BasePage.cs | 15 ++++++---- LiteDB/DbEngine/Services/PageService.cs | 4 +-- LiteDB/DbEngine/Structures/PageAddress.cs | 2 +- LiteDB/LiteDB.csproj | 1 + LiteDB/Query/Query.cs | 4 +-- .../Serializer/Mapper/BsonMapper.Serialize.cs | 8 +++-- LiteDB/Utils/IndexNotFoundException.cs | 25 ++++++++++++++++ 11 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 LiteDB/Utils/IndexNotFoundException.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index d490a3f6b..7b35189f2 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,34 +1,23 @@ # LiteDB v.2 - TODO - Revisar help do Shell - saiu varias coisas -- Mapper de indice está fora do ar - "indexesDef" => IDictionary passar no methodo Find - Implementar novamente o BulkInsert -- Thread Safe +- Thread Safe/Process Safe - Thread safe no BsonMapper static methods (Reflection) + - Lock total de read. - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache - Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache -- Implementar StreamDiskService() -- Implementar ctor new LiteDatabase(Stream) +- Implementar StreamDiskService() + Implementar ctor new LiteDatabase(Stream) - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... - db.Debugger.TextWriters = Stream(); - db.Debbuger.Counters["Abc"].Times | Count - -- Remover conferencia de opções de um indice - se quiser q drop e recria com outras opções - -## Debbuger - -db.Debug.Write((s) => Console.Write(s)); -db.Debug.Counters.Enabled = true; -db.Debug.Counters.DiskReader -db.Debug.Counters.DiskWriter -db.Debug.Counters.BsonSerialize -db.Debug.Counters.BsonDeserialize -db.Debug.Counters.JournalWrite - - + db.Debugger.Write() = Stream(); + db.Debbgger.DiskRead | DiskWrite | Serialize | Deserialize | Command (maior) + (Counter { Count, TimeElipsed, Start(), Stop(), Reset() }) + db.Debugger.Enabled = false; +- Implementar FindAndModify/UpdateQuery +- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() ## Cache diff --git a/LiteDB/Core/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs index 10b9c5a72..3ea3f4d95 100644 --- a/LiteDB/Core/Collections/Find.cs +++ b/LiteDB/Core/Collections/Find.cs @@ -18,7 +18,24 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) { if (query == null) throw new ArgumentNullException("query"); - var docs = _engine.Find(_name, query, skip, limit); + IEnumerable docs; + + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + docs = _engine.Find(_name, query, skip, limit); + break; + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } foreach (var doc in docs) { diff --git a/LiteDB/DbEngine/Actions/Dump.cs b/LiteDB/DbEngine/Actions/Dump.cs index 39b327bcd..071ba535a 100644 --- a/LiteDB/DbEngine/Actions/Dump.cs +++ b/LiteDB/DbEngine/Actions/Dump.cs @@ -20,13 +20,13 @@ public StringBuilder DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) sb.AppendLine("============="); sb.AppendLine(); - var header = (HeaderPage)BasePage.ReadPage(_disk.ReadPage(0)); + var header = BasePage.ReadPage(_disk.ReadPage(0)); for (uint i = startPage; i <= endPage; i++) { if(i > header.LastPageID) break; - var p = BasePage.ReadPage(_disk.ReadPage(i)); + var p = BasePage.ReadPage(_disk.ReadPage(i)); sb.AppendFormat("{0} <{1},{2}> [{3}] {4}{5} | ", p.PageID.Dump(), diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index 4a73b7535..b965e2471 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -243,7 +243,7 @@ private void Recovery(FileStream journal) // if header, read all byte (to get original filesize) if(pageID == 0) { - var header = (HeaderPage)BasePage.ReadPage(buffer); + var header = BasePage.ReadPage(buffer); fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index 07d50b8fd..2cf2425bb 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -70,7 +70,7 @@ internal abstract class BasePage /// /// This is the data when read first from disk - used to journal operations (IDiskService only will use) /// - public byte[] DiskData { get; protected set; } + public byte[] DiskData { get; set; } public BasePage(uint pageID) { @@ -127,20 +127,25 @@ public static BasePage CreateInstance(uint pageID, PageType pageType) } /// - /// Read a page with correct instance page object + /// Read a page with correct instance page object. Checks for pageType /// - public static BasePage ReadPage(byte[] buffer) + public static T ReadPage(byte[] buffer) + where T : BasePage { var reader = new ByteReader(buffer); var pageID = reader.ReadUInt32(); var pageType = (PageType)reader.ReadByte(); - var page = CreateInstance(pageID, pageType); + + // checks if page is empty and required a specific page + var page = pageType == PageType.Empty && typeof(T) != typeof(BasePage) ? + CreateInstance(pageID) : // create using T + CreateInstance(pageID, pageType); // creating using pageType inside disk page.ReadHeader(reader); page.ReadContent(reader); - return page; + return (T)(BasePage)page; } /// diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 421dd2940..8edd93075 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -32,8 +32,8 @@ public T GetPage(uint pageID) { var buffer = _disk.ReadPage(pageID); - page = (T)BasePage.ReadPage(buffer); - + page = BasePage.ReadPage(buffer); + _cache.AddPage(page); } } diff --git a/LiteDB/DbEngine/Structures/PageAddress.cs b/LiteDB/DbEngine/Structures/PageAddress.cs index 40a778ea0..342c29a8e 100644 --- a/LiteDB/DbEngine/Structures/PageAddress.cs +++ b/LiteDB/DbEngine/Structures/PageAddress.cs @@ -7,7 +7,7 @@ namespace LiteDB { /// - /// Represents a page adress inside a page structure - index could be byte offset position OR index in a list + /// Represents a page adress inside a page structure - index could be byte offset position OR index in a list (6 bytes) /// internal struct PageAddress { diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 66e7a4d2b..90eebd7c4 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -149,6 +149,7 @@ + diff --git a/LiteDB/Query/Query.cs b/LiteDB/Query/Query.cs index 4f9415c71..586a58ce2 100644 --- a/LiteDB/Query/Query.cs +++ b/LiteDB/Query/Query.cs @@ -176,8 +176,8 @@ internal virtual IEnumerable Run(CollectionPage col, IndexService ind // get index for this query var index = col.GetIndex(this.Field); - // no index? throw an exception - if (index == null) throw LiteException.IndexNotFound(col.CollectionName, this.Field); + // no index? throw an index not found exception to auto-create in LiteDatabse + if (index == null) throw new IndexNotFoundException(col.CollectionName, this.Field); // execute query to get all IndexNodes return this.ExecuteIndex(indexer, index); diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index 64c0f0d40..c8e1d8c8d 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -92,9 +92,11 @@ private BsonValue Serialize(Type type, object obj, int depth) return custom(obj); } // check if is a list or array - else if (obj is IList || type.IsArray) + else if (obj is IEnumerable) { - var itemType = type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]; + var itemType = type.IsArray ? + type.GetElementType() : + (type.IsGenericType ? type.GetGenericArguments()[0] : typeof(object)); return this.SerializeArray(itemType, obj as IEnumerable, depth); } @@ -105,7 +107,7 @@ private BsonValue Serialize(Type type, object obj, int depth) return this.SerializeDictionary(itemType, obj as IDictionary, depth); } - // otherwise treat as a plain object + // otherwise serialize as a plain object else { return this.SerializeObject(type, obj, depth); diff --git a/LiteDB/Utils/IndexNotFoundException.cs b/LiteDB/Utils/IndexNotFoundException.cs new file mode 100644 index 000000000..5b3fef992 --- /dev/null +++ b/LiteDB/Utils/IndexNotFoundException.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace LiteDB +{ + /// + /// A specific exception when a query didnt found an index + /// + internal class IndexNotFoundException : LiteException + { + public string Collection { get; set; } + public string Field { get; set; } + + public IndexNotFoundException(string collection, string field) + : base("Index not found") + { + this.Collection = collection; + this.Field = field; + } + } +} From 3f8386e0771a5ccf7b2b5a03f0a598844e666f7e Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 14 Nov 2015 10:31:16 -0200 Subject: [PATCH 59/91] Fix bugs in cache/pages --- LiteDB-TODO.txt | 4 ++ LiteDB.Shell/Commands/Reset.cs | 36 +++++++++++++++++ LiteDB.Shell/LiteDB.Shell.csproj | 1 + LiteDB/DbEngine/Actions/Dump.cs | 4 +- LiteDB/DbEngine/Disks/FileDiskService.cs | 2 +- LiteDB/DbEngine/Pages/BasePage.cs | 10 ++--- LiteDB/DbEngine/Pages/EmptyPage.cs | 20 ++++++++++ LiteDB/DbEngine/Services/CacheService.cs | 40 +++++++++---------- LiteDB/DbEngine/Services/CollectionService.cs | 6 ++- LiteDB/DbEngine/Services/DataService.cs | 5 ++- LiteDB/DbEngine/Services/IndexService.cs | 10 ++--- LiteDB/DbEngine/Services/PageService.cs | 30 ++++++++++---- .../DbEngine/Services/TransactionService.cs | 2 +- 13 files changed, 123 insertions(+), 47 deletions(-) create mode 100644 LiteDB.Shell/Commands/Reset.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 7b35189f2..df9fd1862 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,5 +1,9 @@ # LiteDB v.2 - TODO +- Revisar toda questao das paginas/cache +> db.col1.insert {a:1} +> db.col.ensureindex a + - Revisar help do Shell - saiu varias coisas - Implementar novamente o BulkInsert - Thread Safe/Process Safe diff --git a/LiteDB.Shell/Commands/Reset.cs b/LiteDB.Shell/Commands/Reset.cs new file mode 100644 index 000000000..857c3372d --- /dev/null +++ b/LiteDB.Shell/Commands/Reset.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text; + +#if DEBUG +namespace LiteDB.Shell.Commands +{ + /// + /// Delete database e open again - used for tests only + /// + internal class Reset : ConsoleCommand + { + public override bool IsCommand(StringScanner s) + { + return s.Scan(@"reset\s+").Length > 0; + } + + public override void Execute(LiteShell shell, StringScanner s, Display display, InputCommand input) + { + var filename = s.Scan(@".+"); + + if (shell.Database != null) + { + shell.Database.Dispose(); + } + + File.Delete(filename); + + shell.Database = new LiteDatabase(filename); + } + } +} +#endif \ No newline at end of file diff --git a/LiteDB.Shell/LiteDB.Shell.csproj b/LiteDB.Shell/LiteDB.Shell.csproj index 4f0d71ebb..101196e1c 100644 --- a/LiteDB.Shell/LiteDB.Shell.csproj +++ b/LiteDB.Shell/LiteDB.Shell.csproj @@ -53,6 +53,7 @@ + diff --git a/LiteDB/DbEngine/Actions/Dump.cs b/LiteDB/DbEngine/Actions/Dump.cs index 071ba535a..39b327bcd 100644 --- a/LiteDB/DbEngine/Actions/Dump.cs +++ b/LiteDB/DbEngine/Actions/Dump.cs @@ -20,13 +20,13 @@ public StringBuilder DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) sb.AppendLine("============="); sb.AppendLine(); - var header = BasePage.ReadPage(_disk.ReadPage(0)); + var header = (HeaderPage)BasePage.ReadPage(_disk.ReadPage(0)); for (uint i = startPage; i <= endPage; i++) { if(i > header.LastPageID) break; - var p = BasePage.ReadPage(_disk.ReadPage(i)); + var p = BasePage.ReadPage(_disk.ReadPage(i)); sb.AppendFormat("{0} <{1},{2}> [{3}] {4}{5} | ", p.PageID.Dump(), diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index b965e2471..4a73b7535 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -243,7 +243,7 @@ private void Recovery(FileStream journal) // if header, read all byte (to get original filesize) if(pageID == 0) { - var header = BasePage.ReadPage(buffer); + var header = (HeaderPage)BasePage.ReadPage(buffer); fileSize = (header.LastPageID + 1) * BasePage.PAGE_SIZE; } diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index 2cf2425bb..9cd784f9c 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -129,23 +129,19 @@ public static BasePage CreateInstance(uint pageID, PageType pageType) /// /// Read a page with correct instance page object. Checks for pageType /// - public static T ReadPage(byte[] buffer) - where T : BasePage + public static BasePage ReadPage(byte[] buffer) { var reader = new ByteReader(buffer); var pageID = reader.ReadUInt32(); var pageType = (PageType)reader.ReadByte(); - // checks if page is empty and required a specific page - var page = pageType == PageType.Empty && typeof(T) != typeof(BasePage) ? - CreateInstance(pageID) : // create using T - CreateInstance(pageID, pageType); // creating using pageType inside disk + var page = CreateInstance(pageID, pageType); page.ReadHeader(reader); page.ReadContent(reader); - return (T)(BasePage)page; + return page; } /// diff --git a/LiteDB/DbEngine/Pages/EmptyPage.cs b/LiteDB/DbEngine/Pages/EmptyPage.cs index 72d174941..e93504669 100644 --- a/LiteDB/DbEngine/Pages/EmptyPage.cs +++ b/LiteDB/DbEngine/Pages/EmptyPage.cs @@ -43,6 +43,26 @@ public override void UpdateItemCount() this.FreeBytes = PAGE_AVAILABLE_BYTES; } + public T ConvertTo() + where T : BasePage + { + var copy = BasePage.CreateInstance(this.PageID); + + copy.NextPageID = this.NextPageID; + copy.PrevPageID = this.PrevPageID; + copy.IsDirty = this.IsDirty; + + if (this.DiskData.Length > 0) + { + this.DiskData = new byte[BasePage.PAGE_SIZE]; + Buffer.BlockCopy(this.DiskData, 0, copy.DiskData, 0, BasePage.PAGE_SIZE); + } + + copy.UpdateItemCount(); + + return copy; + } + #region Read/Write pages protected override void ReadContent(ByteReader reader) diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 6b9ecacad..34b7a2288 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -14,7 +14,7 @@ namespace LiteDB /// internal class CacheService : IDisposable { - // single cache structure - use Sort dictionary for get pages in order (fast to store in sequence on disk) + // double cache structure - use Sort dictionary for get pages in order (fast to store in sequence on disk) private SortedDictionary _cache; private SortedDictionary _dirty; @@ -29,10 +29,10 @@ public CacheService() /// /// Get a page in my cache, first check if is in my dirty list. If not, check in my cache list. Returns null if not found /// - public T GetPage(uint pageID) - where T : BasePage + public BasePage GetPage(uint pageID) { - return (T)_dirty.GetOrDefault(pageID, null) ?? (T)_cache.GetOrDefault(pageID, null); + // check for in dirty list - if not found, check in cache list + return _dirty.GetOrDefault(pageID, null) ?? _cache.GetOrDefault(pageID, null); } /// @@ -57,11 +57,10 @@ public void SetPageDirty(BasePage page) { lock(_dirty) { - // if page already dirty do nothing, not even add in _dirty dictionary (page type can be changed when delete a page) - if (page.IsDirty) return; - _dirty[page.PageID] = page; + if (page.IsDirty) return; + page.IsDirty = true; // if page is new (not exits on datafile), there is no journal for them @@ -96,20 +95,21 @@ public void ClearDirty() { lock(_cache) { - foreach (var page in _dirty.Values) - { - page.IsDirty = false; - - // remove all non-header/collection pages (this can be optional in future) - if (page.PageType == PageType.Extend || - page.PageType == PageType.Empty || - page.PageType == PageType.Index || - page.PageType == PageType.Data) - { - _cache.Remove(page.PageID); - } - } + //foreach (var page in _dirty.Values) + //{ + // page.IsDirty = false; + + // // remove all non-header/collection pages (this can be optional in future) + // if (page.PageType == PageType.Extend || + // page.PageType == PageType.Empty || + // page.PageType == PageType.Index || + // page.PageType == PageType.Data) + // { + // _cache.Remove(page.PageID); + // } + //} + _cache.Clear(); _dirty.Clear(); } } diff --git a/LiteDB/DbEngine/Services/CollectionService.cs b/LiteDB/DbEngine/Services/CollectionService.cs index 3b0ff83a0..7ae8a9371 100644 --- a/LiteDB/DbEngine/Services/CollectionService.cs +++ b/LiteDB/DbEngine/Services/CollectionService.cs @@ -114,6 +114,10 @@ public void Drop(CollectionPage col) // add index page to delete list page pages.Add(node.Position.PageID); } + + // remove head+tail nodes in all indexes + pages.Add(index.HeadNode.PageID); + pages.Add(index.TailNode.PageID); } // and now, lets delete all this pages @@ -127,7 +131,7 @@ public void Drop(CollectionPage col) // remove page from collection list _pager.AddOrRemoveToFreeList(false, col, header, ref header.FirstCollectionPageID); - _pager.DeletePage(col.PageID, false); + _pager.DeletePage(col.PageID); } } } diff --git a/LiteDB/DbEngine/Services/DataService.cs b/LiteDB/DbEngine/Services/DataService.cs index dc410cc60..d43603e1d 100644 --- a/LiteDB/DbEngine/Services/DataService.cs +++ b/LiteDB/DbEngine/Services/DataService.cs @@ -175,18 +175,19 @@ public DataBlock Delete(CollectionPage col, PageAddress blockAddress) // first, remove from free list _pager.AddOrRemoveToFreeList(false, page, col, ref col.FreeDataPageID); - _pager.DeletePage(page.PageID, false); + _pager.DeletePage(page.PageID); } else { // add or remove to free list _pager.AddOrRemoveToFreeList(page.FreeBytes > DataPage.DATA_RESERVED_BYTES, page, col, ref col.FreeDataPageID); + + _pager.SetDirty(page); } col.DocumentCount--; _pager.SetDirty(col); - _pager.SetDirty(page); return block; } diff --git a/LiteDB/DbEngine/Services/IndexService.cs b/LiteDB/DbEngine/Services/IndexService.cs index f5df0667e..56534c2a0 100644 --- a/LiteDB/DbEngine/Services/IndexService.cs +++ b/LiteDB/DbEngine/Services/IndexService.cs @@ -195,15 +195,15 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) // first, remove from free list _pager.AddOrRemoveToFreeList(false, page, index.Page, ref index.FreeIndexPageID); - _pager.DeletePage(page.PageID, false); + _pager.DeletePage(page.PageID); } else { // add or remove page from free list _pager.AddOrRemoveToFreeList(page.FreeBytes > IndexPage.INDEX_RESERVED_BYTES, node.Page, index.Page, ref index.FreeIndexPageID); - } - _pager.SetDirty(page); + _pager.SetDirty(page); + } } @@ -222,9 +222,9 @@ public void DropIndex(CollectionIndex index) } // now delete all pages - foreach (var page in pages) + foreach (var pageID in pages) { - _pager.DeletePage(page); + _pager.DeletePage(pageID); } } diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 8edd93075..403dd5a4e 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -24,21 +24,29 @@ public PageService(IDiskService disk, CacheService cache) public T GetPage(uint pageID) where T : BasePage { - var page = _cache.GetPage(pageID); + var page = _cache.GetPage(pageID); + // is not on cache? load from disk if (page == null) { - lock(_disk) - { - var buffer = _disk.ReadPage(pageID); + var buffer = _disk.ReadPage(pageID); + page = BasePage.ReadPage(buffer); + _cache.AddPage(page); + } - page = BasePage.ReadPage(buffer); + // if page is empty, convert to T + if(page.PageType == PageType.Empty && typeof(T) != typeof(BasePage)) + { + page = ((EmptyPage)page).ConvertTo(); - _cache.AddPage(page); - } + // add to cache again (with new type) + _cache.AddPage(page); + + // if convert a dirty page, set new page as dirty too + if(page.IsDirty) this.SetDirty(page); } - return page; + return (T)page; } /// @@ -110,6 +118,8 @@ public T NewPage(BasePage prevPage = null) /// /// Delete an page using pageID - transform them in Empty Page and add to EmptyPageList + /// If you delete a page, you can using same old instance of page - page will be converter to EmptyPage + /// If need deleted page, use GetPage again /// public void DeletePage(uint pageID, bool addSequence = false) { @@ -158,6 +168,8 @@ public T GetFreePage(uint startPageID, int size) return this.NewPage(); } + #region Add Or Remove do empty list + /// /// Add or Remove a page in a sequence /// @@ -295,5 +307,7 @@ private void MoveToFreeList(BasePage page, BasePage startPage, ref uint fieldPag this.RemoveToFreeList(page, startPage, ref fieldPageID); this.AddToFreeList(page, startPage, ref fieldPageID); } + + #endregion } } diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 9f24d4775..245923bf3 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -104,7 +104,7 @@ public bool AvoidDirtyRead() // if is in transaction pages are not dirty (begin trans was checked) if (_trans == true) return false; - var cache = _cache.GetPage(0); + var cache = (HeaderPage)_cache.GetPage(0); if (cache == null) return false; From b610889df2c1b75af675fdf28d25fb0ea4135420 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 14 Nov 2015 10:34:54 -0200 Subject: [PATCH 60/91] Remove unused code --- LiteDB/DbEngine/Pages/EmptyPage.cs | 20 -------------------- LiteDB/DbEngine/Services/PageService.cs | 10 +++------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/LiteDB/DbEngine/Pages/EmptyPage.cs b/LiteDB/DbEngine/Pages/EmptyPage.cs index e93504669..72d174941 100644 --- a/LiteDB/DbEngine/Pages/EmptyPage.cs +++ b/LiteDB/DbEngine/Pages/EmptyPage.cs @@ -43,26 +43,6 @@ public override void UpdateItemCount() this.FreeBytes = PAGE_AVAILABLE_BYTES; } - public T ConvertTo() - where T : BasePage - { - var copy = BasePage.CreateInstance(this.PageID); - - copy.NextPageID = this.NextPageID; - copy.PrevPageID = this.PrevPageID; - copy.IsDirty = this.IsDirty; - - if (this.DiskData.Length > 0) - { - this.DiskData = new byte[BasePage.PAGE_SIZE]; - Buffer.BlockCopy(this.DiskData, 0, copy.DiskData, 0, BasePage.PAGE_SIZE); - } - - copy.UpdateItemCount(); - - return copy; - } - #region Read/Write pages protected override void ReadContent(ByteReader reader) diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index 403dd5a4e..bb2252dd4 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -34,17 +34,13 @@ public T GetPage(uint pageID) _cache.AddPage(page); } +#if DEBUG // if page is empty, convert to T if(page.PageType == PageType.Empty && typeof(T) != typeof(BasePage)) { - page = ((EmptyPage)page).ConvertTo(); - - // add to cache again (with new type) - _cache.AddPage(page); - - // if convert a dirty page, set new page as dirty too - if(page.IsDirty) this.SetDirty(page); + throw new SystemException("Pager.GetPage() never shuld happend"); } +#endif return (T)page; } From 2f5275bc898cdf74cf70c129c0af17667f4a06f8 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 14 Nov 2015 11:53:42 -0200 Subject: [PATCH 61/91] Bulk insert --- LiteDB-TODO.txt | 5 +--- LiteDB.Tests/BulkTest.cs | 39 +++++++++++++++++++++++++ LiteDB.Tests/LiteDB.Tests.csproj | 1 + LiteDB/Core/Collections/InsertBulk.cs | 39 +++++++++++++++++++++++++ LiteDB/DbEngine/Actions/Aggregate.cs | 2 ++ LiteDB/DbEngine/Services/PageService.cs | 9 ++++-- LiteDB/LiteDB.csproj | 1 + 7 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 LiteDB.Tests/BulkTest.cs create mode 100644 LiteDB/Core/Collections/InsertBulk.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index df9fd1862..78bb8df2e 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,9 +1,5 @@ # LiteDB v.2 - TODO -- Revisar toda questao das paginas/cache -> db.col1.insert {a:1} -> db.col.ensureindex a - - Revisar help do Shell - saiu varias coisas - Implementar novamente o BulkInsert - Thread Safe/Process Safe @@ -22,6 +18,7 @@ db.Debugger.Enabled = false; - Implementar FindAndModify/UpdateQuery - O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() +- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? ## Cache diff --git a/LiteDB.Tests/BulkTest.cs b/LiteDB.Tests/BulkTest.cs new file mode 100644 index 000000000..d7b461381 --- /dev/null +++ b/LiteDB.Tests/BulkTest.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; + +namespace UnitTest +{ + [TestClass] + public class BulkTest + { + [TestMethod] + public void Bulk_Test() + { + using (var db = new LiteDatabase(DB.Path())) + { + var col = db.GetCollection("b"); + + col.InsertBulk(GetDocs(), 50); + + Assert.AreEqual(220, col.Count()); + } + } + + private IEnumerable GetDocs() + { + for (var i = 0; i < 220; i++) + { + var doc = new BsonDocument() + .Add("_id", Guid.NewGuid()) + .Add("content", DB.LoremIpsum(20, 50, 1, 2, 1)); + + yield return doc; + } + } + } +} diff --git a/LiteDB.Tests/LiteDB.Tests.csproj b/LiteDB.Tests/LiteDB.Tests.csproj index 495ff3795..e1f484d35 100644 --- a/LiteDB.Tests/LiteDB.Tests.csproj +++ b/LiteDB.Tests/LiteDB.Tests.csproj @@ -56,6 +56,7 @@ + diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs new file mode 100644 index 000000000..6aebbdee2 --- /dev/null +++ b/LiteDB/Core/Collections/InsertBulk.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LiteDB +{ + public partial class LiteCollection + { + /// + /// Bulk documents to a collection - use data chunks for most efficient insert + /// + public int InsertBulk(IEnumerable docs, int buffer = 32768) + { + if (docs == null) throw new ArgumentNullException("docs"); + if (buffer <= 1) throw new ArgumentException("buffer must be bigger than 1"); + + var count = 0; + + while (true) + { + // get a slice of documents of docs + var slice = docs.Skip(count).Take(buffer); + + // insert this docs + var included = this.Insert(slice); + + // if included less than buffer, there is no more to insert + if (included < buffer) + { + return count + included; + } + + count += included; + } + } + } +} diff --git a/LiteDB/DbEngine/Actions/Aggregate.cs b/LiteDB/DbEngine/Actions/Aggregate.cs index f0736b164..a8a876d01 100644 --- a/LiteDB/DbEngine/Actions/Aggregate.cs +++ b/LiteDB/DbEngine/Actions/Aggregate.cs @@ -65,6 +65,8 @@ public int Count(string colName, Query query) if (col == null) return 0; + if (query == null) return (int)col.DocumentCount; + // run query in this collection var nodes = query.Run(col, _indexer); diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index bb2252dd4..ebf96c4b5 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -29,9 +29,12 @@ public T GetPage(uint pageID) // is not on cache? load from disk if (page == null) { - var buffer = _disk.ReadPage(pageID); - page = BasePage.ReadPage(buffer); - _cache.AddPage(page); + lock(_disk) + { + var buffer = _disk.ReadPage(pageID); + page = BasePage.ReadPage(buffer); + _cache.AddPage(page); + } } #if DEBUG diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 90eebd7c4..b7d14256f 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -51,6 +51,7 @@ + From e1b541f59c44ba36dd1c3edccfb1aa60b42e38ea Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 14 Nov 2015 15:43:56 -0200 Subject: [PATCH 62/91] StreamDiskService --- LiteDB-TODO.txt | 13 +- LiteDB.Tests/AutoIdTest.cs | 2 +- LiteDB.Tests/BulkTest.cs | 2 +- LiteDB.Tests/DropCollectionTest.cs | 3 +- LiteDB.Tests/FileStorageTest.cs | 2 +- LiteDB.Tests/IncludeTest.cs | 2 +- LiteDB.Tests/IndexOrderTest.cs | 2 +- LiteDB/Core/LiteDatabase.cs | 7 + LiteDB/DbEngine/Disks/StreamDiskService.cs | 125 ++++++++++++++++++ LiteDB/LiteDB.csproj | 1 + .../Serializer/Mapper/BsonMapper.Serialize.cs | 18 +-- 11 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 LiteDB/DbEngine/Disks/StreamDiskService.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 78bb8df2e..0e409437a 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,24 +1,26 @@ # LiteDB v.2 - TODO +- Implementar StreamDiskService() + Implementar ctor new LiteDatabase(Stream) +- Unit Teste devem sempre usar MemoryStream() (exceto concorrencia e/ou algo especifico pra disco) + +- Implementar FindAndModify/UpdateQuery +- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? IBsonMapper? + + - Revisar help do Shell - saiu varias coisas -- Implementar novamente o BulkInsert - Thread Safe/Process Safe - Thread safe no BsonMapper static methods (Reflection) - Lock total de read. - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache -- Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Revisar BsonArray.Length não faz cache -- Implementar StreamDiskService() + Implementar ctor new LiteDatabase(Stream) - Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... db.Debugger.Write() = Stream(); db.Debbgger.DiskRead | DiskWrite | Serialize | Deserialize | Command (maior) (Counter { Count, TimeElipsed, Start(), Stop(), Reset() }) db.Debugger.Enabled = false; -- Implementar FindAndModify/UpdateQuery - O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() -- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? ## Cache @@ -58,6 +60,7 @@ e) Leitura conforme indice (retorno poucos registros) - DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? - Folder root: "src", "test", "nuget" - Encrypt datafile (EncryptDiskService : FileDiskService) +- Queria que o IDiskService não tivesse informações sobre Journal/Recovery - Como implementar pro DNX - Mapeador/Configuration estilo EF diff --git a/LiteDB.Tests/AutoIdTest.cs b/LiteDB.Tests/AutoIdTest.cs index 76a2c0a22..1d0f1e7e2 100644 --- a/LiteDB.Tests/AutoIdTest.cs +++ b/LiteDB.Tests/AutoIdTest.cs @@ -38,7 +38,7 @@ public class AutoIdTest [TestMethod] public void AutoId_Test() { - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { var cs_int = db.GetCollection("int"); var cs_guid = db.GetCollection("guid"); diff --git a/LiteDB.Tests/BulkTest.cs b/LiteDB.Tests/BulkTest.cs index d7b461381..fb228531d 100644 --- a/LiteDB.Tests/BulkTest.cs +++ b/LiteDB.Tests/BulkTest.cs @@ -14,7 +14,7 @@ public class BulkTest [TestMethod] public void Bulk_Test() { - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { var col = db.GetCollection("b"); diff --git a/LiteDB.Tests/DropCollectionTest.cs b/LiteDB.Tests/DropCollectionTest.cs index fb4f08f1c..28266d009 100644 --- a/LiteDB.Tests/DropCollectionTest.cs +++ b/LiteDB.Tests/DropCollectionTest.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -13,7 +14,7 @@ public class DropCollectionTest [TestMethod] public void DropCollection_Test() { - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { Assert.IsFalse(db.CollectionExists("customerCollection")); var collection = db.GetCollection("customerCollection"); diff --git a/LiteDB.Tests/FileStorageTest.cs b/LiteDB.Tests/FileStorageTest.cs index ed503ecef..d3bb12468 100644 --- a/LiteDB.Tests/FileStorageTest.cs +++ b/LiteDB.Tests/FileStorageTest.cs @@ -19,7 +19,7 @@ public void FileStorage_InsertDelete() // create a dump file File.WriteAllText("Core.dll", "FileCoreContent"); - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { db.FileStorage.Upload("Core.dll", "Core.dll"); diff --git a/LiteDB.Tests/IncludeTest.cs b/LiteDB.Tests/IncludeTest.cs index 41e6f2740..46fdc1d81 100644 --- a/LiteDB.Tests/IncludeTest.cs +++ b/LiteDB.Tests/IncludeTest.cs @@ -40,7 +40,7 @@ public class IncludeTest [TestMethod] public void Include_Test() { - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { var customers = db.GetCollection("customers"); var products = db.GetCollection("products"); diff --git a/LiteDB.Tests/IndexOrderTest.cs b/LiteDB.Tests/IndexOrderTest.cs index ebef6b971..06bf9baaa 100644 --- a/LiteDB.Tests/IndexOrderTest.cs +++ b/LiteDB.Tests/IndexOrderTest.cs @@ -14,7 +14,7 @@ public class IndexOrderTest [TestMethod] public void Index_Order() { - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { var col = db.GetCollection("order"); diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index c52b6ad41..45835f483 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -38,6 +38,13 @@ public LiteDatabase(string connectionString) _mapper = BsonMapper.Global; } + public LiteDatabase(Stream stream) + { + // initialize engine using StreamDisk + _engine = new DbEngine(new StreamDiskService(stream)); + _mapper = BsonMapper.Global; + } + /// /// Starts LiteDB database using full parameters /// diff --git a/LiteDB/DbEngine/Disks/StreamDiskService.cs b/LiteDB/DbEngine/Disks/StreamDiskService.cs new file mode 100644 index 000000000..7bacfc712 --- /dev/null +++ b/LiteDB/DbEngine/Disks/StreamDiskService.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace LiteDB +{ + /// + /// A simple implementation of diskservice using base Stream (no journal, thread safe) + /// + internal class StreamDiskService : IDiskService + { + private Stream _stream; + + public StreamDiskService(Stream stream) + { + _stream = stream; + } + + /// + /// Checks only if stream is empty + /// + public bool Initialize() + { + return (_stream.Length == 0); + } + + #region Lock/Unlock + + /// + /// Lock stream + /// + public void Lock() + { + } + + /// + /// Release lock + /// + public void Unlock() + { + } + + #endregion + + #region Read/Write + + /// + /// Read first 2 bytes from datafile - contains changeID (avoid to read all header page) + /// + public ushort GetChangeID() + { + var bytes = new byte[2]; + _stream.Seek(HeaderPage.CHANGE_ID_POSITION, SeekOrigin.Begin); + _stream.Read(bytes, 0, 2); + return BitConverter.ToUInt16(bytes, 0); + } + + /// + /// Read page bytes from disk + /// + public byte[] ReadPage(uint pageID) + { + var buffer = new byte[BasePage.PAGE_SIZE]; + var position = (long)pageID * (long)BasePage.PAGE_SIZE; + + // position cursor + if (_stream.Position != position) + { + _stream.Seek(position, SeekOrigin.Begin); + } + + // read bytes from data file + _stream.Read(buffer, 0, BasePage.PAGE_SIZE); + + return buffer; + } + + /// + /// Persist single page bytes to disk + /// + public void WritePage(uint pageID, byte[] buffer) + { + var position = (long)pageID * (long)BasePage.PAGE_SIZE; + + // position cursor + if (_stream.Position != position) + { + _stream.Seek(position, SeekOrigin.Begin); + } + + _stream.Write(buffer, 0, BasePage.PAGE_SIZE); + } + + #endregion + + #region Journal file + + public void WriteJournal(uint pageID, byte[] data) + { + } + + public void CommitJournal(long fileSize) + { + } + + public void DeleteJournal() + { + } + + public void Dispose() + { + if(_stream != null) + { + _stream.Dispose(); + } + } + + #endregion + + } +} diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index b7d14256f..7434db56f 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -53,6 +53,7 @@ + diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index c8e1d8c8d..c2ecabf46 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -91,15 +91,6 @@ private BsonValue Serialize(Type type, object obj, int depth) { return custom(obj); } - // check if is a list or array - else if (obj is IEnumerable) - { - var itemType = type.IsArray ? - type.GetElementType() : - (type.IsGenericType ? type.GetGenericArguments()[0] : typeof(object)); - - return this.SerializeArray(itemType, obj as IEnumerable, depth); - } // for dictionary else if (obj is IDictionary) { @@ -107,6 +98,15 @@ private BsonValue Serialize(Type type, object obj, int depth) return this.SerializeDictionary(itemType, obj as IDictionary, depth); } + // check if is a list or array + else if (obj is IEnumerable) + { + var itemType = type.IsArray ? + type.GetElementType() : + (type.IsGenericType ? type.GetGenericArguments()[0] : typeof(object)); + + return this.SerializeArray(itemType, obj as IEnumerable, depth); + } // otherwise serialize as a plain object else { From f4e9995e8eae78a5b3903e44e6ec574d4fdb6d7f Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sat, 14 Nov 2015 21:53:42 -0200 Subject: [PATCH 63/91] Start logging --- LiteDB-TODO.txt | 30 ++++++++++-- LiteDB/Core/Collections/Include.cs | 6 +-- LiteDB/Core/Collections/Insert.cs | 10 ++-- LiteDB/Core/Collections/LiteCollection.cs | 6 ++- LiteDB/Core/LiteDatabase.cs | 10 ++-- LiteDB/DbEngine/Disks/FileDiskService.cs | 30 +++++++++++- LiteDB/LiteDB.csproj | 1 + LiteDB/Utils/Logger.cs | 57 +++++++++++++++++++++++ 8 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 LiteDB/Utils/Logger.cs diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 0e409437a..f9a9498bb 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,11 +1,10 @@ # LiteDB v.2 - TODO -- Implementar StreamDiskService() + Implementar ctor new LiteDatabase(Stream) -- Unit Teste devem sempre usar MemoryStream() (exceto concorrencia e/ou algo especifico pra disco) - - Implementar FindAndModify/UpdateQuery -- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? IBsonMapper? +- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() +- Debbuger? Acho importante no teste da cache +- Unit Teste devem sempre usar MemoryStream() (exceto concorrencia e/ou algo especifico pra disco) - Revisar help do Shell - saiu varias coisas - Thread Safe/Process Safe @@ -20,7 +19,6 @@ db.Debbgger.DiskRead | DiskWrite | Serialize | Deserialize | Command (maior) (Counter { Count, TimeElipsed, Start(), Stop(), Reset() }) db.Debugger.Enabled = false; -- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() ## Cache @@ -61,6 +59,7 @@ e) Leitura conforme indice (retorno poucos registros) - Folder root: "src", "test", "nuget" - Encrypt datafile (EncryptDiskService : FileDiskService) - Queria que o IDiskService não tivesse informações sobre Journal/Recovery +- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? IBsonMapper? - Como implementar pro DNX - Mapeador/Configuration estilo EF @@ -91,6 +90,27 @@ e) Leitura conforme indice (retorno poucos registros) - Linq, shell, fileStorage, fts - API +db.Logger.Level = None, Error, Info, Verbose; +db.Logger.Output = Console.Out; + +db.Logger.Error(ex, "text"); +db.Logger.Info("text", arg0, arg1); +db.Logger.Verbose("text", arg0, arg1); + + [] +2014-11-03T18:28:32.450-0500 I NETWORK [initandlisten] waiting for connections on port 27017 + +Components: +ACCESS +COMMAND +CONTROL +INDEX +QUERY +STORAGE +JOURNAL + + + ================================================================================================================ ================================================================================================================ diff --git a/LiteDB/Core/Collections/Include.cs b/LiteDB/Core/Collections/Include.cs index f32334624..006d9a05a 100644 --- a/LiteDB/Core/Collections/Include.cs +++ b/LiteDB/Core/Collections/Include.cs @@ -34,7 +34,7 @@ public LiteCollection Include(Expression> dbref) if(array.Count == 0) return; // all doc refs in an array must be same collection, lets take first only - var col = new LiteCollection(array[0].AsDocument["$ref"], _engine, _mapper); + var col = new LiteCollection(array[0].AsDocument["$ref"], _engine, _mapper, _log); for(var i = 0; i < array.Count; i++) { @@ -46,14 +46,14 @@ public LiteCollection Include(Expression> dbref) { // for BsonDocument, get property value e update with full object refence var doc = value.AsDocument; - var col = new LiteCollection(doc["$ref"], _engine, _mapper); + var col = new LiteCollection(doc["$ref"], _engine, _mapper, _log); var obj = col.FindById(doc["$id"]); bson.Set(path, obj); } }; // cloning this collection and adding this include - var newcol = new LiteCollection(_name, _engine, _mapper); + var newcol = new LiteCollection(_name, _engine, _mapper, _log); newcol._includes.AddRange(_includes); newcol._includes.Add(action); diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index a105815b4..9af70c5d6 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -15,12 +15,14 @@ public BsonValue Insert(T document) { if (document == null) throw new ArgumentNullException("document"); - _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper)); + _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper, _log)); var doc = _mapper.ToDocument(document); _engine.InsertDocuments(_name, new BsonDocument[] { doc }); + _log.Debug("COMMAND", "inserted document _id = {0}", doc["_id"]); + return doc["_id"]; } @@ -37,16 +39,16 @@ public int Insert(IEnumerable docs) /// /// Convert each T document in a BsonDocument, setting autoId for each one /// - /// - /// private IEnumerable GetBsonDocs(IEnumerable docs) { foreach (var document in docs) { - _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper)); + _mapper.SetAutoId(document, new LiteCollection(_name, _engine, _mapper, _log)); var doc = _mapper.ToDocument(document); + _log.Debug("COMMAND", "inserted document _id = {0}", doc["_id"]); + yield return doc; } } diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index f8950e5d7..2d87293ea 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -9,9 +9,10 @@ namespace LiteDB public sealed partial class LiteCollection where T : new() { - private DbEngine _engine; private string _name; + private DbEngine _engine; private BsonMapper _mapper; + private Logger _log; private List> _includes; private QueryVisitor _visitor; @@ -20,11 +21,12 @@ public sealed partial class LiteCollection /// public string Name { get { return _name; } } - internal LiteCollection(string name, DbEngine engine, BsonMapper mapper) + internal LiteCollection(string name, DbEngine engine, BsonMapper mapper, Logger log) { _name = name; _engine = engine; _mapper = mapper; + _log = log; _visitor = new QueryVisitor(mapper); _includes = new List>(); } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 45835f483..6674cb713 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -16,8 +16,12 @@ public partial class LiteDatabase : IDisposable private BsonMapper _mapper; + private Logger _log = new Logger(); + public BsonMapper Mapper { get { return _mapper; } } + public Logger Log { get { return _log; } } + /// /// Starts LiteDB database using a connectionString /// @@ -34,7 +38,7 @@ public LiteDatabase(string connectionString) if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); // initialize engine creating a new FileDiskService for data access - _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password)); + _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password, _log)); _mapper = BsonMapper.Global; } @@ -63,7 +67,7 @@ public LiteDatabase(IDiskService diskService, BsonMapper mapper) public LiteCollection GetCollection(string name) where T : new() { - return new LiteCollection(name, _engine, _mapper); + return new LiteCollection(name, _engine, _mapper, _log); } /// @@ -72,7 +76,7 @@ public LiteCollection GetCollection(string name) /// Collection name (case insensitive) public LiteCollection GetCollection(string name) { - return new LiteCollection(name, _engine, _mapper); + return new LiteCollection(name, _engine, _mapper, _log); } /// diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index 4a73b7535..55deee739 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -23,16 +23,18 @@ internal class FileDiskService : IDiskService private string _journalFilename; private bool _journalEnabled; + private Logger _log; private TimeSpan _timeout; private bool _readonly; private string _password; - public FileDiskService(string filename, bool journalEnabled, TimeSpan timeout, bool readOnly, string password) + public FileDiskService(string filename, bool journalEnabled, TimeSpan timeout, bool readOnly, string password, Logger logger) { _filename = filename; _timeout = timeout; _readonly = readOnly; _password = password; + _log = logger; _journalEnabled = _readonly ? false : journalEnabled; // readonly? no journal _journalFilename = Path.Combine(Path.GetDirectoryName(_filename), @@ -54,6 +56,8 @@ public bool Initialize() if(_stream.Length == 0) { + _log.Debug(Logger.DISK, "initialize new datafile"); + return true; } else @@ -73,6 +77,7 @@ public void Lock() this.TryExec(() => { _lockLength = _stream.Length; _stream.Lock(0, _lockLength); + _log.Debug(Logger.DISK, "lock datafile"); }); } @@ -82,6 +87,7 @@ public void Lock() public void Unlock() { _stream.Unlock(0, _lockLength); + _log.Debug(Logger.DISK, "unlock datafile"); } #endregion @@ -119,6 +125,8 @@ public byte[] ReadPage(uint pageID) _stream.Read(buffer, 0, BasePage.PAGE_SIZE); }); + _log.Debug(Logger.DISK, "read page #{0:0000}", pageID); + return buffer; } @@ -136,6 +144,8 @@ public void WritePage(uint pageID, byte[] buffer) } _stream.Write(buffer, 0, BasePage.PAGE_SIZE); + + _log.Debug(Logger.DISK, "write page #{0:0000}", pageID); } #endregion @@ -156,6 +166,8 @@ public void WriteJournal(uint pageID, byte[] data) }); } + _log.Debug(Logger.JOURNAL, "write page #{0:0000}", pageID); + // just write original bytes in order that are changed _journal.Write(data, 0, BasePage.PAGE_SIZE); } @@ -174,6 +186,8 @@ public void CommitJournal(long fileSize) _journal.Flush(); } + _log.Debug(Logger.JOURNAL, "journal file commited"); + // fileSize parameter tell me final size of data file - helpful to extend first datafile _stream.SetLength(fileSize); } @@ -190,6 +204,8 @@ public void DeleteJournal() // remove journal file File.Delete(_journalFilename); + + _log.Debug(Logger.JOURNAL, "journal file deleted"); } } @@ -212,6 +228,8 @@ private void TryRecovery() { var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); + _log.Debug(Logger.RECOVERY, "detected journal file"); + // test if journal was finish if(finish == 1) { @@ -222,6 +240,8 @@ private void TryRecovery() journal.Close(); File.Delete(_journalFilename); + + _log.Debug(Logger.RECOVERY, "journal file deleted - datafile is recoreved"); }); } @@ -251,8 +271,12 @@ private void Recovery(FileStream journal) // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); _stream.Write(buffer, 0, BasePage.PAGE_SIZE); + + _log.Debug(Logger.RECOVERY, "recovering page #{0:0000}", pageID); } + _log.Debug(Logger.RECOVERY, "resize datafile to {0}", fileSize); + // redim filesize if grow more than original before rollback _stream.SetLength(fileSize); } @@ -285,6 +309,8 @@ private void TryExec(Action action) } } + _log.Error(Logger.DISK, "timeout disk access"); + throw LiteException.LockTimeout(_timeout); } @@ -300,7 +326,7 @@ private void OpenExclusiveFile(string filename, Action success) } catch (FileNotFoundException) { - // do nothing - no journal, no recovery + // do nothing - no journal file, no recovery } catch (IOException ex) { diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 7434db56f..1a8024792 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -165,6 +165,7 @@ + diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs new file mode 100644 index 000000000..01941b520 --- /dev/null +++ b/LiteDB/Utils/Logger.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace LiteDB +{ + /// + /// A internal class to log/debug intenal commands + /// + public class Logger + { + public const string DISK = "DISK"; + public const string JOURNAL = "JOURNAL"; + public const string RECOVERY = "RECOVERY"; + public const string COMMAND = "COMMAND"; + public const string INDEX = "INDEX"; + public const string QUERY = "QUERY"; + + public Stopwatch DiskRead = new Stopwatch(); + public Stopwatch DiskWrite = new Stopwatch(); + public Stopwatch Serialize = new Stopwatch(); + public Stopwatch Deserialize = new Stopwatch(); + + public TextWriter Output = Console.Out; + + public bool Enabled = false; + + public void Reset() + { + } + + internal void Debug(string category, string format, params object[] args) + { + } + + internal void Info(string category, string format, params object[] args) + { + } + + internal void Timer(string format, params object[] args) + { + } + + internal void Error(string category, Exception ex, string caller) + { + } + + internal void Error(string category, string text) + { + } + + } +} From 9916d993860e71bd571b221038b8a86550df72d5 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 00:56:11 -0200 Subject: [PATCH 64/91] Implementing logger --- LiteDB.Shell/Commands/Debug.cs | 24 +++++++++ LiteDB.Shell/LiteDB.Shell.csproj | 1 + LiteDB/Core/Collections/Insert.cs | 4 +- LiteDB/Core/LiteDatabase.cs | 3 ++ LiteDB/DbEngine/Disks/FileDiskService.cs | 20 +++++--- LiteDB/DbEngine/Pages/BasePage.cs | 2 + LiteDB/Utils/Logger.cs | 65 +++++++++++++++++++----- 7 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 LiteDB.Shell/Commands/Debug.cs diff --git a/LiteDB.Shell/Commands/Debug.cs b/LiteDB.Shell/Commands/Debug.cs new file mode 100644 index 000000000..54750a2a4 --- /dev/null +++ b/LiteDB.Shell/Commands/Debug.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; + +namespace LiteDB.Shell.Commands +{ + internal class Debug : ConsoleCommand + { + public override bool IsCommand(StringScanner s) + { + return s.Scan(@"debug\s*").Length > 0; + } + + public override void Execute(LiteShell shell, StringScanner s, Display d, InputCommand input) + { + var sb = new StringBuilder(); + var enabled = !(s.Scan(@"off\s*").Length > 0); + + shell.Database.Log.Level = enabled ? Logger.FULL : Logger.NONE; + } + } +} diff --git a/LiteDB.Shell/LiteDB.Shell.csproj b/LiteDB.Shell/LiteDB.Shell.csproj index 101196e1c..39923a2f5 100644 --- a/LiteDB.Shell/LiteDB.Shell.csproj +++ b/LiteDB.Shell/LiteDB.Shell.csproj @@ -53,6 +53,7 @@ + diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 9af70c5d6..608b6fd02 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -21,7 +21,7 @@ public BsonValue Insert(T document) _engine.InsertDocuments(_name, new BsonDocument[] { doc }); - _log.Debug("COMMAND", "inserted document _id = {0}", doc["_id"]); + _log.Debug(Logger.COMMAND, "inserted document _id = {0}", doc["_id"]); return doc["_id"]; } @@ -47,7 +47,7 @@ private IEnumerable GetBsonDocs(IEnumerable docs) var doc = _mapper.ToDocument(document); - _log.Debug("COMMAND", "inserted document _id = {0}", doc["_id"]); + _log.Debug(Logger.COMMAND, "inserted document _id = {0}", doc["_id"]); yield return doc; } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 6674cb713..e9cab643c 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -34,9 +34,12 @@ public LiteDatabase(string connectionString) var timeout = str.GetValue("timeout", new TimeSpan(0, 1, 0)); var readOnly = str.GetValue("readonly", false); var password = str.GetValue("password", null); + var log = str.GetValue("log", 0); if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); + _log.Level = log; + // initialize engine creating a new FileDiskService for data access _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password, _log)); _mapper = BsonMapper.Global; diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index 55deee739..04c3c1c1d 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -54,6 +54,8 @@ public bool Initialize() _readonly ? FileShare.Read : FileShare.ReadWrite, BasePage.PAGE_SIZE); + _log.Debug(Logger.DISK, "open datafile '{0}', page size {1}", Path.GetFileName(_filename), BasePage.PAGE_SIZE); + if(_stream.Length == 0) { _log.Debug(Logger.DISK, "initialize new datafile"); @@ -163,13 +165,15 @@ public void WriteJournal(uint pageID, byte[] data) this.TryExec(() => { _journal = new FileStream(_journalFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); + + _log.Debug(Logger.JOURNAL, "journal file created '{0}'", Path.GetFileName(_journalFilename)); }); } - _log.Debug(Logger.JOURNAL, "write page #{0:0000}", pageID); - // just write original bytes in order that are changed _journal.Write(data, 0, BasePage.PAGE_SIZE); + + _log.Debug(Logger.JOURNAL, "write page #{0:0000}", pageID); } public void CommitJournal(long fileSize) @@ -184,9 +188,9 @@ public void CommitJournal(long fileSize) // flush all journal file data to disk _journal.Flush(); - } - _log.Debug(Logger.JOURNAL, "journal file commited"); + _log.Debug(Logger.JOURNAL, "journal file commited"); + } // fileSize parameter tell me final size of data file - helpful to extend first datafile _stream.SetLength(fileSize); @@ -228,7 +232,7 @@ private void TryRecovery() { var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); - _log.Debug(Logger.RECOVERY, "detected journal file"); + _log.Info(Logger.RECOVERY, "detected journal file"); // test if journal was finish if(finish == 1) @@ -241,7 +245,7 @@ private void TryRecovery() File.Delete(_journalFilename); - _log.Debug(Logger.RECOVERY, "journal file deleted - datafile is recoreved"); + _log.Info(Logger.RECOVERY, "journal file deleted - datafile is recovered"); }); } @@ -275,7 +279,7 @@ private void Recovery(FileStream journal) _log.Debug(Logger.RECOVERY, "recovering page #{0:0000}", pageID); } - _log.Debug(Logger.RECOVERY, "resize datafile to {0}", fileSize); + _log.Info(Logger.RECOVERY, "resize datafile to {0}", fileSize); // redim filesize if grow more than original before rollback _stream.SetLength(fileSize); @@ -309,7 +313,7 @@ private void TryExec(Action action) } } - _log.Error(Logger.DISK, "timeout disk access"); + _log.Error(Logger.DISK, "timeout disk access after {0}", _timeout); throw LiteException.LockTimeout(_timeout); } diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index 9cd784f9c..d3df9cb2b 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -141,6 +141,8 @@ public static BasePage ReadPage(byte[] buffer) page.ReadHeader(reader); page.ReadContent(reader); + page.DiskData = buffer; + return page; } diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs index 01941b520..aee951126 100644 --- a/LiteDB/Utils/Logger.cs +++ b/LiteDB/Utils/Logger.cs @@ -13,44 +13,81 @@ namespace LiteDB /// public class Logger { - public const string DISK = "DISK"; - public const string JOURNAL = "JOURNAL"; - public const string RECOVERY = "RECOVERY"; - public const string COMMAND = "COMMAND"; - public const string INDEX = "INDEX"; - public const string QUERY = "QUERY"; + public const byte COMMAND = 2; + public const byte RECOVERY = 4; + public const byte QUERY = 8; + public const byte INDEX = 16; + public const byte JOURNAL = 32; + public const byte DISK = 64; + public const byte CACHE = 128; + + public const byte ERROR = 1; + public const byte INFO = 2; + public const byte DEBUG = 4; + + public const byte NONE = 0; + public const byte FULL = 255; public Stopwatch DiskRead = new Stopwatch(); public Stopwatch DiskWrite = new Stopwatch(); public Stopwatch Serialize = new Stopwatch(); public Stopwatch Deserialize = new Stopwatch(); - public TextWriter Output = Console.Out; + public byte Component { get; set; } + public byte Level { get; set; } - public bool Enabled = false; + public Action WriteLine = (text) => + { + var aux = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.WriteLine(text); + Console.ForegroundColor = aux; + }; - public void Reset() + public Logger() { + this.Component = FULL; + this.Level = NONE; } - internal void Debug(string category, string format, params object[] args) + public void Reset() { } - internal void Info(string category, string format, params object[] args) + internal void Error(byte component, string format, params object[] args) { + this.Write('E', component, string.Format(format, args)); } - internal void Timer(string format, params object[] args) + internal void Info(byte component, string format, params object[] args) { + this.Write('I', component, string.Format(format, args)); } - internal void Error(string category, Exception ex, string caller) + internal void Debug(byte component, string format, params object[] args) { + this.Write('D', component, string.Format(format, args)); } - internal void Error(string category, string text) + internal void Write(char severity, byte component, string text) { + if((severity & this.Level) == 0 || (component & this.Component) == 0) return; + + var comp = + component == COMMAND ? "COMMAND" : + component == RECOVERY ? "RECOVERY" : + component == QUERY ? "QUERY" : + component == INDEX ? "INDEX" : + component == JOURNAL ? "JOURNAL" : + component == DISK ? "DISK" : "CACHE"; + + var msg = string.Format("{0:HH:mm:ss.ffff} {1} [{2}] {3}", + DateTime.Now, + severity, + comp, + text); + + WriteLine(msg); } } From e04cdef8f3e32526c9f55d6e88d6be886848b223 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 01:30:42 -0200 Subject: [PATCH 65/91] Implement logger --- LiteDB/Core/LiteDatabase.cs | 6 +-- LiteDB/DbEngine/DbEngine.cs | 8 +++- LiteDB/DbEngine/Disks/FileDiskService.cs | 23 ++++++---- LiteDB/DbEngine/Services/CacheService.cs | 42 +++++++------------ .../DbEngine/Services/TransactionService.cs | 2 +- LiteDB/Utils/Logger.cs | 38 +++++++---------- 6 files changed, 53 insertions(+), 66 deletions(-) diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index e9cab643c..fff92e9a7 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -41,14 +41,14 @@ public LiteDatabase(string connectionString) _log.Level = log; // initialize engine creating a new FileDiskService for data access - _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password, _log)); + _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password, _log), _log); _mapper = BsonMapper.Global; } public LiteDatabase(Stream stream) { // initialize engine using StreamDisk - _engine = new DbEngine(new StreamDiskService(stream)); + _engine = new DbEngine(new StreamDiskService(stream), _log); _mapper = BsonMapper.Global; } @@ -57,7 +57,7 @@ public LiteDatabase(Stream stream) /// public LiteDatabase(IDiskService diskService, BsonMapper mapper) { - _engine = new DbEngine(diskService); + _engine = new DbEngine(diskService, _log); _mapper = mapper; } diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index 29399301d..cb6559068 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -15,6 +15,8 @@ internal partial class DbEngine : IDisposable { #region Services instances + private Logger _log; + private CacheService _cache; private IDiskService _disk; @@ -31,8 +33,10 @@ internal partial class DbEngine : IDisposable private object _locker = new object(); - public DbEngine(IDiskService disk) + public DbEngine(IDiskService disk, Logger log) { + _log = log; + _disk = disk; var isNew = _disk.Initialize(); @@ -42,7 +46,7 @@ public DbEngine(IDiskService disk) _disk.WritePage(0, new HeaderPage().WritePage()); } - _cache = new CacheService(); + _cache = new CacheService(_log); _pager = new PageService(_disk, _cache); _indexer = new IndexService(_pager); _data = new DataService(_pager); diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index 04c3c1c1d..385a6613c 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -15,6 +15,11 @@ internal class FileDiskService : IDiskService /// private const int JOURNAL_FINISH_POSITION = 19; + /// + /// Position, on page, about page type + /// + private const int PAGE_TYPE = 4; + private FileStream _stream; private string _filename; private long _lockLength; @@ -127,7 +132,7 @@ public byte[] ReadPage(uint pageID) _stream.Read(buffer, 0, BasePage.PAGE_SIZE); }); - _log.Debug(Logger.DISK, "read page #{0:0000}", pageID); + _log.Debug(Logger.DISK, "read page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); return buffer; } @@ -147,14 +152,14 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); - _log.Debug(Logger.DISK, "write page #{0:0000}", pageID); + _log.Debug(Logger.DISK, "write page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); } #endregion #region Journal file - public void WriteJournal(uint pageID, byte[] data) + public void WriteJournal(uint pageID, byte[] buffer) { if(_journalEnabled == false) return; @@ -171,9 +176,9 @@ public void WriteJournal(uint pageID, byte[] data) } // just write original bytes in order that are changed - _journal.Write(data, 0, BasePage.PAGE_SIZE); + _journal.Write(buffer, 0, BasePage.PAGE_SIZE); - _log.Debug(Logger.JOURNAL, "write page #{0:0000}", pageID); + _log.Debug(Logger.JOURNAL, "write page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); } public void CommitJournal(long fileSize) @@ -232,7 +237,7 @@ private void TryRecovery() { var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); - _log.Info(Logger.RECOVERY, "detected journal file"); + _log.Debug(Logger.RECOVERY, "detected journal file"); // test if journal was finish if(finish == 1) @@ -245,7 +250,7 @@ private void TryRecovery() File.Delete(_journalFilename); - _log.Info(Logger.RECOVERY, "journal file deleted - datafile is recovered"); + _log.Debug(Logger.RECOVERY, "journal file deleted - datafile is recovered"); }); } @@ -279,7 +284,7 @@ private void Recovery(FileStream journal) _log.Debug(Logger.RECOVERY, "recovering page #{0:0000}", pageID); } - _log.Info(Logger.RECOVERY, "resize datafile to {0}", fileSize); + _log.Debug(Logger.RECOVERY, "resize datafile to {0}", fileSize); // redim filesize if grow more than original before rollback _stream.SetLength(fileSize); @@ -313,7 +318,7 @@ private void TryExec(Action action) } } - _log.Error(Logger.DISK, "timeout disk access after {0}", _timeout); + _log.Error("timeout disk access after {0}", _timeout); throw LiteException.LockTimeout(_timeout); } diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 34b7a2288..094961407 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -18,12 +18,15 @@ internal class CacheService : IDisposable private SortedDictionary _cache; private SortedDictionary _dirty; + private Logger _log; + public Action MarkAsDirtyAction = (page) => { }; - public CacheService() + public CacheService(Logger log) { _cache = new SortedDictionary(); _dirty = new SortedDictionary(); + _log = log; } /// @@ -32,7 +35,14 @@ public CacheService() public BasePage GetPage(uint pageID) { // check for in dirty list - if not found, check in cache list - return _dirty.GetOrDefault(pageID, null) ?? _cache.GetOrDefault(pageID, null); + var page = _dirty.GetOrDefault(pageID, null) ?? _cache.GetOrDefault(pageID, null); + + if (page != null) + { + _log.Debug(Logger.CACHE, "get page #{0:0000} ({1})", page.PageID, page.PageType); + } + + return page; } /// @@ -85,33 +95,9 @@ public bool Clear() _cache.Clear(); } - return hasDirty; - } + _log.Debug(Logger.CACHE, "clear cache ({0} dirty pages)", _dirty.Count); - /// - /// Set all pages as clear and remove them from dirty list - /// - public void ClearDirty() - { - lock(_cache) - { - //foreach (var page in _dirty.Values) - //{ - // page.IsDirty = false; - - // // remove all non-header/collection pages (this can be optional in future) - // if (page.PageType == PageType.Extend || - // page.PageType == PageType.Empty || - // page.PageType == PageType.Index || - // page.PageType == PageType.Data) - // { - // _cache.Remove(page.PageID); - // } - //} - - _cache.Clear(); - _dirty.Clear(); - } + return hasDirty; } public bool HasDirtyPages { get { return _dirty.Count() > 0; } } diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 245923bf3..68a6bf620 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -67,7 +67,7 @@ public void Commit() _disk.DeleteJournal(); // set all dirty pages as clear on cache - _cache.ClearDirty(); + _cache.Clear(); } // unlock datafile diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs index aee951126..e72649a3f 100644 --- a/LiteDB/Utils/Logger.cs +++ b/LiteDB/Utils/Logger.cs @@ -9,10 +9,12 @@ namespace LiteDB { /// - /// A internal class to log/debug intenal commands + /// A logger class to log all information about database. Used with levels. Level = 0 - 255 /// public class Logger { + public const byte NONE = 0; + public const byte ERROR = 1; public const byte COMMAND = 2; public const byte RECOVERY = 4; public const byte QUERY = 8; @@ -20,20 +22,16 @@ public class Logger public const byte JOURNAL = 32; public const byte DISK = 64; public const byte CACHE = 128; - - public const byte ERROR = 1; - public const byte INFO = 2; - public const byte DEBUG = 4; - - public const byte NONE = 0; - public const byte FULL = 255; + public const byte FULL = 255; public Stopwatch DiskRead = new Stopwatch(); public Stopwatch DiskWrite = new Stopwatch(); public Stopwatch Serialize = new Stopwatch(); public Stopwatch Deserialize = new Stopwatch(); - public byte Component { get; set; } + /// + /// To full logger use Logger.FULL or any combination of Logger constants + /// public byte Level { get; set; } public Action WriteLine = (text) => @@ -46,7 +44,6 @@ public class Logger public Logger() { - this.Component = FULL; this.Level = NONE; } @@ -54,26 +51,22 @@ public void Reset() { } - internal void Error(byte component, string format, params object[] args) - { - this.Write('E', component, string.Format(format, args)); - } - - internal void Info(byte component, string format, params object[] args) + public void Error(string format, params object[] args) { - this.Write('I', component, string.Format(format, args)); + this.Write(ERROR, string.Format(format, args)); } - internal void Debug(byte component, string format, params object[] args) + public void Debug(byte component, string format, params object[] args) { - this.Write('D', component, string.Format(format, args)); + this.Write(component, string.Format(format, args)); } - internal void Write(char severity, byte component, string text) + internal void Write(byte component, string text) { - if((severity & this.Level) == 0 || (component & this.Component) == 0) return; + if((component & this.Level) == 0) return; var comp = + component == ERROR ? "ERROR" : component == COMMAND ? "COMMAND" : component == RECOVERY ? "RECOVERY" : component == QUERY ? "QUERY" : @@ -81,9 +74,8 @@ internal void Write(char severity, byte component, string text) component == JOURNAL ? "JOURNAL" : component == DISK ? "DISK" : "CACHE"; - var msg = string.Format("{0:HH:mm:ss.ffff} {1} [{2}] {3}", + var msg = string.Format("{0:HH:mm:ss.ffff} [{1}] {2}", DateTime.Now, - severity, comp, text); From eb17fbf906faf0d35c1b4d14b1ef6514a3493474 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 07:32:55 -0200 Subject: [PATCH 66/91] Better way to get list item type #77 --- .../Serializer/Mapper/BsonMapper.Serialize.cs | 6 +---- LiteDB/Serializer/Mapper/Reflection.cs | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index c2ecabf46..46198488b 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -101,11 +101,7 @@ private BsonValue Serialize(Type type, object obj, int depth) // check if is a list or array else if (obj is IEnumerable) { - var itemType = type.IsArray ? - type.GetElementType() : - (type.IsGenericType ? type.GetGenericArguments()[0] : typeof(object)); - - return this.SerializeArray(itemType, obj as IEnumerable, depth); + return this.SerializeArray(Reflection.GetListItemType(obj), obj as IEnumerable, depth); } // otherwise serialize as a plain object else diff --git a/LiteDB/Serializer/Mapper/Reflection.cs b/LiteDB/Serializer/Mapper/Reflection.cs index a235b0bbf..e511ef427 100644 --- a/LiteDB/Serializer/Mapper/Reflection.cs +++ b/LiteDB/Serializer/Mapper/Reflection.cs @@ -131,11 +131,6 @@ public static Dictionary GetProperties(Type type, Func)) + { + return i.GetGenericArguments()[0]; + } + } + + return typeof(object); + } + /// /// Returns true if Type is any kind of Array/IList/ICollection/.... /// From e04369b08d7de807652cab014dd0178cac65c959 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 10:54:18 -0200 Subject: [PATCH 67/91] Logging --- LiteDB-TODO.txt | 22 ++++++- LiteDB/Core/Collections/Insert.cs | 4 -- LiteDB/Core/LiteDatabase.cs | 15 +---- LiteDB/DbEngine/Actions/Collection.cs | 4 ++ LiteDB/DbEngine/Actions/Delete.cs | 2 + LiteDB/DbEngine/Actions/Find.cs | 2 + LiteDB/DbEngine/Actions/Index.cs | 4 ++ LiteDB/DbEngine/Actions/Insert.cs | 2 + LiteDB/DbEngine/Actions/Update.cs | 2 + LiteDB/DbEngine/DbEngine.cs | 8 ++- LiteDB/DbEngine/Disks/FileDiskService.cs | 73 ++++++++++++++---------- LiteDB/DbEngine/Services/CacheService.cs | 16 +----- LiteDB/DbEngine/Services/IndexService.cs | 13 ++++- LiteDB/Utils/Logger.cs | 44 ++++---------- 14 files changed, 111 insertions(+), 100 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index f9a9498bb..5263eb0d2 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -3,6 +3,8 @@ - Implementar FindAndModify/UpdateQuery - O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() - Debbuger? Acho importante no teste da cache +- LazyLoad para engine +- Remover todos que encontrar String.Format (concatena na mão) - Unit Teste devem sempre usar MemoryStream() (exceto concorrencia e/ou algo especifico pra disco) @@ -100,7 +102,25 @@ db.Logger.Verbose("text", arg0, arg1); [] 2014-11-03T18:28:32.450-0500 I NETWORK [initandlisten] waiting for connections on port 27017 -Components: +Level Description +F Fatal +E Error +W Warning +I Informational, for Verbosity Level of 0 +D Debug, for All Verbosity Levels > 0 + +LEVEL + - NONE + - ERROR + - RECOVERY + - COMMAND + - QUERY + - INDEX + - JOURNAL + - DISK + - CACHE + - FULL + ACCESS COMMAND CONTROL diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 608b6fd02..124a4d84e 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -21,8 +21,6 @@ public BsonValue Insert(T document) _engine.InsertDocuments(_name, new BsonDocument[] { doc }); - _log.Debug(Logger.COMMAND, "inserted document _id = {0}", doc["_id"]); - return doc["_id"]; } @@ -47,8 +45,6 @@ private IEnumerable GetBsonDocs(IEnumerable docs) var doc = _mapper.ToDocument(document); - _log.Debug(Logger.COMMAND, "inserted document _id = {0}", doc["_id"]); - yield return doc; } } diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index fff92e9a7..4e8728f17 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -27,21 +27,8 @@ public partial class LiteDatabase : IDisposable /// public LiteDatabase(string connectionString) { - var str = new ConnectionString(connectionString); - - var filename = str.GetValue("filename", ""); - var journal = str.GetValue("journal", true); - var timeout = str.GetValue("timeout", new TimeSpan(0, 1, 0)); - var readOnly = str.GetValue("readonly", false); - var password = str.GetValue("password", null); - var log = str.GetValue("log", 0); - - if(string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException("filename"); - - _log.Level = log; - // initialize engine creating a new FileDiskService for data access - _engine = new DbEngine(new FileDiskService(filename, journal, timeout, readOnly, password, _log), _log); + _engine = new DbEngine(new FileDiskService(connectionString, _log), _log); _mapper = BsonMapper.Global; } diff --git a/LiteDB/DbEngine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs index 670204fd5..64511639e 100644 --- a/LiteDB/DbEngine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -28,6 +28,8 @@ public bool DropCollection(string colName) { if(col == null) return false; + _log.Write(Logger.COMMAND, "drop collection {0}", colName); + _collections.Drop(col); return true; @@ -43,6 +45,8 @@ public bool RenameCollection(string colName, string newName) { if(col == null) return false; + _log.Write(Logger.COMMAND, "rename collection '{0}' -> '{1}'", colName, newName); + // change collection name and save col.CollectionName = newName; diff --git a/LiteDB/DbEngine/Actions/Delete.cs b/LiteDB/DbEngine/Actions/Delete.cs index c5793cd6d..c0007960a 100644 --- a/LiteDB/DbEngine/Actions/Delete.cs +++ b/LiteDB/DbEngine/Actions/Delete.cs @@ -26,6 +26,8 @@ public int DeleteDocuments(string colName, Query query) foreach (var node in nodes) { + _log.Write(Logger.COMMAND, "delete document on '{0}' :: _id = {1}", colName, node.Key); + // read dataBlock (do not read all extend pages, i will not use) var dataBlock = _data.Read(node.DataBlock, false); diff --git a/LiteDB/DbEngine/Actions/Find.cs b/LiteDB/DbEngine/Actions/Find.cs index 913634236..fc2fb4719 100644 --- a/LiteDB/DbEngine/Actions/Find.cs +++ b/LiteDB/DbEngine/Actions/Find.cs @@ -29,6 +29,8 @@ public IEnumerable Find(string colName, Query query, int skip = 0, // for each document, read data and deserialize as document foreach (var node in nodes) { + _log.Write(Logger.QUERY, "read document on '{0}' :: _id = {1}", colName, node.Key); + var dataBlock = _data.Read(node.DataBlock, true); var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; diff --git a/LiteDB/DbEngine/Actions/Index.cs b/LiteDB/DbEngine/Actions/Index.cs index f9c27c123..c3fc909eb 100644 --- a/LiteDB/DbEngine/Actions/Index.cs +++ b/LiteDB/DbEngine/Actions/Index.cs @@ -19,6 +19,8 @@ public bool EnsureIndex(string colName, string field, IndexOptions options) // check if index already exists if (col.GetIndex(field) != null) return false; + _log.Write(Logger.COMMAND, "create index on '{0}' :: '{1}' unique: {2}", colName, field, options.Unique); + // create index head var index = _indexer.CreateIndex(col); @@ -70,6 +72,8 @@ public bool DropIndex(string colName, string field) // no index, no drop if (index == null) return false; + _log.Write(Logger.COMMAND, "drop index on '{0}' :: '{1}'", colName, field); + // delete all data pages + indexes pages _indexer.DropIndex(index); diff --git a/LiteDB/DbEngine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs index 3ab507b99..b3695721c 100644 --- a/LiteDB/DbEngine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -34,6 +34,8 @@ public int InsertDocuments(string colName, IEnumerable docs) throw LiteException.InvalidDataType("_id", id); } + _log.Write(Logger.COMMAND, "insert document on '{0}' :: _id = {1}", colName, id); + // serialize object var bytes = BsonSerializer.Serialize(doc); diff --git a/LiteDB/DbEngine/Actions/Update.cs b/LiteDB/DbEngine/Actions/Update.cs index 7d9e05aa0..200b9b3b1 100644 --- a/LiteDB/DbEngine/Actions/Update.cs +++ b/LiteDB/DbEngine/Actions/Update.cs @@ -26,6 +26,8 @@ public int UpdateDocuments(string colName, IEnumerable docs) // normalize id before find var id = doc["_id"].Normalize(col.PK.Options); + _log.Write(Logger.COMMAND, "update document on '{0}' :: _id = ", colName, id); + // find indexNode from pk index var indexNode = _indexer.Find(col.PK, id, false, Query.Ascending); diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index cb6559068..704ecfa18 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -36,7 +36,6 @@ internal partial class DbEngine : IDisposable public DbEngine(IDiskService disk, Logger log) { _log = log; - _disk = disk; var isNew = _disk.Initialize(); @@ -46,7 +45,7 @@ public DbEngine(IDiskService disk, Logger log) _disk.WritePage(0, new HeaderPage().WritePage()); } - _cache = new CacheService(_log); + _cache = new CacheService(); _pager = new PageService(_disk, _cache); _indexer = new IndexService(_pager); _data = new DataService(_pager); @@ -66,6 +65,8 @@ private CollectionPage GetCollectionPage(string name, bool addIfNotExits) if(col == null && addIfNotExits) { + _log.Write(Logger.COMMAND, "create new collection '{0}'", name); + col = _collections.Add(name); } @@ -90,8 +91,9 @@ private T Transaction(string colName, bool addIfNotExists, Func("filename", ""); + var journalEnabled = str.GetValue("journal", true); + _timeout = str.GetValue("timeout", new TimeSpan(0, 1, 0)); + _readonly = str.GetValue("readonly", false); + _password = str.GetValue("password", null); + var level = str.GetValue("log", null); + + if (string.IsNullOrWhiteSpace(_filename)) throw new ArgumentNullException("filename"); + + // setup log + log-level + _log = log; + if(level.HasValue) _log.Level = level.Value; _journalEnabled = _readonly ? false : journalEnabled; // readonly? no journal _journalFilename = Path.Combine(Path.GetDirectoryName(_filename), @@ -52,6 +61,8 @@ public FileDiskService(string filename, bool journalEnabled, TimeSpan timeout, b /// public bool Initialize() { + _log.Write(Logger.DISK, "open datafile '{0}', page size {1}", Path.GetFileName(_filename), BasePage.PAGE_SIZE); + // open data file (r/w or r) _stream = new FileStream(_filename, _readonly ? FileMode.Open : FileMode.OpenOrCreate, @@ -59,11 +70,9 @@ public bool Initialize() _readonly ? FileShare.Read : FileShare.ReadWrite, BasePage.PAGE_SIZE); - _log.Debug(Logger.DISK, "open datafile '{0}', page size {1}", Path.GetFileName(_filename), BasePage.PAGE_SIZE); - if(_stream.Length == 0) { - _log.Debug(Logger.DISK, "initialize new datafile"); + _log.Write(Logger.DISK, "initialize new datafile"); return true; } @@ -83,8 +92,8 @@ public void Lock() { this.TryExec(() => { _lockLength = _stream.Length; + _log.Write(Logger.DISK, "lock datafile"); _stream.Lock(0, _lockLength); - _log.Debug(Logger.DISK, "lock datafile"); }); } @@ -93,8 +102,8 @@ public void Lock() /// public void Unlock() { + _log.Write(Logger.DISK, "unlock datafile"); _stream.Unlock(0, _lockLength); - _log.Debug(Logger.DISK, "unlock datafile"); } #endregion @@ -132,7 +141,7 @@ public byte[] ReadPage(uint pageID) _stream.Read(buffer, 0, BasePage.PAGE_SIZE); }); - _log.Debug(Logger.DISK, "read page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); + _log.Write(Logger.DISK, "read page #{0:0000} :: {1}", pageID, (PageType)buffer[PAGE_TYPE]); return buffer; } @@ -144,6 +153,8 @@ public void WritePage(uint pageID, byte[] buffer) { var position = (long)pageID * (long)BasePage.PAGE_SIZE; + _log.Write(Logger.DISK, "write page #{0:0000} :: {1}", pageID, (PageType)buffer[PAGE_TYPE]); + // position cursor if (_stream.Position != position) { @@ -151,8 +162,6 @@ public void WritePage(uint pageID, byte[] buffer) } _stream.Write(buffer, 0, BasePage.PAGE_SIZE); - - _log.Debug(Logger.DISK, "write page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); } #endregion @@ -169,16 +178,16 @@ public void WriteJournal(uint pageID, byte[] buffer) // open journal file in EXCLUSIVE mode this.TryExec(() => { - _journal = new FileStream(_journalFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); + _log.Write(Logger.JOURNAL, "create journal file"); - _log.Debug(Logger.JOURNAL, "journal file created '{0}'", Path.GetFileName(_journalFilename)); + _journal = new FileStream(_journalFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); }); } + _log.Write(Logger.JOURNAL, "write page #{0:0000} :: {1}", pageID, (PageType)buffer[PAGE_TYPE]); + // just write original bytes in order that are changed _journal.Write(buffer, 0, BasePage.PAGE_SIZE); - - _log.Debug(Logger.JOURNAL, "write page #{0:0000} ({1})", pageID, (PageType)buffer[PAGE_TYPE]); } public void CommitJournal(long fileSize) @@ -187,14 +196,14 @@ public void CommitJournal(long fileSize) if(_journal != null) { + _log.Write(Logger.JOURNAL, "commit journal file"); + // write a mark (byte 1) to know when journal is finish // after that, if found a non-exclusive-open journal file, must be recovery _journal.WriteByte(JOURNAL_FINISH_POSITION, 1); // flush all journal file data to disk _journal.Flush(); - - _log.Debug(Logger.JOURNAL, "journal file commited"); } // fileSize parameter tell me final size of data file - helpful to extend first datafile @@ -207,14 +216,14 @@ public void DeleteJournal() if(_journal != null) { + _log.Write(Logger.JOURNAL, "delete journal file"); + // close journal stream and delete file _journal.Dispose(); _journal = null; // remove journal file File.Delete(_journalFilename); - - _log.Debug(Logger.JOURNAL, "journal file deleted"); } } @@ -235,22 +244,26 @@ private void TryRecovery() // if I can open journal file, test FINISH_POSITION. If no journal, do not call action() this.OpenExclusiveFile(_journalFilename, (journal) => { - var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); + _log.Write(Logger.RECOVERY, "journal file detected"); - _log.Debug(Logger.RECOVERY, "detected journal file"); + var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); // test if journal was finish if(finish == 1) { this.Recovery(journal); } + else + { + _log.Write(Logger.RECOVERY, "journal file are not commited, no recovery"); + } // close stream for delete file journal.Close(); File.Delete(_journalFilename); - _log.Debug(Logger.RECOVERY, "journal file deleted - datafile is recovered"); + _log.Write(Logger.RECOVERY, "recovery finish"); }); } @@ -269,8 +282,10 @@ private void Recovery(FileStream journal) // read pageID (first 4 bytes) var pageID = BitConverter.ToUInt32(buffer, 0); + _log.Write(Logger.RECOVERY, "recover page #{0:0000}", pageID); + // if header, read all byte (to get original filesize) - if(pageID == 0) + if (pageID == 0) { var header = (HeaderPage)BasePage.ReadPage(buffer); @@ -280,11 +295,9 @@ private void Recovery(FileStream journal) // write in stream _stream.Seek(pageID * BasePage.PAGE_SIZE, SeekOrigin.Begin); _stream.Write(buffer, 0, BasePage.PAGE_SIZE); - - _log.Debug(Logger.RECOVERY, "recovering page #{0:0000}", pageID); } - _log.Debug(Logger.RECOVERY, "resize datafile to {0}", fileSize); + _log.Write(Logger.RECOVERY, "resize datafile to {0} bytes", fileSize); // redim filesize if grow more than original before rollback _stream.SetLength(fileSize); @@ -318,7 +331,7 @@ private void TryExec(Action action) } } - _log.Error("timeout disk access after {0}", _timeout); + _log.Write(Logger.ERROR, "timeout disk access after {0}", _timeout); throw LiteException.LockTimeout(_timeout); } diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 094961407..484d753a5 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -18,15 +18,12 @@ internal class CacheService : IDisposable private SortedDictionary _cache; private SortedDictionary _dirty; - private Logger _log; - public Action MarkAsDirtyAction = (page) => { }; - public CacheService(Logger log) + public CacheService() { _cache = new SortedDictionary(); _dirty = new SortedDictionary(); - _log = log; } /// @@ -35,14 +32,7 @@ public CacheService(Logger log) public BasePage GetPage(uint pageID) { // check for in dirty list - if not found, check in cache list - var page = _dirty.GetOrDefault(pageID, null) ?? _cache.GetOrDefault(pageID, null); - - if (page != null) - { - _log.Debug(Logger.CACHE, "get page #{0:0000} ({1})", page.PageID, page.PageType); - } - - return page; + return _dirty.GetOrDefault(pageID, null) ?? _cache.GetOrDefault(pageID, null); } /// @@ -95,8 +85,6 @@ public bool Clear() _cache.Clear(); } - _log.Debug(Logger.CACHE, "clear cache ({0} dirty pages)", _dirty.Count); - return hasDirty; } diff --git a/LiteDB/DbEngine/Services/IndexService.cs b/LiteDB/DbEngine/Services/IndexService.cs index 56534c2a0..aa0461ed9 100644 --- a/LiteDB/DbEngine/Services/IndexService.cs +++ b/LiteDB/DbEngine/Services/IndexService.cs @@ -112,14 +112,23 @@ private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) // now, let's link my index node on right place var cur = this.GetNode(index.HeadNode); + // using as cache last + IndexNode cache = null; + // scan from top left for (var i = IndexNode.MAX_LEVEL_LENGTH - 1; i >= 0; i--) { + // get cache for last node + cache = cache != null && cache.Position.Equals(cur.Next[i]) ? cache : this.GetNode(cur.Next[i]); + // for(; ; ) { ... } - for (; cur.Next[i].IsEmpty == false; cur = this.GetNode(cur.Next[i])) + for (; cur.Next[i].IsEmpty == false; cur = cache) { + // get cache for last node + cache = cache != null && cache.Position.Equals(cur.Next[i]) ? cache : this.GetNode(cur.Next[i]); + // read next node to compare - var diff = this.GetNode(cur.Next[i]).Key.CompareTo(key); + var diff = cache.Key.CompareTo(key); // if unique and diff = 0, throw index exception (must rollback transaction - others nodes can be dirty) if (diff == 0 && index.Options.Unique) throw LiteException.IndexDuplicateKey(index.Field, key); diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs index e72649a3f..d1db8a84a 100644 --- a/LiteDB/Utils/Logger.cs +++ b/LiteDB/Utils/Logger.cs @@ -10,36 +10,29 @@ namespace LiteDB { /// /// A logger class to log all information about database. Used with levels. Level = 0 - 255 + /// All log will be trigger before operation execute (better for log) + /// Do not use string.Format, it's too slow: http://stackoverflow.com/questions/16432/string-output-format-or-concat-in-c /// public class Logger { public const byte NONE = 0; public const byte ERROR = 1; - public const byte COMMAND = 2; - public const byte RECOVERY = 4; - public const byte QUERY = 8; - public const byte INDEX = 16; + public const byte RECOVERY = 2; + public const byte COMMAND = 4; + public const byte QUERY = 16; public const byte JOURNAL = 32; public const byte DISK = 64; - public const byte CACHE = 128; public const byte FULL = 255; - public Stopwatch DiskRead = new Stopwatch(); - public Stopwatch DiskWrite = new Stopwatch(); - public Stopwatch Serialize = new Stopwatch(); - public Stopwatch Deserialize = new Stopwatch(); - /// - /// To full logger use Logger.FULL or any combination of Logger constants + /// To full logger use Logger.FULL or any combination of Logger constants like Level = Logger.ERROR | Logger.COMMAND | Logger.DISK /// public byte Level { get; set; } public Action WriteLine = (text) => { - var aux = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine(text); - Console.ForegroundColor = aux; }; public Logger() @@ -51,33 +44,20 @@ public void Reset() { } - public void Error(string format, params object[] args) - { - this.Write(ERROR, string.Format(format, args)); - } - - public void Debug(byte component, string format, params object[] args) - { - this.Write(component, string.Format(format, args)); - } - - internal void Write(byte component, string text) + public void Write(byte component, string message, params object[] args) { if((component & this.Level) == 0) return; + var text = string.Format(message, args); + var comp = component == ERROR ? "ERROR" : - component == COMMAND ? "COMMAND" : component == RECOVERY ? "RECOVERY" : - component == QUERY ? "QUERY" : - component == INDEX ? "INDEX" : + component == COMMAND ? "COMMAND" : component == JOURNAL ? "JOURNAL" : - component == DISK ? "DISK" : "CACHE"; + component == DISK ? "DISK" : "QUERY"; - var msg = string.Format("{0:HH:mm:ss.ffff} [{1}] {2}", - DateTime.Now, - comp, - text); + var msg = DateTime.Now.ToString("HH:mm:ss.ffff") + "[" + comp + "] " + text; WriteLine(msg); } From 8b4ca256f37b07342daeffc8bbb6230d5650987e Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 11:27:25 -0200 Subject: [PATCH 68/91] Update my todo --- LiteDB-TODO.txt | 120 +++++++++-------------------------------- LiteDB/Utils/Logger.cs | 1 - 2 files changed, 24 insertions(+), 97 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 5263eb0d2..8044f8828 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,56 +1,32 @@ # LiteDB v.2 - TODO -- Implementar FindAndModify/UpdateQuery -- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() -- Debbuger? Acho importante no teste da cache - LazyLoad para engine -- Remover todos que encontrar String.Format (concatena na mão) - -- Unit Teste devem sempre usar MemoryStream() (exceto concorrencia e/ou algo especifico pra disco) - +- Implementar FindAndModify/UpdateQuery +- Debbuger? TextWriters para poder fazer log pra aquivo de forma mais elegante. Como implementar cor? Fazer igual ao shell? +- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() - Revisar help do Shell - saiu varias coisas -- Thread Safe/Process Safe - - Thread safe no BsonMapper static methods (Reflection) - - Lock total de read. - - Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) - - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - - Avaliar uso do ConcurrentDictionary na cache - Revisar BsonArray.Length não faz cache -- Fazer db.Debbuger? Com opção de Verbose, opções de avaliação de timing de performance... - db.Debugger.Write() = Stream(); - db.Debbgger.DiskRead | DiskWrite | Serialize | Deserialize | Command (maior) - (Counter { Count, TimeElipsed, Start(), Stop(), Reset() }) - db.Debugger.Enabled = false; - -## Cache - -Para saber qual caminho seguir em relação a cache, acho que precisa fazer branchs para testar: - -1) A "dev" como está: usa cache sem limites -2) Não usa cache nenhuma para leitura, apenas para dirty pages -3) Usa cache para salvar com limitador de tamanho. Vai gravando em disco conforme vai ficando dirty (grava a cada X paginas sujas) - -Preciso criar uma banchmark para testar os mais diversos tipos de situação: - -a) Criação de um banco grande de 1 unica coleção -b) Criação de um banco grande de 100 coleções diferentes -c) Criação de indice -d) Leitura de todos os registros -e) Leitura conforme indice (retorno poucos registros) - -> Para cada teste, deve usar documento grande/complexo e documento pequeno/simples -> Avaliar tempo/consumo total de memória -> Aproveitar os testes para fazer PAGE_SIZE de 8K tb - -?? Remover escrita ta header page no filedisk -?? pager.SetDirty(page) antes de alterar -?? Implementer SortDictionaryMRU -?? Implementar cache de 2 repos - -## DbRef - - Implementar DbRefAttribute -- Implementar testes +- ThreadSafe +- Unit tests + + +## Thread Safe / Process Safe +- Thread safe no BsonMapper static methods (Reflection) +- Lock total de read. +- Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) +- Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) +- Avaliar uso do ConcurrentDictionary na cache + +## Testes de Performance +- Fazer um script grande para testar no shell +- Focar no insert/delete/find?? FileStorage +- Testar sempre como release +- Sempre com journal? +===> Opções de melhorias +* Testar outros dicionarios na cache +* Testar BitConvert ao inves de unsafe +* PageSize 8K e 16K ## A pensar: @@ -67,14 +43,11 @@ e) Leitura conforme indice (retorno poucos registros) - Mapeador/Configuration estilo EF db.Configuration.Add(new CredorConfiguration()) -- Adicionar as operacoes do banco para um servico - manter no Collection apenas as chamadas + Linq conv + API - - DocumentService (_docsrv) * Insert/Update/Delete/Find - - EnsureIndex/Find - - Não contempla transacao ## Para versão 2 - Criar site simples, bacana, estilos: + novo site mongodb http://pouchdb.com/ vuejs.org https://github.com/vuejs/vuejs.org https://hexo.io/ (esse é bem simples e acho q atende bem) @@ -91,48 +64,3 @@ e) Leitura conforme indice (retorno poucos registros) - Utils - Linq, shell, fileStorage, fts - API - -db.Logger.Level = None, Error, Info, Verbose; -db.Logger.Output = Console.Out; - -db.Logger.Error(ex, "text"); -db.Logger.Info("text", arg0, arg1); -db.Logger.Verbose("text", arg0, arg1); - - [] -2014-11-03T18:28:32.450-0500 I NETWORK [initandlisten] waiting for connections on port 27017 - -Level Description -F Fatal -E Error -W Warning -I Informational, for Verbosity Level of 0 -D Debug, for All Verbosity Levels > 0 - -LEVEL - - NONE - - ERROR - - RECOVERY - - COMMAND - - QUERY - - INDEX - - JOURNAL - - DISK - - CACHE - - FULL - -ACCESS -COMMAND -CONTROL -INDEX -QUERY -STORAGE -JOURNAL - - - - -================================================================================================================ -================================================================================================================ - -private string json = "{\"suggestions\":[{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":true},{\"id\":296,\"name\":\"Arrumando as Malas\",\"description\":\"Atenção passageiros do voo Superplayer 042, embarque imediato pela lista de número 296 com destino à felicidade.\",\"key\":\"para-arrumar-malas\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/6/16e70ddd-8e8c-4acf-b8eb-a63d7ddb9a17.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/d/cd99fc5a-a6dc-451c-a7d8-0c84a7c061e2.jpg\"},\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"lastplayed\"},\"favorite\":false,\"updated\":false},{\"id\":840,\"name\":\"Manhãs Calminhas\",\"description\":\"Você pode estar de férias, desempregado, ser seu próprio chefe ou não ligar pra pontualidade. Dê o play aqui e aproveite essa manhã!\",\"key\":\"para-manhas-tranquilas-calma\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/9/19fbac76-26a0-4538-aeab-f813b6b99622.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/2/024b0f12-7c5f-4df0-9123-a799f439b415.jpg\"},\"color\":\"#828bc3\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":177,\"name\":\"Rock Gaúcho\",\"description\":\"Pega a chinoca, monta no cavalo\r\ne desbrava essa lista cheia de clássicos do rock gauchesco, tchê!\",\"key\":\"rock-gaucho\",\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"mostplayed\"},\"favorite\":false,\"updated\":false},{\"id\":670,\"name\":\"Acampando\",\"description\":\"Que tipo de pássaro você é? Se você for do tipo que gosta de acampar fora daquele som comercial, dê uma bicada aqui.\",\"key\":\"para-acampar\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d4e68cf-cd0f-4969-94ac-54cf4981794e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/1/c1513f1d-8af8-4c7e-9b62-beeaf0bee222.jpg\"},\"color\":\"#ce1560\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":201,\"name\":\"Lareira e chocolate quente\",\"description\":\"Músicas aconchegantes para viajar olhando pro fogo da lareira. Só cuidado pra não queimar a língua no chocolate quente.\",\"key\":\"inverno\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/90eed4e8-c9e8-43aa-b0d8-aa143e9ddaa4.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/7/67f9f311-32c4-442f-adb9-d6f4609419e8.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":860,\"name\":\"Sonhando Acordado\",\"description\":\"Para você que tá meio distante. Tem alguém aí?\",\"key\":\"sonhando-acordado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/e/aece78a9-26da-4ad9-b678-89e44078939c.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/c/bcf262b8-b928-4c7e-8e65-31390cddcfa1.jpg\"},\"color\":\"#48cc8a\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":731,\"name\":\"De Conchinha\",\"description\":\"A trilha ideal para aquele momento amorzinho de ficar perto, juntinho, com cabelo na cara e braço dormente. Mas é bom. <3\",\"key\":\"de-conchinha\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/1/7/1792d012-c5a3-40ea-b9c4-750c990ffdf8.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/a/2ac0a72e-bc10-4241-89b2-dc8205071a15.jpg\"},\"color\":\"#662d91\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":232,\"name\":\"Pedalando \",\"description\":\"Pra trocar o carro pela bike e aproveitar a vida! =-)\",\"key\":\"bicicleta\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/1/615b5c82-2aeb-42cb-b8fd-9e6f6f25fcc7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/6/e/6efc656b-e6e8-4b5c-b840-b557d1887e63.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":15,\"name\":\"Indie Folk\",\"description\":\"Fogueiras, cabanas no meio do mato e essa playlist pra armar o cenário ideal. \",\"key\":\"indie-folk\",\"color\":\"#60c2c4\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":82,\"name\":\"Pegando no Sono\",\"description\":\"Notas perfeitamente combinadas pra você não precisar contar ovelhas.\",\"key\":\"para-dormir\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/e/9eb3d46f-7c40-44c4-9a25-f835d6a54958.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/4/8/48a41b36-e5de-4191-b685-7b220700c4bf.jpg\"},\"color\":\"#f7931e\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":true},{\"id\":357,\"name\":\"Coolzinhando\",\"description\":\"A melhor trilha para colocar em prática as receitas do Jamie Oliver. Sem pressão, só diversão.\",\"key\":\"coolzinhando\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/8/5/85e60977-d236-4522-ac31-ffb585ebb7af.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/0/d0c477cd-48f6-4329-8ac4-9db2d5e59c92.jpg\"},\"color\":\"#2cb2bf\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":355,\"name\":\"Café da Manhã\",\"description\":\"A trilha que deixará seu café da manhã tão perfeito quanto o de um comercial de margarina.\",\"key\":\"cafe-da-manha\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/b/3/b34b1dbe-de8d-40c3-a044-6c9c9152e0ca.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/2/2/2221fc45-8c08-49f9-a57a-c13f2bd3e7bc.jpg\"},\"color\":\"#e5176b\",\"sponsor\":{\"name\":\"CanecaTag\",\"link\":\"http://bit.ly/sp-canecatag-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/9/f/9ff99244-7ebe-4bf6-8c35-b1ac547eb97e.png\",\"logoReverse\":\"http://ads02.cdn.superplayer.fm/5/c/5cc12450-454c-41e1-ba42-c9ce4ac4c5a1.png\"},\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":296,\"name\":\"Arrumando as Malas\",\"key\":\"para-arrumar-malas\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false},{\"id\":361,\"name\":\"PicNic & Cupcakes\",\"description\":\"Músicas pra você curtir aquele solzinho do fim da tarde descalço na grama.\",\"key\":\"para-picnic\",\"color\":\"#f2a33b\",\"suggestion\":{\"reason\":\"relatedlastplayed\",\"playlist\":{\"id\":840,\"name\":\"Manhãs Calminhas\",\"key\":\"para-manhas-tranquilas-calma\",\"favorite\":false,\"updated\":false}},\"favorite\":false,\"updated\":false}],\"highlights\":[{\"id\":988,\"name\":\"Olla - The Balada Never Ends\",\"description\":\"Várias baladas, uma dica: com Olla, #TheBaladaNeverEnds. \",\"key\":\"olla-the-balada-never-ends\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/1/21ff4200-2f94-450b-9495-4db489954ea5.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/9/a9588b81-49e7-4bb0-9ce9-b0e3d7292878.jpg\"},\"color\":\"#0d0e00\",\"sponsor\":{\"name\":\"Olla\",\"link\":\"http://bit.ly/sp-olla-versite-outubro\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c610fc8d-5fc4-4a16-9883-3c53102a9709.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":744,\"name\":\"50 Tons de Blues\",\"description\":\"Pra quem prefere um blues ao cinza, e sente prazer é pelas guitarras de B. B. King.\",\"key\":\"50-tons-blues\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/d/0d7ff11e-3e67-4797-aed4-9e1839f3ec4f.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/a/6/a686d2f4-0cb1-4949-8ca2-ec9e8eaf8f42.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":927,\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"description\":\"Tem uma forma muito mais legal de garantir a força pra encarar o dia. Abrindo Fibz e dando play aqui!\",\"key\":\"fibz-fibra-pro-seu-dia-a-dia\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/6/e/6e1cb14e-1316-47f6-a773-2545fe28879f.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/f/bf68c15e-8344-4c1c-9e12-a62b96ec4b69.jpg\"},\"color\":\"#e39600\",\"link\":\"http://bit.ly/sp-fibz-versite\",\"sponsor\":{\"name\":\"Fibz - Fibra Pro Seu Dia a Dia \",\"link\":\"http://bit.ly/sp-fibz-versite\",\"logo\":\"http://ads00.cdn.superplayer.fm/c/6/c6cebf41-5743-459d-a26b-a2e2e9d42380.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1007,\"name\":\"Case 2015\",\"description\":\"Hard work beats talent. Assinantes do Superplayer tem 40% de desconto na maior feira de empreendedorismo da América Latina.\",\"key\":\"case-2015\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/d/c/dc244c66-59b9-4e86-801e-88e26b70e19f.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/9/0/900ebfee-fb3a-439c-a70c-be83c3f81d75.jpg\"},\"color\":\"#60c2c4\",\"link\":\"http://bit.ly/1O55ogq\",\"sponsor\":{\"name\":\"Case 2015\",\"link\":\"http://bit.ly/1O55ogq\",\"logo\":\"http://ads00.cdn.superplayer.fm/6/3/63cf6b21-8ed9-41e2-b1ca-3b57a26dfc5a.png\"},\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":979,\"name\":\"Unisinos - Escola Politécnica\",\"description\":\"A playlist com a cara da nossa escola: variada e cheia de qualidade.\",\"key\":\"unisinos-escola-politecnica\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/9/0/908242d2-8696-4112-a579-5d96c71ab3be.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/6/f65f63b1-0b8b-429a-ba72-04809ca57475.jpg\"},\"color\":\"#2fbbb1\",\"sponsor\":{\"name\":\"Unisinos - Escola Politécnica\",\"link\":\"http://bit.ly/spl-unisinos-politecnica\",\"logo\":\"http://ads01.cdn.superplayer.fm/d/8/d86e8544-15a8-494c-8a0d-b83e2d8d91ce.png\"},\"custom\":true,\"paid\":true,\"favorite\":false,\"updated\":false},{\"id\":1004,\"name\":\"Escorpião\",\"description\":\"Uma playlist intensa, pra ser ouvida com moderação (se você for escorpiano, favor ignorar esse conselho).\",\"key\":\"signo-escorpiao\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/5/1/5150bf52-a851-4f1c-8db0-de72309313ba.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/7/0/703e2d52-56f5-437f-9883-2b87ef6b17fc.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":999,\"name\":\"MBB - Música Bonitinha Brasileira\",\"description\":\"As musiquinhas nacionais mais fofas que você pode imaginar.\",\"key\":\"mbb\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/7/274a422c-00e3-49a8-8bbc-c4d027b7d49b.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/6/b6e1216a-3454-4d69-ba37-ad2f8acfe5c4.jpg\"},\"color\":\"#48cc8a\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":1008,\"name\":\"Acústico Cover\",\"description\":\"Versões fofinhas de grandes sucessos do pop e rock internacionais.\",\"key\":\"acustico-cover\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e0fb5d9-66fa-43fb-a363-e189f0d9464e.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/7/c7db7c2f-49e3-404c-bb5f-d76fe4be09e2.jpg\"},\"color\":\"#2cb2bf\",\"paid\":false,\"favorite\":false,\"updated\":true},{\"id\":129,\"name\":\"Nova Geração da MPB \",\"description\":\"Os novos nomes da MPB que vêm arrasando! Herdeiros ilegítimos e musicais de Caetano e Chico.\",\"key\":\"nova-geracao-da-mpb\",\"color\":\"#f7931e\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":756,\"name\":\"Top 50 Love\",\"description\":\"As 50 músicas mais amadas pelo público no último mês..\",\"key\":\"top-50-love\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/e/f/ef5fb493-86ca-44f7-a7bf-7fca81a5f94a.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/f/b/fb621e0d-3b5c-4733-909b-6e65984f874a.jpg\"},\"color\":\"#2cb2bf\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false},{\"id\":605,\"name\":\"Oktoberfest\",\"description\":\"Wir haben gehoert, dass, wenn Sie Deutsch nicht sprechen koennen, Sie einige Pints Weissbier trinken koennten um die Worte fliessend zu machen.\",\"key\":\"oktoberfest\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/c/d/cd2b4084-b4e8-435a-ab65-f28ac026d1b8.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/d/2/d21cbbc2-94b7-454d-b830-0c4ccaef31a7.jpg\"},\"color\":\"#ce1560\",\"custom\":true,\"paid\":false,\"favorite\":false,\"updated\":false}],\"alfred\":{\"playlists\":[{\"id\":855,\"name\":\"225 Músicas para a Viagem a Marte\",\"description\":\"Há vida em Marte? Posicione-se na cadeira e dê o sinal para o major Tom iniciar a decolagem. E boa viagem.\",\"key\":\"viagem-a-marte\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/0/e/0e396660-0a60-43dd-9375-3feafe6180d7.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/0/a/0a91d575-f680-4845-8675-e914af124463.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":961,\"name\":\"Vaporwave\",\"description\":\"Aqui a onda é: diferente, trash, retrô e experimental.\",\"key\":\"vaporwave\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/d/7d510dce-f524-48a3-bfbe-de1ac7d1b450.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/c/f/cf815082-24d2-4de9-a0d6-b6d8166bc6bc.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":203,\"name\":\"Estimulando a Criatividade\",\"description\":\"Pra dar um gás e te deixar pronto pro toró de palpite!\",\"key\":\"para-criatividade\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/7/6/76dd131b-affc-41ca-810d-598cb171d057.jpg\",\"2x1\":\"http://lists01.cdn.superplayer.fm/1/8/1818f82d-5c35-49f5-839e-ffe4cb18e864.jpg\"},\"color\":\"#f2a33b\",\"favorite\":false,\"updated\":true},{\"id\":783,\"name\":\"Brainstorming\",\"description\":\"Peraí, e se a gente fizesse uma playlist MAS que a pessoa conseguisse ouvir COM OS OLHOS?\",\"key\":\"brainstorming\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/4/1/418f4e59-102f-49e3-b91b-1282afc48e5d.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/5/35d5cc1d-035a-495b-9751-bb3c8eed6ba1.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":21,\"name\":\"Indie Rock\",\"description\":\"Pra navegar no submundo meio inacessível do rock independente. \",\"key\":\"indie-rock\",\"color\":\"#ce1560\",\"favorite\":false,\"updated\":false},{\"id\":312,\"name\":\"Focando no Trabalho\",\"description\":\"Melhor que Ritalina!\",\"key\":\"super-concentrado\",\"arts\":{\"2x2\":\"http://lists01.cdn.superplayer.fm/a/3/a349b1bb-eb7b-4027-b80f-40b2ed3faa3c.jpg\",\"2x1\":\"http://lists00.cdn.superplayer.fm/3/0/30801d94-e0cc-4eb8-97aa-1171dfe8e370.jpg\"},\"color\":\"#2cb2bf\",\"favorite\":false,\"updated\":false},{\"id\":935,\"name\":\"Rock no Escritório\",\"description\":\"Pra bater o pé e usar o material do escritório como bateria improvisada.\",\"key\":\"rock-no-escritorio\",\"arts\":{\"2x2\":\"http://lists02.cdn.superplayer.fm/2/5/2555a9c5-e197-4225-bf78-a8941361e75e.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/e/4/e4391e4b-99be-4532-8415-8414cb22da8c.jpg\"},\"color\":\"#48cc8a\",\"favorite\":false,\"updated\":false},{\"id\":210,\"name\":\"Livros e Jazz\",\"description\":\"Dá play e vá ler um livro. \",\"key\":\"para-ler-com-jazz\",\"arts\":{\"2x2\":\"http://lists00.cdn.superplayer.fm/f/b/fb4d5bfe-a006-4224-836e-b3cd091b69ec.jpg\",\"2x1\":\"http://lists02.cdn.superplayer.fm/b/d/bd17163d-82d6-4b23-9d55-83d85d93f83a.jpg\"},\"color\":\"#828bc3\",\"favorite\":false,\"updated\":false}]}}"; diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs index d1db8a84a..05dc9c2f8 100644 --- a/LiteDB/Utils/Logger.cs +++ b/LiteDB/Utils/Logger.cs @@ -11,7 +11,6 @@ namespace LiteDB /// /// A logger class to log all information about database. Used with levels. Level = 0 - 255 /// All log will be trigger before operation execute (better for log) - /// Do not use string.Format, it's too slow: http://stackoverflow.com/questions/16432/string-output-format-or-concat-in-c /// public class Logger { From f26845d65292384fa8365178f3dab831f04d555d Mon Sep 17 00:00:00 2001 From: mbdavid Date: Sun, 15 Nov 2015 20:43:35 -0200 Subject: [PATCH 69/91] Lazy load dbengine --- LiteDB-TODO.txt | 16 ++++++++++--- LiteDB/Core/LiteDatabase.cs | 41 +++++++++++++++++++-------------- LiteDB/DbEngine/Actions/Find.cs | 3 +++ LiteDB/Utils/Logger.cs | 17 +++++++------- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 8044f8828..b35e01a80 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,16 +1,14 @@ # LiteDB v.2 - TODO - LazyLoad para engine -- Implementar FindAndModify/UpdateQuery -- Debbuger? TextWriters para poder fazer log pra aquivo de forma mais elegante. Como implementar cor? Fazer igual ao shell? - O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() - Revisar help do Shell - saiu varias coisas +- FileLimit and Initial Size - Revisar BsonArray.Length não faz cache - Implementar DbRefAttribute - ThreadSafe - Unit tests - ## Thread Safe / Process Safe - Thread safe no BsonMapper static methods (Reflection) - Lock total de read. @@ -43,6 +41,18 @@ - Mapeador/Configuration estilo EF db.Configuration.Add(new CredorConfiguration()) +## V2 Features +- New DbEngine +- IDiskService +- New DbRef +- Entity<> mapper +- ThreadSafe +- New lock read/write +- Debugger +- Better journal : restore before crash +- ACID per document/collections +- Lazy load +* ByteArray, DbEngine, abstract BasePage ## Para versão 2 diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 4e8728f17..8272e0425 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -12,7 +12,7 @@ namespace LiteDB /// public partial class LiteDatabase : IDisposable { - private DbEngine _engine; + private Lazy _engine; private BsonMapper _mapper; @@ -23,20 +23,25 @@ public partial class LiteDatabase : IDisposable public Logger Log { get { return _log; } } /// - /// Starts LiteDB database using a connectionString + /// Starts LiteDB database using a connectionString for filesystem database /// public LiteDatabase(string connectionString) { - // initialize engine creating a new FileDiskService for data access - _engine = new DbEngine(new FileDiskService(connectionString, _log), _log); _mapper = BsonMapper.Global; + _engine = new Lazy( + () => new DbEngine(new FileDiskService(connectionString, _log), _log), + false); } + /// + /// Initialize database using any read/write Stream (like MemoryStream) + /// public LiteDatabase(Stream stream) { - // initialize engine using StreamDisk - _engine = new DbEngine(new StreamDiskService(stream), _log); _mapper = BsonMapper.Global; + _engine = new Lazy( + () => new DbEngine(new StreamDiskService(stream), _log), + false); } /// @@ -44,8 +49,10 @@ public LiteDatabase(Stream stream) /// public LiteDatabase(IDiskService diskService, BsonMapper mapper) { - _engine = new DbEngine(diskService, _log); _mapper = mapper; + _engine = new Lazy( + () => new DbEngine(diskService, _log), + false); } #region Collections @@ -57,7 +64,7 @@ public LiteDatabase(IDiskService diskService, BsonMapper mapper) public LiteCollection GetCollection(string name) where T : new() { - return new LiteCollection(name, _engine, _mapper, _log); + return new LiteCollection(name, _engine.Value, _mapper, _log); } /// @@ -66,7 +73,7 @@ public LiteCollection GetCollection(string name) /// Collection name (case insensitive) public LiteCollection GetCollection(string name) { - return new LiteCollection(name, _engine, _mapper, _log); + return new LiteCollection(name, _engine.Value, _mapper, _log); } /// @@ -74,7 +81,7 @@ public LiteCollection GetCollection(string name) /// public IEnumerable GetCollectionNames() { - return _engine.GetCollectionNames(); + return _engine.Value.GetCollectionNames(); } /// @@ -82,7 +89,7 @@ public IEnumerable GetCollectionNames() /// public bool CollectionExists(string name) { - return _engine.GetCollectionNames().Contains(name); + return _engine.Value.GetCollectionNames().Contains(name); } /// @@ -90,7 +97,7 @@ public bool CollectionExists(string name) /// public bool DropCollection(string name) { - return _engine.DropCollection(name); + return _engine.Value.DropCollection(name); } /// @@ -98,7 +105,7 @@ public bool DropCollection(string name) /// public bool RenameCollection(string oldName, string newName) { - return _engine.RenameCollection(oldName, newName); + return _engine.Value.RenameCollection(oldName, newName); } #endregion @@ -112,7 +119,7 @@ public bool RenameCollection(string oldName, string newName) /// public LiteFileStorage FileStorage { - get { return _fs ?? (_fs = new LiteFileStorage(_engine)); } + get { return _fs ?? (_fs = new LiteFileStorage(_engine.Value)); } } #endregion @@ -139,19 +146,19 @@ public BsonValue RunCommand(string command) internal string DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) { - return _engine.DumpPages(startPage, endPage).ToString(); + return _engine.Value.DumpPages(startPage, endPage).ToString(); } internal string DumpIndex(string colName, string field) { - return _engine.DumpIndex(colName, field).ToString(); + return _engine.Value.DumpIndex(colName, field).ToString(); } #endregion public void Dispose() { - _engine.Dispose(); + if(_engine.IsValueCreated) _engine.Value.Dispose(); } } } diff --git a/LiteDB/DbEngine/Actions/Find.cs b/LiteDB/DbEngine/Actions/Find.cs index fc2fb4719..d2bd2c7f4 100644 --- a/LiteDB/DbEngine/Actions/Find.cs +++ b/LiteDB/DbEngine/Actions/Find.cs @@ -9,6 +9,9 @@ namespace LiteDB { internal partial class DbEngine : IDisposable { + /// + /// Find for documents in a collection using Query definition + /// public IEnumerable Find(string colName, Query query, int skip = 0, int limit = int.MaxValue) { // get my collection page diff --git a/LiteDB/Utils/Logger.cs b/LiteDB/Utils/Logger.cs index 05dc9c2f8..2e219cbe9 100644 --- a/LiteDB/Utils/Logger.cs +++ b/LiteDB/Utils/Logger.cs @@ -28,7 +28,10 @@ public class Logger /// public byte Level { get; set; } - public Action WriteLine = (text) => + /// + /// Output function to write log - default is log in console + /// + public Action Output = (text) => { Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine(text); @@ -39,10 +42,9 @@ public Logger() this.Level = NONE; } - public void Reset() - { - } - + /// + /// Write log text to output using inside a component (statics const of Logger) + /// public void Write(byte component, string message, params object[] args) { if((component & this.Level) == 0) return; @@ -56,10 +58,9 @@ public void Write(byte component, string message, params object[] args) component == JOURNAL ? "JOURNAL" : component == DISK ? "DISK" : "QUERY"; - var msg = DateTime.Now.ToString("HH:mm:ss.ffff") + "[" + comp + "] " + text; + var msg = DateTime.Now.ToString("HH:mm:ss.ffff") + " [" + comp + "] " + text; - WriteLine(msg); + this.Output(msg); } - } } From e23ed9171d9e84cd8d00db05f899ed604cdb3202 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 16 Nov 2015 17:50:15 -0200 Subject: [PATCH 70/91] Fix minor changes --- LiteDB-TODO.txt | 2 ++ LiteDB/DbEngine/Actions/Insert.cs | 7 ++++++- LiteDB/DbEngine/Services/CacheService.cs | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index b35e01a80..a0c450646 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -8,6 +8,8 @@ - Implementar DbRefAttribute - ThreadSafe - Unit tests +- Colocar DbParams na header. _engine.GetParams(), _engine.SetParams(p) (limit 4k) + ## Thread Safe / Process Safe - Thread safe no BsonMapper static methods (Reflection) diff --git a/LiteDB/DbEngine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs index b3695721c..58040a9ae 100644 --- a/LiteDB/DbEngine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -17,9 +17,12 @@ public int InsertDocuments(string colName, IEnumerable docs) return this.Transaction(colName, true, (col) => { var count = 0; + var s = new System.Diagnostics.Stopwatch(); foreach (var doc in docs) { + s.Start(); + BsonValue id; // add ObjectId to _id if _id not found @@ -62,10 +65,12 @@ public int InsertDocuments(string colName, IEnumerable docs) // point my dataBlock dataBlock.IndexRef[index.Slot] = node.Position; } - + s.Stop(); count++; } + Console.WriteLine("===>" + s.ElapsedMilliseconds); + return count++; }); } diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 484d753a5..676404777 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -14,16 +14,16 @@ namespace LiteDB /// internal class CacheService : IDisposable { - // double cache structure - use Sort dictionary for get pages in order (fast to store in sequence on disk) - private SortedDictionary _cache; - private SortedDictionary _dirty; + // double cache structure - use normal dictionary for get pages in order + private Dictionary _cache; + private Dictionary _dirty; public Action MarkAsDirtyAction = (page) => { }; public CacheService() { - _cache = new SortedDictionary(); - _dirty = new SortedDictionary(); + _cache = new Dictionary(); + _dirty = new Dictionary(); } /// From 586a2bbe77afb95899319d1892e6973ae17b20d6 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 16 Nov 2015 20:39:26 -0200 Subject: [PATCH 71/91] Initial size and Limit size --- LiteDB-TODO.txt | 1 + LiteDB/DbEngine/Actions/Insert.cs | 6 ---- LiteDB/DbEngine/DbEngine.cs | 2 +- LiteDB/DbEngine/Disks/FileDiskService.cs | 18 +++++++++++ LiteDB/Utils/ConnectionString.cs | 41 ++++++++++++++++++++++++ LiteDB/Utils/LiteException.cs | 5 +++ 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index a0c450646..229c895d3 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -9,6 +9,7 @@ - ThreadSafe - Unit tests - Colocar DbParams na header. _engine.GetParams(), _engine.SetParams(p) (limit 4k) +- Review das exception (remover o q nao existe) - Passar o Logger por parametro no ctor do LiteExeption (e no rollback?) ## Thread Safe / Process Safe diff --git a/LiteDB/DbEngine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs index 58040a9ae..f0d2bbf92 100644 --- a/LiteDB/DbEngine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -17,12 +17,9 @@ public int InsertDocuments(string colName, IEnumerable docs) return this.Transaction(colName, true, (col) => { var count = 0; - var s = new System.Diagnostics.Stopwatch(); foreach (var doc in docs) { - s.Start(); - BsonValue id; // add ObjectId to _id if _id not found @@ -65,12 +62,9 @@ public int InsertDocuments(string colName, IEnumerable docs) // point my dataBlock dataBlock.IndexRef[index.Slot] = node.Position; } - s.Stop(); count++; } - Console.WriteLine("===>" + s.ElapsedMilliseconds); - return count++; }); } diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index 704ecfa18..75f8ec123 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -93,7 +93,7 @@ private T Transaction(string colName, bool addIfNotExists, Func("timeout", new TimeSpan(0, 1, 0)); _readonly = str.GetValue("readonly", false); _password = str.GetValue("password", null); + _initialSize = str.GetFileSize("initial size", 0); + _limitSize = str.GetFileSize("limit size", 0); var level = str.GetValue("log", null); if (string.IsNullOrWhiteSpace(_filename)) throw new ArgumentNullException("filename"); + if (_initialSize > 0 && _initialSize < (BasePage.PAGE_SIZE * 10)) throw new ArgumentException("initial size too low"); + if (_limitSize > 0 && _limitSize < (BasePage.PAGE_SIZE * 10)) throw new ArgumentException("limit size too low"); + if (_initialSize > 0 && _limitSize > 0 && _initialSize > _limitSize) throw new ArgumentException("limit size less than initial size"); // setup log + log-level _log = log; @@ -74,6 +81,14 @@ public bool Initialize() { _log.Write(Logger.DISK, "initialize new datafile"); + // if has a initial size, reserve this space + if(_initialSize > 0) + { + _log.Write(Logger.DISK, "initial datafile size {0}", _initialSize); + + _stream.SetLength(_initialSize); + } + return true; } else @@ -192,6 +207,9 @@ public void WriteJournal(uint pageID, byte[] buffer) public void CommitJournal(long fileSize) { + // checks if new fileSize will exceed limit size + if(_limitSize > 0 && fileSize > _limitSize) throw LiteException.FileSizeExceeds(_limitSize); + if (_journalEnabled == false) return; if(_journal != null) diff --git a/LiteDB/Utils/ConnectionString.cs b/LiteDB/Utils/ConnectionString.cs index 693c82687..48baa5e8c 100644 --- a/LiteDB/Utils/ConnectionString.cs +++ b/LiteDB/Utils/ConnectionString.cs @@ -47,5 +47,46 @@ public T GetValue(string key, T defaultValue) throw new LiteException("Invalid connection string value type for " + key); } } + + /// + /// Get a value from a key converted in file size format: "1gb", "10 mb", "80000" + /// + /// + /// + /// + public long GetFileSize(string key, long defaultSize) + { + var size = this.GetValue(key, ""); + + if (size.Length == 0) return defaultSize; + + var match = Regex.Match(size, @"^(\d+)\s*([tgmk])?(b|byte|bytes)?$", RegexOptions.IgnoreCase); + + if (!match.Success) return 0; + + var num = Convert.ToInt64(match.Groups[1].Value); + + switch (match.Groups[2].Value.ToLower()) + { + case "t": return num * 1024L * 1024L * 1024L * 1024L; + case "g": return num * 1024L * 1024L * 1024L; + case "m": return num * 1024L * 1024L; + case "k": return num * 1024L; + case "": return num; + } + + return 0; + } + + public static String FormatFileSize(long byteCount) + { + string[] suf = { "B", "KB", "MB", "GB", "TB" }; //Longs run out around EB + if (byteCount == 0) + return "0" + suf[0]; + long bytes = Math.Abs(byteCount); + int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + double num = Math.Round(bytes / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * num).ToString() + suf[place]; + } } } diff --git a/LiteDB/Utils/LiteException.cs b/LiteDB/Utils/LiteException.cs index c85cf2283..2cbd54000 100644 --- a/LiteDB/Utils/LiteException.cs +++ b/LiteDB/Utils/LiteException.cs @@ -52,6 +52,11 @@ public static LiteException InvalidDatabaseVersion(int version) return new LiteException(105, "Invalid database version: {0}", version); } + public static LiteException FileSizeExceeds(long limit) + { + return new LiteException(105, "Database size exceeds limit of {0}", ConnectionString.FormatFileSize(limit)); + } + public static LiteException CollectionLimitExceeded(int limit) { return new LiteException(106, "This database exceeded the maximum limit of collections: {0}", limit); From 2717dca1c47620ec94e49e4ddd1da8cb042214a5 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Mon, 16 Nov 2015 22:19:36 -0200 Subject: [PATCH 72/91] Add site template --- LiteDB-TODO.txt | 2 +- LiteDB.org/assets/css/litedb.css | 72 +++++++++++++++++++ .../assets/images/logo_litedb.svg | 0 LiteDB.org/index.html | 58 +++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 LiteDB.org/assets/css/litedb.css rename logo_litedb.svg => LiteDB.org/assets/images/logo_litedb.svg (100%) create mode 100644 LiteDB.org/index.html diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 229c895d3..bc6c349bc 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -9,7 +9,7 @@ - ThreadSafe - Unit tests - Colocar DbParams na header. _engine.GetParams(), _engine.SetParams(p) (limit 4k) -- Review das exception (remover o q nao existe) - Passar o Logger por parametro no ctor do LiteExeption (e no rollback?) +- Review das exception (remover o q nao existe) - Passar o Logger por parametro no ctor do LiteExeption (e no rollback vai duplicar?) ## Thread Safe / Process Safe diff --git a/LiteDB.org/assets/css/litedb.css b/LiteDB.org/assets/css/litedb.css new file mode 100644 index 000000000..d0cf94c73 --- /dev/null +++ b/LiteDB.org/assets/css/litedb.css @@ -0,0 +1,72 @@ +body { +} + +.nav-top { + font-weight: bold; + list-style: none; + color:#BCBEC0; + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; +} + +.nav-top li { + display: inline-block; + padding: 0px 20px; +} + +.nav-top a { + color:#BCBEC0; + font-weight: bold; + padding: 10px 0px; +} + +.nav-top a:hover { + text-decoration: none; + border-bottom: 3px solid #BCBEC0; +} + +.install { + text-align: center; + margin: 30px; + +} +.install span { + background-color: #283542; + color: #BCBEC0;; + font-family: Courier New, Courier, monospace; + font-size: 14px; + padding: 15px; +} +.install a { + background-color: #0e83cd; + font-size: 14px; + padding: 15px; + color: white; +} + + +h1 { + text-align: center; + color:#BCBEC0; + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 40px; + line-height: 1.15; + font-weight: 300; +} + +h2 { + text-align: center; + color:#BCBEC0; + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 20px; + line-height: 1.15; + font-weight: 300; + margin: 30px 0px 40px;; +} + +.social-share{ + width: 100px !important; + height: 20px !important; + border: none; + overflow: hidden; + margin: 2px; +} \ No newline at end of file diff --git a/logo_litedb.svg b/LiteDB.org/assets/images/logo_litedb.svg similarity index 100% rename from logo_litedb.svg rename to LiteDB.org/assets/images/logo_litedb.svg diff --git a/LiteDB.org/index.html b/LiteDB.org/index.html new file mode 100644 index 000000000..81c194d0b --- /dev/null +++ b/LiteDB.org/index.html @@ -0,0 +1,58 @@ + + + + + LiteDB :: A .NET embedded NoSQL database + + + + + + + + + +
+
+
+
+ +
+
+ +
+
+
+
+
+

A embedded NoSQL database for .NET

+

A open source database with zero configuration and 100% write in C#

+
+ > Install-Package LiteDB +
+
+
+ + +
+
+
+
+
+ Feature 1 +
+
+ Feature 2 +
+
+ Feature 3 +
+
+
+ + \ No newline at end of file From b62a4a1441677621648a03a9558f2f9ecf9946ed Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 17 Nov 2015 11:47:56 -0200 Subject: [PATCH 73/91] Site template --- LiteDB.org/assets/css/litedb.css | 72 ------ LiteDB.org/css/litedb.css | 222 ++++++++++++++++++ LiteDB.org/css/prism.css | 139 +++++++++++ .../{assets/images => img}/logo_litedb.svg | 0 LiteDB.org/index.html | 148 +++++++++--- LiteDB.org/js/prism.js | 4 + 6 files changed, 476 insertions(+), 109 deletions(-) delete mode 100644 LiteDB.org/assets/css/litedb.css create mode 100644 LiteDB.org/css/litedb.css create mode 100644 LiteDB.org/css/prism.css rename LiteDB.org/{assets/images => img}/logo_litedb.svg (100%) create mode 100644 LiteDB.org/js/prism.js diff --git a/LiteDB.org/assets/css/litedb.css b/LiteDB.org/assets/css/litedb.css deleted file mode 100644 index d0cf94c73..000000000 --- a/LiteDB.org/assets/css/litedb.css +++ /dev/null @@ -1,72 +0,0 @@ -body { -} - -.nav-top { - font-weight: bold; - list-style: none; - color:#BCBEC0; - font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; -} - -.nav-top li { - display: inline-block; - padding: 0px 20px; -} - -.nav-top a { - color:#BCBEC0; - font-weight: bold; - padding: 10px 0px; -} - -.nav-top a:hover { - text-decoration: none; - border-bottom: 3px solid #BCBEC0; -} - -.install { - text-align: center; - margin: 30px; - -} -.install span { - background-color: #283542; - color: #BCBEC0;; - font-family: Courier New, Courier, monospace; - font-size: 14px; - padding: 15px; -} -.install a { - background-color: #0e83cd; - font-size: 14px; - padding: 15px; - color: white; -} - - -h1 { - text-align: center; - color:#BCBEC0; - font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; - font-size: 40px; - line-height: 1.15; - font-weight: 300; -} - -h2 { - text-align: center; - color:#BCBEC0; - font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; - font-size: 20px; - line-height: 1.15; - font-weight: 300; - margin: 30px 0px 40px;; -} - -.social-share{ - width: 100px !important; - height: 20px !important; - border: none; - overflow: hidden; - margin: 2px; -} \ No newline at end of file diff --git a/LiteDB.org/css/litedb.css b/LiteDB.org/css/litedb.css new file mode 100644 index 000000000..b8bab27e9 --- /dev/null +++ b/LiteDB.org/css/litedb.css @@ -0,0 +1,222 @@ +body { + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; + margin: 0px; +} + +header { + background-color: #171F26; + padding: 0px 0px; + display: flex; + justify-content: center; +} + +.container { + display: flex; + width: 1200px; + justify-content: space-between; + margin: 0px auto; +} + +.logo { + width: 120px; + height: 50px; + margin: 10px 0px; +} + +.nav { + font-weight: bold; + list-style: none; + color:#BCBEC0; + margin: 0px; + display: flex; +} + +.nav li { + padding: 0px 5px; +} + +.nav a { + display: block; + color:#BCBEC0; + font-weight: bold; + padding: 25px 15px; + text-decoration: none; + border-bottom: 3px solid transparent; +} + +.nav a:hover { + text-decoration: none; + border-bottom-color: #BCBEC0; + background-color: rgba(0,0,0,.5) +} + +/*--*/ + +.banner { + background-color: #171F26; + padding: 20px; +} + +.banner h1 { + text-align: center; + color:#BCBEC0; + font-size: 40px; + line-height: 1.15; + margin: 0px; + padding: 80px 0px 20px; +} + +.banner h2 { + text-align: center; + color:#BCBEC0; + font-size: 20px; + line-height: 1.15; + font-weight: 300; + margin: 0px; +} + +.install { + text-align: center; + padding: 60px 0px; +} + +.install span { + background-color: #283542; + color: #BCBEC0; + font-family: Courier New, Courier, monospace; + font-size: 14px; + padding: 15px; + display: inline-block; +} + +.install a { + background-color: #0e83cd; + font-size: 14px; + padding: 15px; + color: white; +} + +.social-buttons { + text-align: center; +} + +.social-share{ + width: 100px !important; + height: 20px !important; + border: none; + overflow: hidden; + margin: 2px; +} + +/* -- */ + +.feature { + flex:1; + margin: 40px 30px 40px 0px; + padding: 0px; +} +.container .feature:last-child { + margin-right: 0px; +} + +.feature h1 { + font-size: 28px; + color: #171F26; +} + +.feature .fa { + margin: 0px 15px; + color: #171F26; + width: 20px; +} + +.feature p { + margin: 0px 0px 0px 50px; + line-height: 1.5em; +} + +.feature ul { + margin: 0px 0px 0px 10px; + line-height: 1.5em; +} + +.feature-pack { + background-color: yellow; +} + +/* -- */ + +.banner-title { + background-color: #f3f3f3; + padding: 40px; + margin-bottom: 40px; + font-size: 36px; + color: #171F26; +} + +.banner-title .container { + justify-content: flex-start; +} + +.banner-title h2 { + display: inline-block; + font-size: 18px; + color: gray; + font-weight: 300; + margin-left: 30px; +} +/* -- */ +.examples { + flex: 1; + width: 200px; +} +.examples ul { + list-style: none; + margin: 0px; + padding: 0px; +} +.examples li { + padding: 20px 30px; +} +.examples li i { + margin-right: 10px; +} +.examples li.active { + background-color: #F5F2F0; + color: black; +} +.examples li.active a { + color: black; +} +.examples a { + color: #0e83cd; + text-decoration: none; +} +.examples a:hover { + text-decoration: underline; +} + +pre { + flex: 2; + margin: 0px !important; +} + + +/* -- */ +footer { + text-align: center; + background-color: #171F26; + color:#BCBEC0; + margin-top: 20px; + padding: 30px; +} + +footer a { + color:#BCBEC0; + text-decoration: none; +} + +footer a:hover { + color:#BCBEC0; + text-decoration: underline; +} \ No newline at end of file diff --git a/LiteDB.org/css/prism.css b/LiteDB.org/css/prism.css new file mode 100644 index 000000000..0ea8217ce --- /dev/null +++ b/LiteDB.org/css/prism.css @@ -0,0 +1,139 @@ +/* http://prismjs.com/download.html?themes=prism&languages=clike+csharp */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #a67f59; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/LiteDB.org/assets/images/logo_litedb.svg b/LiteDB.org/img/logo_litedb.svg similarity index 100% rename from LiteDB.org/assets/images/logo_litedb.svg rename to LiteDB.org/img/logo_litedb.svg diff --git a/LiteDB.org/index.html b/LiteDB.org/index.html index 81c194d0b..1964ccca6 100644 --- a/LiteDB.org/index.html +++ b/LiteDB.org/index.html @@ -5,54 +5,128 @@ LiteDB :: A .NET embedded NoSQL database - - + + - - + + + -
+ + + +
-
-
- -
-
- -
-
+ +
-
-
-

A embedded NoSQL database for .NET

-

A open source database with zero configuration and 100% write in C#

+ + + @@ -240,6 +240,7 @@

And much more...

$($(this).attr("data-code")).show(); if($(this).attr("data-code") == "#codeshell") setTimeout(function() { $(".textbox").focus(); }, 0); }); + $("#clickshell").click(function() { $('[data-code="#codeshell"]').click(); }); diff --git a/LiteDB.org/js/shell.js b/LiteDB.org/js/shell.js index 753e68c07..613ef7be5 100644 --- a/LiteDB.org/js/shell.js +++ b/LiteDB.org/js/shell.js @@ -1,49 +1,90 @@ -var ver = 3; -var db = localStorage.getItem('db'); - -if (!db || db.substr(0, 3) != 'db' + ver) { - db = 'db' + ver + '_' + (new Date()).getTime(); - localStorage.setItem('db', db); -} - -var hist = []; -var idx = 0; - -$('.shell').on('click', function () { $('.textbox').focus(); }); - -$('.textbox').on('keyup', function (e) { - - var $el = $(this); - var $prompt = $el.parent(); - var text = $el.val().trim(); - - if (e.keyCode == 13 && text.length > 0) { - - $prompt.before('
' + text + '
'); - $el.val('') - $prompt.hide(); - hist.push(text); - idx = hist.length; - - //$.post('api.ashx', { db: db, cmd: text }).done(function (result) { - var result = "Not implemented yet. Run on http://litedb.azurewebsites.net" - var css = /^ERROR:\s/.test(result) ? 'error' : 'result'; - $prompt.before('
' + $('
').text(result).html() + '
'); - $prompt.show(); - $el.focus(); - setTimeout(function () { - var div = $('.shell').get(0); - div.scrollTop = div.scrollHeight; - }, 100); - //}); - } - else if (e.keyCode == 38) { - idx = Math.max(0, idx - 1); - $el.val(hist[idx]); +(function() { + + var db = localStorage.getItem('db-v2'); + + if (!db) { + db = 'db_' + (new Date()).getTime(); + localStorage.setItem('db-v2', db); } - else if (e.keyCode == 40) { - idx = Math.min(hist.length - 1, idx + 1); - $el.val(hist[idx]); + + var hist = []; + var idx = 0; + + $('.shell').on('click', function () { $('.textbox').focus(); }); + + $('.textbox').on('keyup', function (e) { + + var $el = $(this); + var $prompt = $el.parent(); + var text = $el.val().trim(); + + if (e.keyCode == 13 && text.length > 0) { + + $prompt.before('
' + text + '
'); + $el.val('') + $prompt.hide(); + hist.push(text); + idx = hist.length; + + execute({ db: db, cmd: text }, function (result) { + var css = /^ERROR:\s/.test(result) ? 'error' : 'result'; + $prompt.before('
' + $('
').text(result).html() + '
'); + $prompt.show(); + $el.focus(); + setTimeout(function () { + var div = $('.shell').get(0); + div.scrollTop = div.scrollHeight; + }, 100); + }); + } + else if (e.keyCode == 38) { + idx = Math.max(0, idx - 1); + $el.val(hist[idx]); + } + else if (e.keyCode == 40) { + idx = Math.min(hist.length - 1, idx + 1); + $el.val(hist[idx]); + } + + }); + + function execute(params, cb) { + if (params.cmd == 'help') return cb(help); + var host = location.hostname == "localhost" ? "" : "http://litedb.azurewebsites.net/"; + + $.post(host + 'WebShell.ashx', params).done(function (result) { + cb(result); + }); } -}); + var help = +'Web Shell Commands - try offline version for more commands\n' + +'==========================================================\n' + +'\n' + +'> show collections\n' + +' List all collection in database\n' + +'> db..insert \n' + +' Insert a new document into collection\n' + +'> db..update \n' + +' Update a document inside collection\n' + +'> db..delete \n' + +' Delete documents using a filter clausule (see find)\n' + +'> db..find [top N] \n' + +' Show filtered documents based on index search\n' + +'> db..count \n' + +' Show count rows according query filter\n' + +'> db..ensureIndex [unique]\n' + +' Create a new index document field\n' + +' = [=|>|>=|<|<=|!=|like|between] \n' + +' Filter query syntax\n' + +' = ( [and|or] [and|or] ...)\n' + +' Multi queries syntax\n' + +'\n' + +'Try:\n' + +' > db.customers.insert { _id:1, name:\"John Doe\", age: 37 }\n' + +' > db.customers.ensureIndex name\n' + +' > db.customers.find name like \"John\"\n' + +' > db.customers.find limit 10 (name like \"John\" and _id between [0, 100])\n'; + +})(); + diff --git a/LiteDB.sln b/LiteDB.sln index 87b0f4736..11a0ac82d 100644 --- a/LiteDB.sln +++ b/LiteDB.sln @@ -9,6 +9,28 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Shell", "LiteDB.Shel EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Tests", "LiteDB.Tests\LiteDB.Tests.csproj", "{BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}" EndProject +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "LiteDB.org", "LiteDB.org\", "{FA39B5CD-49DD-4057-8714-66F24AA1CFF5}" + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + ProjectReferences = "{e808051a-83b7-4fa9-b004-d064ea162b60}|LiteDB.dll;" + Debug.AspNetCompiler.VirtualPath = "/localhost_50030" + Debug.AspNetCompiler.PhysicalPath = "LiteDB.org\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_50030\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_50030" + Release.AspNetCompiler.PhysicalPath = "LiteDB.org\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_50030\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "50030" + SlnRelativePath = "LiteDB.org\" + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +49,10 @@ Global {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.Build.0 = Release|Any CPU + {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LiteDB/Core/LiteDatabase.cs b/LiteDB/Core/LiteDatabase.cs index 2d0ce12e4..d9f7ca62d 100644 --- a/LiteDB/Core/LiteDatabase.cs +++ b/LiteDB/Core/LiteDatabase.cs @@ -124,26 +124,16 @@ public LiteFileStorage FileStorage #endregion - #region Shell - - private LiteShell _shell = null; + #region Utils /// - /// Run a shell command in current database. Returns a BsonValue as result + /// Reduce datafile size re-creating all collection in another datafile - return how many bytes are reduced. /// - public BsonValue RunCommand(string command) + public int Shrink() { - if (_shell == null) - { - _shell = new LiteShell(this); - } - return _shell.Run(command); + return _engine.Value.Shrink(); } - #endregion - - #region Dump - internal string DumpPages(uint startPage = 0, uint endPage = uint.MaxValue) { return _engine.Value.DumpPages(startPage, endPage).ToString(); @@ -154,19 +144,11 @@ internal string DumpIndex(string colName, string field) return _engine.Value.DumpIndex(colName, field).ToString(); } - #endregion - - /// - /// Reduce datafile size re-creating all collection in another datafile - return how many bytes are reduced. - /// - public int Shrink() - { - return _engine.Value.Shrink(); - } - public void Dispose() { if(_engine.IsValueCreated) _engine.Value.Dispose(); } + + #endregion } } From 98c93dae2023c064d9b0198e7049175740e7dd24 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Tue, 17 Nov 2015 21:55:47 -0200 Subject: [PATCH 78/91] Removing site --- LiteDB.org/App_Data/db_1447800291631.db | Bin 20480 -> 0 bytes LiteDB.org/WebShell.ashx | 61 ------ LiteDB.org/css/litedb.css | 239 ---------------------- LiteDB.org/css/prism.css | 139 ------------- LiteDB.org/img/logo_litedb.svg | 29 --- LiteDB.org/index.html | 258 ------------------------ LiteDB.org/js/prism.js | 4 - LiteDB.org/js/shell.js | 90 --------- LiteDB.sln | 26 --- 9 files changed, 846 deletions(-) delete mode 100644 LiteDB.org/App_Data/db_1447800291631.db delete mode 100644 LiteDB.org/WebShell.ashx delete mode 100644 LiteDB.org/css/litedb.css delete mode 100644 LiteDB.org/css/prism.css delete mode 100644 LiteDB.org/img/logo_litedb.svg delete mode 100644 LiteDB.org/index.html delete mode 100644 LiteDB.org/js/prism.js delete mode 100644 LiteDB.org/js/shell.js diff --git a/LiteDB.org/App_Data/db_1447800291631.db b/LiteDB.org/App_Data/db_1447800291631.db deleted file mode 100644 index df411c17437b24a83a51e859bb5c9036750d25bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI)!Ab)$5P;!i+k%2(?_P58vX3D>iAPUfq}XUzY!_^O4weA=3Z8gXzk|f_Bx|!zcPk0-B zKBNt>oGzlZ17!5p8jzd$Odp<-L>4=4Sj*)gB=!(S1Q0*~0R#|0009ILKmY**whKr- zFQvVf=mYn8rm3M^up%`j$+A^{i+!o+Rj|MwUAu5x14rrfKQzg3}!8e7-q zXOHuq0^MKG$OZ!4dT`UvmtoRJH=aI6ccm}HfB*srAb -using System; -using System.Web; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using LiteDB; -using LiteDB.Shell; -using LiteDB.Shell.Commands; - -public class WebShell : IHttpHandler -{ - private string[] CMDS = { "ShowCollections", "CollectionInsert", "CollectionUpdate", "CollectionDelete", "CollectionDrop", "CollectionFind", "CollectionMin", "CollectionMax", "CollectionCount", "CollectionEnsureIndex" }; - - public void ProcessRequest (HttpContext context) - { - var dbname = context.Request.Form["db"]; - var command = context.Request.Form["cmd"]; - - context.Response.AppendHeader("Access-Control-Allow-Origin", "litedb.org"); - - try - { - if (!Regex.IsMatch(dbname, @"^db_\d{13}$")) throw new ArgumentException("Invalid database name"); - - var filename = context.Server.MapPath("~/App_Data/" + dbname + ".db"); - - using (var db = new LiteDatabase("filename=" + filename + ";journal=false")) - { - var shell = new LiteShell(db); - - foreach(var cmd in shell.Commands.Keys.Except(CMDS).ToArray()) - { - shell.Commands.Remove(cmd); - } - - var result = shell.Run(command); - - if(result.IsNull) return; - - if(result.IsString) - { - context.Response.Write(result.AsString); - } - else - { - var json = JsonSerializer.Serialize(result, true, false); - context.Response.Write(json); - } - } - } - catch(Exception ex) - { - context.Response.Clear(); - context.Response.Write("ERROR: " + ex.Message); - } - } - - public bool IsReusable { get { return false; } } - -} diff --git a/LiteDB.org/css/litedb.css b/LiteDB.org/css/litedb.css deleted file mode 100644 index 26771a593..000000000 --- a/LiteDB.org/css/litedb.css +++ /dev/null @@ -1,239 +0,0 @@ -body { - font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; - margin: 0px; -} - -header { - background-color: #171F26; - padding: 0px 0px; - display: flex; - justify-content: center; -} - -.container { - display: flex; - width: 1200px; - justify-content: space-between; - margin: 0px auto; -} - -.logo { - width: 120px; - height: 50px; - margin: 10px 0px; -} - -.nav { - font-weight: bold; - list-style: none; - color:#BCBEC0; - margin: 0px; - display: flex; -} - -.nav li { - padding: 0px 5px; -} - -.nav a { - display: block; - color:#BCBEC0; - font-weight: bold; - padding: 25px 15px; - text-decoration: none; - border-bottom: 3px solid transparent; -} - -.nav a:hover { - text-decoration: none; - border-bottom-color: #BCBEC0; - background-color: rgba(0,0,0,.5) -} - -/*--*/ - -.banner { - background-color: #171F26; - padding: 20px; -} - -.banner h1 { - text-align: center; - color:#BCBEC0; - font-size: 40px; - line-height: 1.15; - margin: 0px; - padding: 80px 0px 20px; -} - -.banner h2 { - text-align: center; - color:#BCBEC0; - font-size: 20px; - line-height: 1.15; - font-weight: 300; - margin: 0px; -} - -.install { - text-align: center; - padding: 60px 0px; -} - -.install span { - background-color: #283542; - color: #BCBEC0; - font-family: Courier New, Courier, monospace; - font-size: 14px; - padding: 15px; - display: inline-block; -} - -.install a { - background-color: #0e83cd; - font-size: 14px; - padding: 15px; - color: white; - display: inline-block; - position: relative; - top: +1px; -} - -.social-buttons { - text-align: center; -} - -.social-share{ - width: 100px !important; - height: 20px !important; - border: none; - overflow: hidden; - margin: 2px; -} - -/* -- */ - -.feature { - flex:1; - margin: 40px 30px 40px 0px; - padding: 0px; -} -.container .feature:last-child { - margin-right: 0px; -} - -.feature h1 { - font-size: 28px; - color: #171F26; -} - -.feature .fa { - margin: 0px 15px; - color: #171F26; - width: 20px; -} - -.feature p { - margin: 0px 0px 0px 50px; - line-height: 1.5em; -} - -.feature ul { - margin: 0px 0px 0px 10px; - line-height: 1.5em; -} - -.feature-pack { - background-color: yellow; -} - -/* -- */ - -.banner-title { - background-color: #f3f3f3; - padding: 40px; - margin-bottom: 40px; - font-size: 36px; - color: #171F26; -} - -.banner-title .container { - justify-content: flex-start; -} - -.banner-title h2 { - display: inline-block; - font-size: 18px; - color: gray; - font-weight: 300; - margin-left: 30px; -} -/* -- */ -.examples { - flex: 1; - width: 200px; -} -.examples ul { - list-style: none; - margin: 0px; - padding: 0px; -} -.examples li { - padding: 20px 30px; -} -.examples li i { - margin-right: 10px; -} -.examples li.active { - background-color: #F5F2F0; - color: black; -} -.examples li.active a { - color: black; -} -.examples a { - color: #0e83cd; - text-decoration: none; -} -.examples a:hover { - text-decoration: underline; -} - -.demo-box { - flex: 2; - margin: 0px !important; - background-color: #F5F2F0; -} - - -/* -- */ -footer { - text-align: center; - background-color: #171F26; - color:#BCBEC0; - margin-top: 20px; - padding: 30px; -} - -footer a { - color:#BCBEC0; - text-decoration: none; -} - -footer a:hover { - color:#BCBEC0; - text-decoration: underline; -} - -/* -- SHELL -- */ -.shell { background-color: #2A211C; padding: 20px; color: white; font-family: monospace; font-size: 14px; overflow-y: auto; overflow-x: auto; margin: 20px; height: 500px; } -.prompt { position: relative; padding-left: 15px; } -.prompt:before { position: absolute; top: 0px; left: 0px; content: '>'; color: white; } -.cursor { position: absolute; } -.textbox { width: 100%; border: 0px; background-color: transparent; color: white; outline: none; font-family: monospace; font-size: 14px; } -.shell pre { margin: 5px 0px;} -.welcome { color: #F6F080; } -.result { color: cyan; } -.error { color: #FF7D60; } -.shell::-webkit-scrollbar { width: 8px; height: 8px; } -.shell::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #686868; } diff --git a/LiteDB.org/css/prism.css b/LiteDB.org/css/prism.css deleted file mode 100644 index 0ea8217ce..000000000 --- a/LiteDB.org/css/prism.css +++ /dev/null @@ -1,139 +0,0 @@ -/* http://prismjs.com/download.html?themes=prism&languages=clike+csharp */ -/** - * prism.js default theme for JavaScript, CSS and HTML - * Based on dabblet (http://dabblet.com) - * @author Lea Verou - */ - -code[class*="language-"], -pre[class*="language-"] { - color: black; - text-shadow: 0 1px white; - font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, -code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { - text-shadow: none; - background: #b3d4fc; -} - -pre[class*="language-"]::selection, pre[class*="language-"] ::selection, -code[class*="language-"]::selection, code[class*="language-"] ::selection { - text-shadow: none; - background: #b3d4fc; -} - -@media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } -} - -/* Code blocks */ -pre[class*="language-"] { - padding: 1em; - margin: .5em 0; - overflow: auto; -} - -:not(pre) > code[class*="language-"], -pre[class*="language-"] { - background: #f5f2f0; -} - -/* Inline code */ -:not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; - white-space: normal; -} - -.token.comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: slategray; -} - -.token.punctuation { - color: #999; -} - -.namespace { - opacity: .7; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.deleted { - color: #905; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.inserted { - color: #690; -} - -.token.operator, -.token.entity, -.token.url, -.language-css .token.string, -.style .token.string { - color: #a67f59; - background: hsla(0, 0%, 100%, .5); -} - -.token.atrule, -.token.attr-value, -.token.keyword { - color: #07a; -} - -.token.function { - color: #DD4A68; -} - -.token.regex, -.token.important, -.token.variable { - color: #e90; -} - -.token.important, -.token.bold { - font-weight: bold; -} -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} - diff --git a/LiteDB.org/img/logo_litedb.svg b/LiteDB.org/img/logo_litedb.svg deleted file mode 100644 index f56a949a8..000000000 --- a/LiteDB.org/img/logo_litedb.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - background - - - - Layer 1 - - - - - - - - - - - - - - - - - Lite - DB - - \ No newline at end of file diff --git a/LiteDB.org/index.html b/LiteDB.org/index.html deleted file mode 100644 index 872fa1ac5..000000000 --- a/LiteDB.org/index.html +++ /dev/null @@ -1,258 +0,0 @@ - - - - - LiteDB :: A .NET embedded NoSQL database - - - - - - - - - - - - - -
-
- - -
-
- - - -
-
-

Standalone database

-

LiteDB is serverless database delivered in a single DLL (less than 200kb) fully written in C# managed code.

-

Install via NuGet or just copy DLL to your bin project folder.

-
-
-

Fast and lightweight

-

LiteDB is a simple and fast NoSQL database solution. Ideal for:

-
    -
  • Desktop/local small applications
  • -
  • Application file format
  • -
  • Small web applications
  • -
  • One database per account/user data store
  • -
  • Few concurrency write users operations
  • -
-
-
-

And much more...

-
    -
  • Document ACID transaction
  • -
  • Single datafile (like SQLite)
  • -
  • Recovery data in writing failure (journal mode)
  • -
  • Map your POCO class to BsonDocument
  • -
  • Fluent API for custom mapping
  • -
  • Cross collections references (DbRef)
  • -
  • Store files and stream data (like GridFS in MongoDB)
  • -
  • LINQ support
  • -
  • FREE for everyone - including commercial use
  • -
-
-
- - - -
- - - -
// Basic example
-public class Customer
-{
-    public int Id { get; set; }
-    public string Name { get; set; }
-    public string[] Phones { get; set; }
-    public bool IsActive { get; set; }
-}
-
-// Open database (or create if not exits)
-using(var db = new LiteDatabase(@"MyData.db"))
-{
-    // Get customer collection
-    var customers = db.GetCollection<Customer>("customers");
-
-    // Create your new customer instance
-    var customer = new Customer
-    { 
-        Name = "John Doe", 
-        Phones = new string[] { "8000-0000", "9000-0000" }, 
-        IsActive = true
-    };
-
-    // Insert new customer document (Id will be auto-incremented)
-    customers.Insert(customer);
-
-    // Update a document inside a collection
-    customer.Name = "Joana Doe";
-
-    customers.Update(customer);
-
-    // Index document using a document property
-    customers.EnsureIndex(x => x.Name);
-
-    // Use Linq to query documents
-    var results = customers.Find(x => x.Name.StartsWith("Jo"));
-}
- - - - - - - - - - - -
- -
- Made with ♥ by Mauricio David - @mbdavid - MIT License -
- - - - - - - - - \ No newline at end of file diff --git a/LiteDB.org/js/prism.js b/LiteDB.org/js/prism.js deleted file mode 100644 index a549d1846..000000000 --- a/LiteDB.org/js/prism.js +++ /dev/null @@ -1,4 +0,0 @@ -/* http://prismjs.com/download.html?themes=prism&languages=clike+csharp */ -var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),P=[p,1];b&&P.push(b);var A=new a(i,g?t.tokenize(m,g):m,h);P.push(A),w&&P.push(w),Array.prototype.splice.apply(r,P)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var l={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}t.hooks.run("wrap",l);var o="";for(var s in l.attributes)o+=(o?" ":"")+s+'="'+(l.attributes[s]||"")+'"';return"<"+l.tag+' class="'+l.classes.join(" ")+'" '+o+">"+l.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code,l=n.immediateClose;_self.postMessage(t.highlight(r,t.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); -Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; -Prism.languages.csharp=Prism.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[/@("|')(\1\1|\\\1|\\?(?!\1)[\s\S])*\1/,/("|')(\\?.)*?\1/],number:/\b-?(0x[\da-f]+|\d*\.?\d+f?)\b/i}),Prism.languages.insertBefore("csharp","keyword",{preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(\s*#)\b(define|elif|else|endif|endregion|error|if|line|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}}); diff --git a/LiteDB.org/js/shell.js b/LiteDB.org/js/shell.js deleted file mode 100644 index 613ef7be5..000000000 --- a/LiteDB.org/js/shell.js +++ /dev/null @@ -1,90 +0,0 @@ -(function() { - - var db = localStorage.getItem('db-v2'); - - if (!db) { - db = 'db_' + (new Date()).getTime(); - localStorage.setItem('db-v2', db); - } - - var hist = []; - var idx = 0; - - $('.shell').on('click', function () { $('.textbox').focus(); }); - - $('.textbox').on('keyup', function (e) { - - var $el = $(this); - var $prompt = $el.parent(); - var text = $el.val().trim(); - - if (e.keyCode == 13 && text.length > 0) { - - $prompt.before('
' + text + '
'); - $el.val('') - $prompt.hide(); - hist.push(text); - idx = hist.length; - - execute({ db: db, cmd: text }, function (result) { - var css = /^ERROR:\s/.test(result) ? 'error' : 'result'; - $prompt.before('
' + $('
').text(result).html() + '
'); - $prompt.show(); - $el.focus(); - setTimeout(function () { - var div = $('.shell').get(0); - div.scrollTop = div.scrollHeight; - }, 100); - }); - } - else if (e.keyCode == 38) { - idx = Math.max(0, idx - 1); - $el.val(hist[idx]); - } - else if (e.keyCode == 40) { - idx = Math.min(hist.length - 1, idx + 1); - $el.val(hist[idx]); - } - - }); - - function execute(params, cb) { - if (params.cmd == 'help') return cb(help); - var host = location.hostname == "localhost" ? "" : "http://litedb.azurewebsites.net/"; - - $.post(host + 'WebShell.ashx', params).done(function (result) { - cb(result); - }); - } - - var help = -'Web Shell Commands - try offline version for more commands\n' + -'==========================================================\n' + -'\n' + -'> show collections\n' + -' List all collection in database\n' + -'> db..insert \n' + -' Insert a new document into collection\n' + -'> db..update \n' + -' Update a document inside collection\n' + -'> db..delete \n' + -' Delete documents using a filter clausule (see find)\n' + -'> db..find [top N] \n' + -' Show filtered documents based on index search\n' + -'> db..count \n' + -' Show count rows according query filter\n' + -'> db..ensureIndex [unique]\n' + -' Create a new index document field\n' + -' = [=|>|>=|<|<=|!=|like|between] \n' + -' Filter query syntax\n' + -' = ( [and|or] [and|or] ...)\n' + -' Multi queries syntax\n' + -'\n' + -'Try:\n' + -' > db.customers.insert { _id:1, name:\"John Doe\", age: 37 }\n' + -' > db.customers.ensureIndex name\n' + -' > db.customers.find name like \"John\"\n' + -' > db.customers.find limit 10 (name like \"John\" and _id between [0, 100])\n'; - -})(); - diff --git a/LiteDB.sln b/LiteDB.sln index 11a0ac82d..87b0f4736 100644 --- a/LiteDB.sln +++ b/LiteDB.sln @@ -9,28 +9,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Shell", "LiteDB.Shel EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiteDB.Tests", "LiteDB.Tests\LiteDB.Tests.csproj", "{BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}" EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "LiteDB.org", "LiteDB.org\", "{FA39B5CD-49DD-4057-8714-66F24AA1CFF5}" - ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" - ProjectReferences = "{e808051a-83b7-4fa9-b004-d064ea162b60}|LiteDB.dll;" - Debug.AspNetCompiler.VirtualPath = "/localhost_50030" - Debug.AspNetCompiler.PhysicalPath = "LiteDB.org\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_50030\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_50030" - Release.AspNetCompiler.PhysicalPath = "LiteDB.org\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_50030\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "50030" - SlnRelativePath = "LiteDB.org\" - EndProjectSection -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,10 +27,6 @@ Global {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCC55ED6-CE7B-4811-979C-FE74B3A2D6AE}.Release|Any CPU.Build.0 = Release|Any CPU - {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {FA39B5CD-49DD-4057-8714-66F24AA1CFF5}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 88fa886eee16ac29acf603b6a99e30ef169bd4ae Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 13:00:29 -0200 Subject: [PATCH 79/91] Update unit tests --- LiteDB-TODO.txt | 73 ++++------- LiteDB.Tests/{ => Bson}/BsonTest.cs | 39 +++--- LiteDB.Tests/{ => Bson}/JsonTest.cs | 19 +-- LiteDB.Tests/{ => Bson}/ObjectIdTest.cs | 10 +- .../{ => Concurrency}/ConcurrentTest.cs | 48 ++++--- LiteDB.Tests/Concurrency/ThreadSafeTest.cs | 120 ++++++++++++++++++ LiteDB.Tests/{ => Crud}/BigFileTest.cs | 2 +- LiteDB.Tests/{ => Crud}/BulkTest.cs | 3 +- LiteDB.Tests/{ => Crud}/DropCollectionTest.cs | 2 +- LiteDB.Tests/Crud/FileStorageTest.cs | 42 ++++++ LiteDB.Tests/FileStorageTest.cs | 89 ------------- LiteDB.Tests/{ => Index}/IndexOrderTest.cs | 2 +- LiteDB.Tests/{ => Index}/LinqTest.cs | 8 +- LiteDB.Tests/LiteDB.Tests.csproj | 31 ++--- LiteDB.Tests/{ => Mapping}/AutoIdTest.cs | 2 +- LiteDB.Tests/{ => Mapping}/BsonFieldTest.cs | 6 +- LiteDB.Tests/{ => Mapping}/IncludeTest.cs | 10 +- .../{ => Mapping}/MapperInterfaceTest.cs | 6 +- LiteDB.Tests/{ => Mapping}/MapperTest.cs | 2 +- LiteDB.Tests/Utils/DB.cs | 16 +-- LiteDB/Core/Collections/Aggregate.cs | 65 +++++++++- LiteDB/DbEngine/Actions/Aggregate.cs | 82 +++++++----- LiteDB/DbEngine/Actions/Collection.cs | 7 +- LiteDB/DbEngine/Actions/Find.cs | 37 +++--- LiteDB/DbEngine/Actions/Shrink.cs | 101 ++++++++------- LiteDB/DbEngine/DbEngine.cs | 3 + LiteDB/DbEngine/Disks/FileDiskService.cs | 13 +- LiteDB/DbEngine/Services/CacheService.cs | 35 ++--- LiteDB/DbEngine/Services/PageService.cs | 9 +- .../DbEngine/Services/TransactionService.cs | 28 ++-- LiteDB/Serializer/Mapper/BsonMapper.cs | 13 +- 31 files changed, 508 insertions(+), 415 deletions(-) rename LiteDB.Tests/{ => Bson}/BsonTest.cs (58%) rename LiteDB.Tests/{ => Bson}/JsonTest.cs (70%) rename LiteDB.Tests/{ => Bson}/ObjectIdTest.cs (82%) rename LiteDB.Tests/{ => Concurrency}/ConcurrentTest.cs (69%) create mode 100644 LiteDB.Tests/Concurrency/ThreadSafeTest.cs rename LiteDB.Tests/{ => Crud}/BigFileTest.cs (98%) rename LiteDB.Tests/{ => Crud}/BulkTest.cs (95%) rename LiteDB.Tests/{ => Crud}/DropCollectionTest.cs (97%) create mode 100644 LiteDB.Tests/Crud/FileStorageTest.cs delete mode 100644 LiteDB.Tests/FileStorageTest.cs rename LiteDB.Tests/{ => Index}/IndexOrderTest.cs (98%) rename LiteDB.Tests/{ => Index}/LinqTest.cs (90%) rename LiteDB.Tests/{ => Mapping}/AutoIdTest.cs (99%) rename LiteDB.Tests/{ => Mapping}/BsonFieldTest.cs (99%) rename LiteDB.Tests/{ => Mapping}/IncludeTest.cs (86%) rename LiteDB.Tests/{ => Mapping}/MapperInterfaceTest.cs (88%) rename LiteDB.Tests/{ => Mapping}/MapperTest.cs (99%) diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt index 9cfb75cfc..eb76e6e28 100644 --- a/LiteDB-TODO.txt +++ b/LiteDB-TODO.txt @@ -1,16 +1,28 @@ -# LiteDB v.2 - TODO - +## TODO para v2 BETA (até sexta) - Revisar help do Shell - saiu varias coisas -- Revisar BsonArray.Length não faz cache - ThreadSafe -- Unit tests -- New UserVersion. this.UserVersions(1, (db) => { .. }) -- Shrink -- Migration tool +- Unit tests básicos +- Test script todos os comandos +- Merge para master +- Folder root: "src", "test", "nuget" +- Revisar site -- Implementar DbRefAttribute +## TODO para v2 RC (até 31/dez) +- Atualizar documentação +- Revisar site - O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() - Colocar DbParams na header. _engine.GetParams(), _engine.SetParams(p) (limit 4k) +- Unit tests melhorados +- Colocar documentação neste site +- Migrar unit tests para XUnit ou outro q funcione no ASPNET5 + +## TODO para v2.0 (até 31/jan) +- Revisar BsonArray.Length não faz cache +- New UserVersion. this.UserVersions(1, (db) => { .. }) +- Migration tool? Shell.Import/Export? +- Implementar DbRefAttribute + +////////////////////////////////////////////////////////////////////////////////////////////////////// ## Thread Safe / Process Safe @@ -20,30 +32,13 @@ - Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) - Avaliar uso do ConcurrentDictionary na cache -## Testes de Performance -- Fazer um script grande para testar no shell -- Focar no insert/delete/find?? FileStorage -- Testar sempre como release -- Sempre com journal? -===> Opções de melhorias -* Testar outros dicionarios na cache -* Testar BitConvert ao inves de unsafe -* PageSize 8K e 16K - -## A pensar: +## FUTURE versions 2.x: -- Voltar UserVersion?? -- Solução de Migrations (protected?) - db.Migrations.Add(new Migration1()); - DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? -- Folder root: "src", "test", "nuget" -- Encrypt datafile (EncryptDiskService : FileDiskService) - Queria que o IDiskService não tivesse informações sobre Journal/Recovery +- Encrypt datafile (EncryptDiskService : FileDiskService) - Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? IBsonMapper? - -- Como implementar pro DNX -- Mapeador/Configuration estilo EF - db.Configuration.Add(new CredorConfiguration()) +- PCL e ASPNET 5 ## V2 Features - New DbEngine @@ -58,25 +53,3 @@ - Lazy load * ByteArray, DbEngine, abstract BasePage - FileLimit and Initial Size - -## Para versão 2 - -- Criar site simples, bacana, estilos: - novo site mongodb - http://pouchdb.com/ - vuejs.org https://github.com/vuejs/vuejs.org - https://hexo.io/ (esse é bem simples e acho q atende bem) -- Fazer o web shell na mesma pagina (nem q seja via iframe) -- Colocar documentação neste site -- Logotipo + Nome -- Atualizar codeproject -- Site: fazer versao mobile - -## Modulos do LiteDB -- Core -- DbEngine -- QueryEngine -- Mapper -- Utils - - Linq, shell, fileStorage, fts -- API diff --git a/LiteDB.Tests/BsonTest.cs b/LiteDB.Tests/Bson/BsonTest.cs similarity index 58% rename from LiteDB.Tests/BsonTest.cs rename to LiteDB.Tests/Bson/BsonTest.cs index cbd353151..23b18919b 100644 --- a/LiteDB.Tests/BsonTest.cs +++ b/LiteDB.Tests/Bson/BsonTest.cs @@ -1,12 +1,7 @@ using System; -using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class BsonTest @@ -24,7 +19,8 @@ private BsonDocument CreateDoc() doc["EmptyString"] = ""; doc["maxDate"] = DateTime.MaxValue; doc["minDate"] = DateTime.MinValue; - doc.Set("Customer.Address.Street", "Av. Cacapava"); + + doc.Set("Customer.Address.Street", "Av. Caçapava, Nº 122"); doc["Items"] = new BsonArray(); @@ -47,26 +43,27 @@ public void Bson_Test() var o = CreateDoc(); var bson = BsonSerializer.Serialize(o); - var json = JsonSerializer.Serialize(o); - var d = BsonSerializer.Deserialize(bson); + var doc = BsonSerializer.Deserialize(bson); + + Assert.AreEqual(123, doc["_id"].AsInt32); + Assert.AreEqual(o["_id"].AsInt64, doc["_id"].AsInt64); - Assert.AreEqual(d["_id"], 123); - Assert.AreEqual(d["_id"].AsInt64, o["_id"].AsInt64); + Assert.AreEqual("Av. Caçapava, Nº 122", doc.Get("Customer.Address.Street").AsString); - Assert.AreEqual(o["FirstString"].AsString, d["FirstString"].AsString); - Assert.AreEqual(o["Date"].AsDateTime.ToString(), d["Date"].AsDateTime.ToString()); - Assert.AreEqual(o["CustomerId"].AsGuid, d["CustomerId"].AsGuid); - Assert.AreEqual(o["MyNull"].RawValue, d["MyNull"].RawValue); - Assert.AreEqual(o["EmptyString"].AsString, d["EmptyString"].AsString); + Assert.AreEqual(o["FirstString"].AsString, doc["FirstString"].AsString); + Assert.AreEqual(o["Date"].AsDateTime.ToString(), doc["Date"].AsDateTime.ToString()); + Assert.AreEqual(o["CustomerId"].AsGuid, doc["CustomerId"].AsGuid); + Assert.AreEqual(o["MyNull"].RawValue, doc["MyNull"].RawValue); + Assert.AreEqual(o["EmptyString"].AsString, doc["EmptyString"].AsString); - Assert.AreEqual(d["maxDate"].AsDateTime, DateTime.MaxValue); - Assert.AreEqual(d["minDate"].AsDateTime, DateTime.MinValue); + Assert.AreEqual(DateTime.MaxValue, doc["maxDate"].AsDateTime); + Assert.AreEqual(DateTime.MinValue, doc["minDate"].AsDateTime); - Assert.AreEqual(o["Items"].AsArray.Count, d["Items"].AsArray.Count); - Assert.AreEqual(o["Items"].AsArray[0].AsDocument["Unit"].AsDouble, d["Items"].AsArray[0].AsDocument["Unit"].AsDouble); - Assert.AreEqual(o["Items"].AsArray[4].AsDateTime.ToString(), d["Items"].AsArray[4].AsDateTime.ToString()); + Assert.AreEqual(o["Items"].AsArray.Count, doc["Items"].AsArray.Count); + Assert.AreEqual(o["Items"].AsArray[0].AsDocument["Unit"].AsDouble, doc["Items"].AsArray[0].AsDocument["Unit"].AsDouble); + Assert.AreEqual(o["Items"].AsArray[4].AsDateTime.ToString(), doc["Items"].AsArray[4].AsDateTime.ToString()); } } } diff --git a/LiteDB.Tests/JsonTest.cs b/LiteDB.Tests/Bson/JsonTest.cs similarity index 70% rename from LiteDB.Tests/JsonTest.cs rename to LiteDB.Tests/Bson/JsonTest.cs index 3c1a43b3f..b3a9af343 100644 --- a/LiteDB.Tests/JsonTest.cs +++ b/LiteDB.Tests/Bson/JsonTest.cs @@ -1,12 +1,7 @@ using System; -using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class JsonTest @@ -45,13 +40,13 @@ public void Json_Test() var json = JsonSerializer.Serialize(o, true); - var d = JsonSerializer.Deserialize(json).AsDocument; + var doc = JsonSerializer.Deserialize(json).AsDocument; - Assert.AreEqual(d["Date"].AsDateTime, o["Date"].AsDateTime); - Assert.AreEqual(d["CustomerId"].AsGuid, o["CustomerId"].AsGuid); - Assert.AreEqual(d["Items"].AsArray.Count, o["Items"].AsArray.Count); - Assert.AreEqual(d["_id"], 123); - Assert.AreEqual(d["_id"].AsInt64, o["_id"].AsInt64); + Assert.AreEqual(o["Date"].AsDateTime, doc["Date"].AsDateTime); + Assert.AreEqual(o["CustomerId"].AsGuid, doc["CustomerId"].AsGuid); + Assert.AreEqual(o["Items"].AsArray.Count, doc["Items"].AsArray.Count); + Assert.AreEqual(123, doc["_id"].AsInt32); + Assert.AreEqual(o["_id"].AsInt64, doc["_id"].AsInt64); } } } diff --git a/LiteDB.Tests/ObjectIdTest.cs b/LiteDB.Tests/Bson/ObjectIdTest.cs similarity index 82% rename from LiteDB.Tests/ObjectIdTest.cs rename to LiteDB.Tests/Bson/ObjectIdTest.cs index f1f17feab..a4685d938 100644 --- a/LiteDB.Tests/ObjectIdTest.cs +++ b/LiteDB.Tests/Bson/ObjectIdTest.cs @@ -1,12 +1,6 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class ObjectIdTest diff --git a/LiteDB.Tests/ConcurrentTest.cs b/LiteDB.Tests/Concurrency/ConcurrentTest.cs similarity index 69% rename from LiteDB.Tests/ConcurrentTest.cs rename to LiteDB.Tests/Concurrency/ConcurrentTest.cs index 9272e915b..54bd55d17 100644 --- a/LiteDB.Tests/ConcurrentTest.cs +++ b/LiteDB.Tests/Concurrency/ConcurrentTest.cs @@ -1,13 +1,10 @@ using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; using System.IO; -using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class ConcurrentTest @@ -17,15 +14,15 @@ public class ConcurrentTest [TestMethod] public void Concurrent_Test() { - var file = DB.Path(); - var N = 300; + var dbname = DB.RandomFilename(); + var N = 300; // interate counter - var a = new LiteDatabase(file); - var b = new LiteDatabase(file); - var c = new LiteDatabase(file); - var d = new LiteDatabase(file); + var a = new LiteDatabase(dbname); + var b = new LiteDatabase(dbname); + var c = new LiteDatabase(dbname); + var d = new LiteDatabase(dbname); - // Task A -> Insert 100 documents + // task A -> insert N documents var ta = Task.Factory.StartNew(() => { var col = a.GetCollection("col1"); @@ -33,11 +30,11 @@ public void Concurrent_Test() for (var i = 1; i <= N; i++) { - col.Insert(this.CreateDoc(i, "My String")); + col.Insert(this.CreateDoc(i, "-insert-")); } }); - // Task B -> Update 100 documents + // task B -> update N documents var tb = Task.Factory.StartNew(() => { var col = b.GetCollection("col1"); @@ -45,16 +42,16 @@ public void Concurrent_Test() while (i <= N) { - var doc = this.CreateDoc(i, "update value"); + var doc = this.CreateDoc(i, "-update-"); doc["date"] = new DateTime(2015, 1, 1); - doc["value"] = null; + doc["desc"] = null; var success = col.Update(doc); if (success) i++; } }); - // TasK C -> Delete 99 documents (keep only _id = 1) + // tasK C -> delete N-1 documents (keep only _id = 1) var tc = Task.Factory.StartNew(() => { var col = c.GetCollection("col1"); @@ -62,7 +59,8 @@ public void Concurrent_Test() while (i <= N) { - if(col.Exists(Query.And(Query.EQ("_id", i), Query.EQ("name", "update value")))) + // delete document after update + if(col.Exists(Query.And(Query.EQ("_id", i), Query.EQ("name", "-update-")))) { var success = col.Delete(i); if (success) i++; @@ -70,12 +68,12 @@ public void Concurrent_Test() } }); - // Task D -> Upload 40 files + delete 20 + // task D -> upload 40 files + delete 20 var td = Task.Factory.StartNew(() => { for (var i = 1; i <= 40; i++) { - d.FileStorage.Upload("f" + i, this.CreateMemoryFile(1024 * 512)); + d.FileStorage.Upload("f" + i, this.CreateMemoryFile(20000)); } for (var i = 1; i <= 20; i++) { @@ -91,17 +89,18 @@ public void Concurrent_Test() c.Dispose(); d.Dispose(); - using (var db = new LiteDatabase(file)) + using (var db = new LiteDatabase(dbname)) { var col = db.GetCollection("col1"); var doc = col.FindById(1); - Assert.AreEqual(doc["name"].AsString, "update value"); - Assert.AreEqual(doc["date"].AsDateTime, new DateTime(2015, 1, 1)); - Assert.AreEqual(doc["value"].IsNull, true); + Assert.AreEqual("-update-", doc["name"].AsString); + Assert.AreEqual(new DateTime(2015, 1, 1), doc["date"].AsDateTime); + Assert.AreEqual(true, doc["desc"].IsNull); Assert.AreEqual(col.Count(), 1); - Assert.AreEqual(db.FileStorage.FindAll().Count(), 20); + Assert.AreEqual(1, col.Count()); + Assert.AreEqual(20, db.FileStorage.FindAll().Count()); } } @@ -121,7 +120,6 @@ private BsonDocument CreateDoc(int id, string name) private MemoryStream CreateMemoryFile(int size) { var buffer = new byte[size]; - return new MemoryStream(buffer); } } diff --git a/LiteDB.Tests/Concurrency/ThreadSafeTest.cs b/LiteDB.Tests/Concurrency/ThreadSafeTest.cs new file mode 100644 index 000000000..ed01f127a --- /dev/null +++ b/LiteDB.Tests/Concurrency/ThreadSafeTest.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using System.Threading.Tasks; + +namespace LiteDB.Tests +{ + [TestClass] + public class ThreadSafeTest + { + private Random _rnd = new Random(); + + [TestMethod] + public void Thread_Test() + { + var dbname = DB.RandomFilename(); + var N = 300; // interate counter + + // use a single instance of LiteDatabase/Collection + var a = new LiteDatabase(dbname); + var col = a.GetCollection("col1"); + col.EnsureIndex("name"); + + // task A -> insert N documents + var ta = Task.Factory.StartNew(() => + { + for (var i = 1; i <= N; i++) + { + col.Insert(this.CreateDoc(i, "-insert-")); + } + }); + + // task B -> update N documents + var tb = Task.Factory.StartNew(() => + { + var i = 1; + while (i <= N) + { + var doc = this.CreateDoc(i, "-update-"); + doc["date"] = new DateTime(2015, 1, 1); + doc["desc"] = null; + + var success = col.Update(doc); + if (success) i++; + } + }); + + // tasK C -> delete N-1 documents (keep only _id = 1) + var tc = Task.Factory.StartNew(() => + { + var i = 2; + while (i <= N) + { + // delete document after update (query before) + if(col.Exists(Query.And(Query.EQ("_id", i), Query.EQ("name", "-update-")))) + { + var success = col.Delete(i); + if (success) i++; + } + + // solution without query + //var success = col.Delete(Query.And(Query.EQ("_id", i), Query.EQ("name", "-update-"))) > 0; + //if (success) i++; + } + }); + + // task D -> upload 40 files + delete 20 + var td = Task.Factory.StartNew(() => + { + for (var i = 1; i <= 40; i++) + { + a.FileStorage.Upload("f" + i, this.CreateMemoryFile(20000)); + } + for (var i = 1; i <= 20; i++) + { + a.FileStorage.Delete("f" + i); + } + }); + + // Now, test data + Task.WaitAll(ta, tb, tc, td); + + a.Dispose(); + + using (var db = new LiteDatabase(dbname)) + { + var cl = db.GetCollection("col1"); + var doc = cl.FindById(1); + + Assert.AreEqual("-update-", doc["name"].AsString); + Assert.AreEqual(new DateTime(2015, 1, 1), doc["date"].AsDateTime); + Assert.AreEqual(true, doc["desc"].IsNull); + Assert.AreEqual(cl.Count(), 1); + + Assert.AreEqual(1, cl.Count()); + Assert.AreEqual(20, db.FileStorage.FindAll().Count()); + } + } + + private BsonDocument CreateDoc(int id, string name) + { + var doc = new BsonDocument(); + + doc["_id"] = id; + doc["name"] = name; + doc["desc"] = DB.LoremIpsum(10, 10, 2, 2, 2); + doc["date"] = DateTime.Now.AddDays(_rnd.Next(300)); + doc["value"] = _rnd.NextDouble() * 5000; + + return doc; + } + + private MemoryStream CreateMemoryFile(int size) + { + var buffer = new byte[size]; + return new MemoryStream(buffer); + } + } +} diff --git a/LiteDB.Tests/BigFileTest.cs b/LiteDB.Tests/Crud/BigFileTest.cs similarity index 98% rename from LiteDB.Tests/BigFileTest.cs rename to LiteDB.Tests/Crud/BigFileTest.cs index 50c426ca2..003ae0673 100644 --- a/LiteDB.Tests/BigFileTest.cs +++ b/LiteDB.Tests/Crud/BigFileTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class BigFileTest diff --git a/LiteDB.Tests/BulkTest.cs b/LiteDB.Tests/Crud/BulkTest.cs similarity index 95% rename from LiteDB.Tests/BulkTest.cs rename to LiteDB.Tests/Crud/BulkTest.cs index fb228531d..75393102f 100644 --- a/LiteDB.Tests/BulkTest.cs +++ b/LiteDB.Tests/Crud/BulkTest.cs @@ -4,9 +4,8 @@ using LiteDB; using System.IO; using System.Collections.Generic; -using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class BulkTest diff --git a/LiteDB.Tests/DropCollectionTest.cs b/LiteDB.Tests/Crud/DropCollectionTest.cs similarity index 97% rename from LiteDB.Tests/DropCollectionTest.cs rename to LiteDB.Tests/Crud/DropCollectionTest.cs index 28266d009..21226b9ac 100644 --- a/LiteDB.Tests/DropCollectionTest.cs +++ b/LiteDB.Tests/Crud/DropCollectionTest.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class DropCollectionTest diff --git a/LiteDB.Tests/Crud/FileStorageTest.cs b/LiteDB.Tests/Crud/FileStorageTest.cs new file mode 100644 index 000000000..be211e6cc --- /dev/null +++ b/LiteDB.Tests/Crud/FileStorageTest.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LiteDB; +using System.IO; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Threading; + +namespace LiteDB.Tests +{ + [TestClass] + public class FileStorage_Test + { + [TestMethod] + public void FileStorage_InsertDelete() + { + // create a dump file + File.WriteAllText("Core.dll", "FileCoreContent"); + + using (var db = new LiteDatabase(new MemoryStream())) + { + db.FileStorage.Upload("Core.dll", "Core.dll"); + + var exists = db.FileStorage.Exists("Core.dll"); + + Assert.AreEqual(true, exists); + + var deleted = db.FileStorage.Delete("Core.dll"); + + Assert.AreEqual(true, deleted); + + var deleted2 = db.FileStorage.Delete("Core.dll"); + + Assert.AreEqual(false, deleted2); + } + + File.Delete("Core.dll"); + } + } +} diff --git a/LiteDB.Tests/FileStorageTest.cs b/LiteDB.Tests/FileStorageTest.cs deleted file mode 100644 index d3bb12468..000000000 --- a/LiteDB.Tests/FileStorageTest.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using LiteDB; -using System.IO; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; -using System.Threading; - -namespace UnitTest -{ - [TestClass] - public class FileStorage_Test - { - [TestMethod] - public void FileStorage_InsertDelete() - { - // create a dump file - File.WriteAllText("Core.dll", "FileCoreContent"); - - using (var db = new LiteDatabase(new MemoryStream())) - { - db.FileStorage.Upload("Core.dll", "Core.dll"); - - var exists = db.FileStorage.Exists("Core.dll"); - - Assert.AreEqual(true, exists); - - var deleted = db.FileStorage.Delete("Core.dll"); - - Assert.AreEqual(true, deleted); - - var deleted2 = db.FileStorage.Delete("Core.dll"); - - Assert.AreEqual(false, deleted2); - - - } - - File.Delete("Core.dll"); - } - - public string fdb = DB.Path(); - public Random rnd = new Random(); - - [TestMethod] - public void FileStorage_Concurrency() - { - using (var db = new LiteDatabase(fdb)) - { - } - - var t1 = new Thread(new ThreadStart(TaskInsert)); - var t2 = new Thread(new ThreadStart(TaskInsert)); - var t3 = new Thread(new ThreadStart(TaskInsert)); - var t4 = new Thread(new ThreadStart(TaskInsert)); - var t5 = new Thread(new ThreadStart(TaskInsert)); - var t6 = new Thread(new ThreadStart(TaskInsert)); - - t1.Start(); - t2.Start(); - t3.Start(); - t4.Start(); - t5.Start(); - t6.Start(); - - t1.Join(); - t2.Join(); - t3.Join(); - t4.Join(); - t5.Join(); - t6.Join(); - - } - - public void TaskInsert() - { - var file = new byte[1 * 1024 * 1025]; // 30MB - - System.Threading.Thread.Sleep(rnd.Next(100)); - - using (var db = new LiteDatabase(fdb)) - { - db.FileStorage.Upload(Guid.NewGuid().ToString(), new MemoryStream(file)); - } - } - } -} diff --git a/LiteDB.Tests/IndexOrderTest.cs b/LiteDB.Tests/Index/IndexOrderTest.cs similarity index 98% rename from LiteDB.Tests/IndexOrderTest.cs rename to LiteDB.Tests/Index/IndexOrderTest.cs index 06bf9baaa..9b0343e6a 100644 --- a/LiteDB.Tests/IndexOrderTest.cs +++ b/LiteDB.Tests/Index/IndexOrderTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] public class IndexOrderTest diff --git a/LiteDB.Tests/LinqTest.cs b/LiteDB.Tests/Index/LinqTest.cs similarity index 90% rename from LiteDB.Tests/LinqTest.cs rename to LiteDB.Tests/Index/LinqTest.cs index 208be8662..2ac0b9e93 100644 --- a/LiteDB.Tests/LinqTest.cs +++ b/LiteDB.Tests/Index/LinqTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { public class User { @@ -30,10 +30,10 @@ public void Linq_Test() { LiteDB.BsonMapper.Global.UseLowerCaseDelimiter('_'); - using (var db = new LiteDatabase(DB.Path())) + using (var db = new LiteDatabase(new MemoryStream())) { var c1 = new User { Id = 1, Name = "Mauricio", Active = true, Domain = new UserDomain { DomainName = "Numeria" } }; - var c2 = new User { Id = 2, Name = "Malafaia", Active = false, Domain = new UserDomain { DomainName = "Numeria" } }; + var c2 = new User { Id = 2, Name = "Malatruco", Active = false, Domain = new UserDomain { DomainName = "Numeria" } }; var c3 = new User { Id = 3, Name = "Chris", Domain = new UserDomain { DomainName = "Numeria" } }; var c4 = new User { Id = 4, Name = "Juliane" }; @@ -67,7 +67,7 @@ public void Linq_Test() // and/or Assert.AreEqual(1, col.Count(x => x.Id > 0 && x.Name == "MAURICIO")); - Assert.AreEqual(2, col.Count(x => x.Name == "malafaia" || x.Name == "MAURICIO")); + Assert.AreEqual(2, col.Count(x => x.Name == "malatruco" || x.Name == "MAURICIO")); } } } diff --git a/LiteDB.Tests/LiteDB.Tests.csproj b/LiteDB.Tests/LiteDB.Tests.csproj index e1f484d35..2975fbaf5 100644 --- a/LiteDB.Tests/LiteDB.Tests.csproj +++ b/LiteDB.Tests/LiteDB.Tests.csproj @@ -56,22 +56,23 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + diff --git a/LiteDB.Tests/AutoIdTest.cs b/LiteDB.Tests/Mapping/AutoIdTest.cs similarity index 99% rename from LiteDB.Tests/AutoIdTest.cs rename to LiteDB.Tests/Mapping/AutoIdTest.cs index 1d0f1e7e2..a6cb92042 100644 --- a/LiteDB.Tests/AutoIdTest.cs +++ b/LiteDB.Tests/Mapping/AutoIdTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { public class EntityInt { diff --git a/LiteDB.Tests/BsonFieldTest.cs b/LiteDB.Tests/Mapping/BsonFieldTest.cs similarity index 99% rename from LiteDB.Tests/BsonFieldTest.cs rename to LiteDB.Tests/Mapping/BsonFieldTest.cs index 0b29fc57f..11030cd94 100644 --- a/LiteDB.Tests/BsonFieldTest.cs +++ b/LiteDB.Tests/Mapping/BsonFieldTest.cs @@ -2,7 +2,7 @@ using LiteDB; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace UnitTest +namespace LiteDB.Tests { public class MyBsonFieldTestClass { @@ -122,10 +122,6 @@ public void BsonField_Test() Assert.AreEqual(obj.GetMyProtectedPropertyNamed(), nobj.GetMyProtectedPropertyNamed()); Assert.AreEqual(obj.GetMyProtectedPropertySerializable(), nobj.GetMyProtectedPropertySerializable()); Assert.AreEqual(nobj.GetMyProtectedPropertyNotSerializable(), null); - - - - } } } \ No newline at end of file diff --git a/LiteDB.Tests/IncludeTest.cs b/LiteDB.Tests/Mapping/IncludeTest.cs similarity index 86% rename from LiteDB.Tests/IncludeTest.cs rename to LiteDB.Tests/Mapping/IncludeTest.cs index 46fdc1d81..bc7855d58 100644 --- a/LiteDB.Tests/IncludeTest.cs +++ b/LiteDB.Tests/Mapping/IncludeTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; -namespace UnitTest +namespace LiteDB.Tests { public class Order { @@ -91,7 +91,13 @@ public void Include_Test() .FindAll() .FirstOrDefault(); - var customerName = query.Customer.Name; + Assert.AreEqual(customer.Name, query.Customer.Name); + Assert.AreEqual(product1.Price, query.Products[0].Price); + Assert.AreEqual(product2.Name, query.Products[1].Name); + Assert.AreEqual(product1.Name, query.ProductArray[0].Name); + Assert.AreEqual(product2.Price, query.ProductColl.ElementAt(0).Price); + Assert.AreEqual(null, query.ProductsNull); + Assert.AreEqual(0, query.ProductEmpty.Count); } } diff --git a/LiteDB.Tests/MapperInterfaceTest.cs b/LiteDB.Tests/Mapping/MapperInterfaceTest.cs similarity index 88% rename from LiteDB.Tests/MapperInterfaceTest.cs rename to LiteDB.Tests/Mapping/MapperInterfaceTest.cs index 76e634667..252369dc5 100644 --- a/LiteDB.Tests/MapperInterfaceTest.cs +++ b/LiteDB.Tests/Mapping/MapperInterfaceTest.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using System.Collections.Specialized; -namespace UnitTest +namespace LiteDB.Tests { [TestClass] @@ -57,8 +57,8 @@ public void MapInterfaces_Test() var bson2 = mapper.ToDocument(c2); // add _type in Impl property var bson3 = mapper.ToDocument(c3); // do not add _type in Impl property - Assert.AreEqual("UnitTest.MapperInterfaceTest+MyClassImpl, UnitTest", bson1["Impl"].AsDocument["_type"].AsString); - Assert.AreEqual("UnitTest.MapperInterfaceTest+MyClassImpl, UnitTest", bson2["Impl"].AsDocument["_type"].AsString); + Assert.AreEqual("LiteDB.Tests.MapperInterfaceTest+MyClassImpl, LiteDB.Tests", bson1["Impl"].AsDocument["_type"].AsString); + Assert.AreEqual("LiteDB.Tests.MapperInterfaceTest+MyClassImpl, LiteDB.Tests", bson2["Impl"].AsDocument["_type"].AsString); Assert.AreEqual(false, bson3["Impl"].AsDocument.ContainsKey("_type")); var k1 = mapper.ToObject(bson1); diff --git a/LiteDB.Tests/MapperTest.cs b/LiteDB.Tests/Mapping/MapperTest.cs similarity index 99% rename from LiteDB.Tests/MapperTest.cs rename to LiteDB.Tests/Mapping/MapperTest.cs index a62eb2c69..be4158af8 100644 --- a/LiteDB.Tests/MapperTest.cs +++ b/LiteDB.Tests/Mapping/MapperTest.cs @@ -8,7 +8,7 @@ using System.Collections.Specialized; using System.Security; -namespace UnitTest +namespace LiteDB.Tests { public enum MyEnum { First, Second } diff --git a/LiteDB.Tests/Utils/DB.cs b/LiteDB.Tests/Utils/DB.cs index 76afca802..f605c3687 100644 --- a/LiteDB.Tests/Utils/DB.cs +++ b/LiteDB.Tests/Utils/DB.cs @@ -7,24 +7,12 @@ using System.Threading.Tasks; using System.IO; -namespace UnitTest +namespace LiteDB.Tests { public class DB { - // Delete Temp databases - //static DB() - //{ - // var dir = System.IO.Path.GetDirectoryName(Path()); - - // foreach (var f in System.IO.Directory.GetFiles(dir, "*.db")) - // { - // System.IO.Directory.Delete(f); - // } - //} - - // Get a unique database name in TestResults folder - public static string Path() + public static string RandomFilename() { var path = System.IO.Path.GetFullPath( System.IO.Directory.GetCurrentDirectory() + diff --git a/LiteDB/Core/Collections/Aggregate.cs b/LiteDB/Core/Collections/Aggregate.cs index 52285bd1a..1a09bdaf3 100644 --- a/LiteDB/Core/Collections/Aggregate.cs +++ b/LiteDB/Core/Collections/Aggregate.cs @@ -16,6 +16,7 @@ public partial class LiteCollection ///
public int Count() { + // do not use indexes - collections has DocumentCount property return _engine.Count(_name, null); } @@ -26,7 +27,21 @@ public int Count(Query query) { if (query == null) throw new ArgumentNullException("query"); - return _engine.Count(_name, query); + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + return _engine.Count(_name, query); + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } } /// @@ -46,7 +61,21 @@ public bool Exists(Query query) { if (query == null) throw new ArgumentNullException("query"); - return _engine.Exists(_name, query); + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + return _engine.Exists(_name, query); + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } } /// @@ -70,7 +99,21 @@ public BsonValue Min(string field) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - return _engine.Min(_name, field); + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + return _engine.Min(_name, field); + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } } /// @@ -100,7 +143,21 @@ public BsonValue Max(string field) { if (string.IsNullOrEmpty(field)) throw new ArgumentNullException("field"); - return _engine.Max(_name, field); + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + return _engine.Max(_name, field); + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } } /// diff --git a/LiteDB/DbEngine/Actions/Aggregate.cs b/LiteDB/DbEngine/Actions/Aggregate.cs index a8a876d01..65dff3b2f 100644 --- a/LiteDB/DbEngine/Actions/Aggregate.cs +++ b/LiteDB/DbEngine/Actions/Aggregate.cs @@ -14,22 +14,25 @@ internal partial class DbEngine : IDisposable /// public BsonValue Min(string colName, string field) { - // get collection page (no col, no min) - var col = this.GetCollectionPage(colName, false); + lock(_locker) + { + // get collection page (no col, no min) + var col = this.GetCollectionPage(colName, false); - if(col == null) return BsonValue.MinValue; + if(col == null) return BsonValue.MinValue; - // get index (no index, no min) - var index = col.GetIndex(field); + // get index (no index, no min) + var index = col.GetIndex(field); - if (index == null) return BsonValue.MinValue; + if (index == null) return BsonValue.MinValue; - var head = _indexer.GetNode(index.HeadNode); - var next = _indexer.GetNode(head.Next[0]); + var head = _indexer.GetNode(index.HeadNode); + var next = _indexer.GetNode(head.Next[0]); - if (next.IsHeadTail(index)) return BsonValue.MinValue; + if (next.IsHeadTail(index)) return BsonValue.MinValue; - return next.Key; + return next.Key; + } } /// @@ -37,22 +40,25 @@ public BsonValue Min(string colName, string field) /// public BsonValue Max(string colName, string field) { - // get collection page (no col, no max) - var col = this.GetCollectionPage(colName, false); + lock(_locker) + { + // get collection page (no col, no max) + var col = this.GetCollectionPage(colName, false); - if (col == null) return BsonValue.MaxValue; + if (col == null) return BsonValue.MaxValue; - // get index (no index, no max) - var index = col.GetIndex(field); + // get index (no index, no max) + var index = col.GetIndex(field); - if (index == null) return BsonValue.MaxValue; + if (index == null) return BsonValue.MaxValue; - var tail = _indexer.GetNode(index.TailNode); - var prev = _indexer.GetNode(tail.Prev[0]); + var tail = _indexer.GetNode(index.TailNode); + var prev = _indexer.GetNode(tail.Prev[0]); - if (prev.IsHeadTail(index)) return BsonValue.MaxValue; + if (prev.IsHeadTail(index)) return BsonValue.MaxValue; - return prev.Key; + return prev.Key; + } } /// @@ -60,18 +66,21 @@ public BsonValue Max(string colName, string field) /// public int Count(string colName, Query query) { - // get collection page (no col, returns 0) - var col = this.GetCollectionPage(colName, false); + lock(_locker) + { + // get collection page (no col, returns 0) + var col = this.GetCollectionPage(colName, false); - if (col == null) return 0; + if (col == null) return 0; - if (query == null) return (int)col.DocumentCount; + if (query == null) return (int)col.DocumentCount; - // run query in this collection - var nodes = query.Run(col, _indexer); + // run query in this collection + var nodes = query.Run(col, _indexer); - // count all nodes - return nodes.Count(); + // count all nodes + return nodes.Count(); + } } /// @@ -79,16 +88,19 @@ public int Count(string colName, Query query) /// public bool Exists(string colName, Query query) { - // get collection page (no col, not exists) - var col = this.GetCollectionPage(colName, false); + lock(_locker) + { + // get collection page (no col, not exists) + var col = this.GetCollectionPage(colName, false); - if (col == null) return false; + if (col == null) return false; - // run query in this collection - var nodes = query.Run(col, _indexer); + // run query in this collection + var nodes = query.Run(col, _indexer); - // check if has at least first - return nodes.FirstOrDefault() != null; + // check if has at least first + return nodes.FirstOrDefault() != null; + } } } } diff --git a/LiteDB/DbEngine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs index 64511639e..821e25a8e 100644 --- a/LiteDB/DbEngine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -14,9 +14,12 @@ internal partial class DbEngine : IDisposable /// public IEnumerable GetCollectionNames() { - _transaction.AvoidDirtyRead(); + lock(_locker) + { + _transaction.AvoidDirtyRead(); - return _collections.GetAll().Select(x => x.CollectionName); + return _collections.GetAll().Select(x => x.CollectionName); + } } /// diff --git a/LiteDB/DbEngine/Actions/Find.cs b/LiteDB/DbEngine/Actions/Find.cs index d2bd2c7f4..b5d637d55 100644 --- a/LiteDB/DbEngine/Actions/Find.cs +++ b/LiteDB/DbEngine/Actions/Find.cs @@ -14,31 +14,34 @@ internal partial class DbEngine : IDisposable /// public IEnumerable Find(string colName, Query query, int skip = 0, int limit = int.MaxValue) { - // get my collection page - var col = this.GetCollectionPage(colName, false); + lock(_locker) + { + // get my collection page + var col = this.GetCollectionPage(colName, false); - // no collection, no documents - if(col == null) yield break; + // no collection, no documents + if(col == null) yield break; - // get nodes from query executor to get all IndexNodes - var nodes = query.Run(col, _indexer); + // get nodes from query executor to get all IndexNodes + var nodes = query.Run(col, _indexer); - // skip first N nodes - if (skip > 0) nodes = nodes.Skip(skip); + // skip first N nodes + if (skip > 0) nodes = nodes.Skip(skip); - // limit in M nodes - if (limit != int.MaxValue) nodes = nodes.Take(limit); + // limit in M nodes + if (limit != int.MaxValue) nodes = nodes.Take(limit); - // for each document, read data and deserialize as document - foreach (var node in nodes) - { - _log.Write(Logger.QUERY, "read document on '{0}' :: _id = {1}", colName, node.Key); + // for each document, read data and deserialize as document + foreach (var node in nodes) + { + _log.Write(Logger.QUERY, "read document on '{0}' :: _id = {1}", colName, node.Key); - var dataBlock = _data.Read(node.DataBlock, true); + var dataBlock = _data.Read(node.DataBlock, true); - var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; + var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; - yield return doc; + yield return doc; + } } } } diff --git a/LiteDB/DbEngine/Actions/Shrink.cs b/LiteDB/DbEngine/Actions/Shrink.cs index 78fca3ca0..dc4a15fba 100644 --- a/LiteDB/DbEngine/Actions/Shrink.cs +++ b/LiteDB/DbEngine/Actions/Shrink.cs @@ -14,72 +14,75 @@ internal partial class DbEngine : IDisposable /// public int Shrink() { - // lock and clear cache - no changes during shrink - _disk.Lock(); - _cache.Clear(); + lock(_locker) + { + // lock and clear cache - no changes during shrink + _disk.Lock(); + _cache.Clear(); - // create a temporary disk - var tempDisk = _disk.GetTempDisk(); + // create a temporary disk + var tempDisk = _disk.GetTempDisk(); - // get initial disk size - var header = _pager.GetPage(0); - var diff = 0; + // get initial disk size + var header = _pager.GetPage(0); + var diff = 0; - // create temp engine instance to copy all documents - using (var tempEngine = new DbEngine(tempDisk, new Logger())) - { - // read all collections - foreach (var col in _collections.GetAll()) + // create temp engine instance to copy all documents + using (var tempEngine = new DbEngine(tempDisk, new Logger())) { - // first copy all indexes - foreach(var index in col.GetIndexes(false)) + // read all collections + foreach (var col in _collections.GetAll()) { - tempEngine.EnsureIndex(col.CollectionName, index.Field, index.Options); - } + // first copy all indexes + foreach(var index in col.GetIndexes(false)) + { + tempEngine.EnsureIndex(col.CollectionName, index.Field, index.Options); + } - // then, read all documents and copy to new engine - var docs = _indexer.FindAll(col.PK, Query.Ascending); + // then, read all documents and copy to new engine + var docs = _indexer.FindAll(col.PK, Query.Ascending); - tempEngine.InsertDocuments(col.CollectionName, - docs.Select(x => BsonSerializer.Deserialize(_data.Read(x.DataBlock, true).Buffer))); - } + tempEngine.InsertDocuments(col.CollectionName, + docs.Select(x => BsonSerializer.Deserialize(_data.Read(x.DataBlock, true).Buffer))); + } - // get final header from temp engine - var tempHeader = tempEngine._pager.GetPage(0); + // get final header from temp engine + var tempHeader = tempEngine._pager.GetPage(0); - // copy info from initial header to final header - tempHeader.ChangeID = header.ChangeID; - tempHeader.UserVersion = header.UserVersion; + // copy info from initial header to final header + tempHeader.ChangeID = header.ChangeID; + tempHeader.UserVersion = header.UserVersion; - // lets create journal file before re-write - for (uint pageID = 0; pageID <= header.LastPageID; pageID++) - { - _disk.WriteJournal(pageID, _disk.ReadPage(pageID)); - } + // lets create journal file before re-write + for (uint pageID = 0; pageID <= header.LastPageID; pageID++) + { + _disk.WriteJournal(pageID, _disk.ReadPage(pageID)); + } - // commit journal + shrink data file - _disk.CommitJournal((tempHeader.LastPageID + 1) * BasePage.PAGE_SIZE); + // commit journal + shrink data file + _disk.CommitJournal((tempHeader.LastPageID + 1) * BasePage.PAGE_SIZE); - // lets re-write all pages copying from new database - for (uint pageID = 0; pageID <= tempHeader.LastPageID; pageID++) - { - _disk.WritePage(pageID, tempDisk.ReadPage(pageID)); - } + // lets re-write all pages copying from new database + for (uint pageID = 0; pageID <= tempHeader.LastPageID; pageID++) + { + _disk.WritePage(pageID, tempDisk.ReadPage(pageID)); + } - // now delete journal - _disk.DeleteJournal(); + // now delete journal + _disk.DeleteJournal(); - // get diff from initial and final last pageID - diff = (int)((header.LastPageID - tempHeader.LastPageID) * BasePage.PAGE_SIZE); - } + // get diff from initial and final last pageID + diff = (int)((header.LastPageID - tempHeader.LastPageID) * BasePage.PAGE_SIZE); + } - // unlock disk to continue - _disk.Unlock(); + // unlock disk to continue + _disk.Unlock(); - // delete temporary disk - _disk.DeleteTempDisk(); + // delete temporary disk + _disk.DeleteTempDisk(); - return diff; + return diff; + } } } } diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index 75f8ec123..6c3ddef16 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -60,6 +60,9 @@ public DbEngine(IDiskService disk, Logger log) /// private CollectionPage GetCollectionPage(string name, bool addIfNotExits) { + // before get a collection, avoid dirty reads + _transaction.AvoidDirtyRead(); + // search my page on collection service var col = _collections.Get(name); diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index e0bdd5e1c..a94becd4e 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -133,8 +133,12 @@ public void Unlock() public ushort GetChangeID() { var bytes = new byte[2]; - _stream.Seek(HeaderPage.CHANGE_ID_POSITION, SeekOrigin.Begin); - _stream.Read(bytes, 0, 2); + + this.TryExec(() => { + _stream.Seek(HeaderPage.CHANGE_ID_POSITION, SeekOrigin.Begin); + _stream.Read(bytes, 0, 2); + }); + return BitConverter.ToUInt16(bytes, 0); } @@ -356,7 +360,10 @@ private void TryExec(Action action) { try { - action(); + lock(_stream) + { + action(); + } return; } catch (UnauthorizedAccessException) diff --git a/LiteDB/DbEngine/Services/CacheService.cs b/LiteDB/DbEngine/Services/CacheService.cs index 676404777..897514bb7 100644 --- a/LiteDB/DbEngine/Services/CacheService.cs +++ b/LiteDB/DbEngine/Services/CacheService.cs @@ -40,13 +40,10 @@ public BasePage GetPage(uint pageID) /// public void AddPage(BasePage page) { - lock(_cache) + // do not cache extend page - never will be reused + if (page.PageType != PageType.Extend) { - // do not cache extend page - never will be reused - if (page.PageType != PageType.Extend) - { - _cache[page.PageID] = page; - } + _cache[page.PageID] = page; } } @@ -55,20 +52,17 @@ public void AddPage(BasePage page) /// public void SetPageDirty(BasePage page) { - lock(_dirty) - { - _dirty[page.PageID] = page; + _dirty[page.PageID] = page; - if (page.IsDirty) return; + if (page.IsDirty) return; - page.IsDirty = true; + page.IsDirty = true; - // if page is new (not exits on datafile), there is no journal for them - if (page.DiskData.Length > 0) - { - // call action passing dirty page - used for journal file writes - MarkAsDirtyAction(page); - } + // if page is new (not exits on datafile), there is no journal for them + if (page.DiskData.Length > 0) + { + // call action passing dirty page - used for journal file writes + MarkAsDirtyAction(page); } } @@ -79,11 +73,8 @@ public bool Clear() { var hasDirty = _dirty.Count > 0; - lock(_cache) - { - _dirty.Clear(); - _cache.Clear(); - } + _dirty.Clear(); + _cache.Clear(); return hasDirty; } diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index ebf96c4b5..bb2252dd4 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -29,12 +29,9 @@ public T GetPage(uint pageID) // is not on cache? load from disk if (page == null) { - lock(_disk) - { - var buffer = _disk.ReadPage(pageID); - page = BasePage.ReadPage(buffer); - _cache.AddPage(page); - } + var buffer = _disk.ReadPage(pageID); + page = BasePage.ReadPage(buffer); + _cache.AddPage(page); } #if DEBUG diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 68a6bf620..f75641e28 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -99,27 +99,21 @@ public void Rollback() /// public bool AvoidDirtyRead() { - lock (_disk) - { - // if is in transaction pages are not dirty (begin trans was checked) - if (_trans == true) return false; - - var cache = (HeaderPage)_cache.GetPage(0); + var cache = (HeaderPage)_cache.GetPage(0); - if (cache == null) return false; + if (cache == null) return false; - // read change direct from disk - var change = _disk.GetChangeID(); - - // if changeID was changed, file was changed by another process - clear all cache - if (cache.ChangeID != change) - { - _cache.Clear(); - return true; - } + // read change direct from disk + var change = _disk.GetChangeID(); - return false; + // if changeID was changed, file was changed by another process - clear all cache + if (cache.ChangeID != change) + { + _cache.Clear(); + return true; } + + return false; } } } diff --git a/LiteDB/Serializer/Mapper/BsonMapper.cs b/LiteDB/Serializer/Mapper/BsonMapper.cs index 8ded019ff..f7a71d37e 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.cs @@ -225,14 +225,17 @@ internal Dictionary GetPropertyMapper(Type type) { Dictionary props; - if (_mapper.TryGetValue(type, out props)) + lock(_mapper) { - return props; - } + if (_mapper.TryGetValue(type, out props)) + { + return props; + } - _mapper[type] = Reflection.GetProperties(type, this.ResolvePropertyName); + _mapper[type] = Reflection.GetProperties(type, this.ResolvePropertyName); - return _mapper[type]; + return _mapper[type]; + } } /// From 292c3d2519adf7858c5e50968e30dc7e8e7e897c Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 15:27:43 -0200 Subject: [PATCH 80/91] Update README --- .vs/config/applicationhost.config | 1027 -------------------------- LiteDB-TODO.txt | 55 -- LiteDB.Shell/Commands/Help.cs | 19 +- LiteDB/Shell/Commands/Others/Dump.cs | 2 + README.md | 27 +- 5 files changed, 28 insertions(+), 1102 deletions(-) delete mode 100644 .vs/config/applicationhost.config delete mode 100644 LiteDB-TODO.txt diff --git a/.vs/config/applicationhost.config b/.vs/config/applicationhost.config deleted file mode 100644 index 9fc3131fe..000000000 --- a/.vs/config/applicationhost.config +++ /dev/null @@ -1,1027 +0,0 @@ - - - - - - - - -
-
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LiteDB-TODO.txt b/LiteDB-TODO.txt deleted file mode 100644 index eb76e6e28..000000000 --- a/LiteDB-TODO.txt +++ /dev/null @@ -1,55 +0,0 @@ -## TODO para v2 BETA (até sexta) -- Revisar help do Shell - saiu varias coisas -- ThreadSafe -- Unit tests básicos -- Test script todos os comandos -- Merge para master -- Folder root: "src", "test", "nuget" -- Revisar site - -## TODO para v2 RC (até 31/dez) -- Atualizar documentação -- Revisar site -- O problema de Max do AutoInc - Que tal uma sequence na collection? NextId() -- Colocar DbParams na header. _engine.GetParams(), _engine.SetParams(p) (limit 4k) -- Unit tests melhorados -- Colocar documentação neste site -- Migrar unit tests para XUnit ou outro q funcione no ASPNET5 - -## TODO para v2.0 (até 31/jan) -- Revisar BsonArray.Length não faz cache -- New UserVersion. this.UserVersions(1, (db) => { .. }) -- Migration tool? Shell.Import/Export? -- Implementar DbRefAttribute - -////////////////////////////////////////////////////////////////////////////////////////////////////// - - -## Thread Safe / Process Safe -- Thread safe no BsonMapper static methods (Reflection) -- Lock total de read. -- Mesmo comandos de QUERY devem ter LOCK por causa do uso da CACHE (ou faço lock na cache?) -- Revisar classes de servico/collection pra ver quais sao as variaveis de classe q mudam situacao (vide _cache) -- Avaliar uso do ConcurrentDictionary na cache - -## FUTURE versions 2.x: - -- DefaultIndexOptions? Ou sem default - não cria indice automaticamente - ou full search? -- Queria que o IDiskService não tivesse informações sobre Journal/Recovery -- Encrypt datafile (EncryptDiskService : FileDiskService) -- Que tal ter a possibilidade utilizar serializacao de terceiros (via JSON.NET)? IBsonMapper? -- PCL e ASPNET 5 - -## V2 Features -- New DbEngine -- IDiskService -- New DbRef -- Entity<> mapper -- ThreadSafe -- New lock read/write -- Debugger -- Better journal : restore before crash -- ACID per document/collections -- Lazy load -* ByteArray, DbEngine, abstract BasePage -- FileLimit and Initial Size diff --git a/LiteDB.Shell/Commands/Help.cs b/LiteDB.Shell/Commands/Help.cs index cfcdcaa1e..78f0085f7 100644 --- a/LiteDB.Shell/Commands/Help.cs +++ b/LiteDB.Shell/Commands/Help.cs @@ -53,17 +53,10 @@ public override void Execute(LiteShell shell, StringScanner s, Display d, InputC d.WriteHelp("> spool on|off", "Spool all output in a spool file"); d.WriteHelp("> -- comment", "Do nothing, its just a comment"); d.WriteHelp("> //", "Support for multi line command"); + d.WriteHelp("> debug on|off", "Enabled debug messages from dbengine"); d.WriteHelp("> version", "Show LiteDB version"); d.WriteHelp("> exit", "Close LiteDB shell"); - d.WriteHelp(); - d.WriteHelp("Transaction commands"); - d.WriteHelp("===================="); - - d.WriteHelp("> begin", "Begins a new transaction"); - d.WriteHelp("> commit", "Commit current transaction"); - d.WriteHelp("> rollback", "Rollback current transaction"); - d.WriteHelp(); d.WriteHelp("Collections commands"); d.WriteHelp("===================="); @@ -75,7 +68,6 @@ public override void Execute(LiteShell shell, StringScanner s, Display d, InputC d.WriteHelp("> db..find [skip N][limit N]", "Show all documents. Can limit/skip results"); d.WriteHelp("> db..find [skip N][limit N]", "Show filtered documents based on index search. See syntax below"); d.WriteHelp("> db..count ", "Show count rows according query filter"); - d.WriteHelp("> db..exec { Action }", "Execute C# code for each document based on filter."); d.WriteHelp("> db..ensureIndex [unique]", "Create a new index document field"); d.WriteHelp("> db..indexes", "List all indexes in this collection"); d.WriteHelp("> db..drop", "Drop collection and destroy all documents inside"); @@ -86,9 +78,9 @@ public override void Execute(LiteShell shell, StringScanner s, Display d, InputC d.WriteHelp(" = [=|>|>=|<|<=|!=|like|contains|in|between] ", "Filter query syntax"); d.WriteHelp(" = ( [and|or] [and|or] ...)", "Multi queries syntax"); d.WriteHelp(" = {_id: ... , key: value, key1: value1 }", "Represent a json (extended version) for a BsonDocument. See special data types"); - d.WriteHelp("Json Date", "{ mydate: { $date :\"2015-01-01T23:59:59Z\"} }"); - d.WriteHelp("Json Guid", "{ myguid: { $guid :\"3a1c34b3-9f66-4d8e-975a-d545d898a4ba\"} }"); - d.WriteHelp("Json Binary", "{ mydata: { $binary :\"base64 byte array\"} }"); + d.WriteHelp("Json Date", "{ field: { $date :\"2015-01-01T23:59:59Z\"} }"); + d.WriteHelp("Json Guid", "{ field: { $guid :\"3a1c34b3-9f66-4d8e-975a-d545d898a4ba\"} }"); + d.WriteHelp("Json Binary", "{ field: { $binary :\"base64 byte array\"} }"); d.WriteHelp(); d.WriteHelp("File storage commands"); @@ -105,7 +97,8 @@ public override void Execute(LiteShell shell, StringScanner s, Display d, InputC d.WriteHelp("Other commands"); d.WriteHelp("=============="); - d.WriteHelp("> dump", "Display dump database information"); + d.WriteHelp("> shrink", "Reduce datafile removing empty pages"); + d.WriteHelp("> dump [n m]", "Display dump database pages (from N page to M page)"); } } } diff --git a/LiteDB/Shell/Commands/Others/Dump.cs b/LiteDB/Shell/Commands/Others/Dump.cs index 3f3dbcd65..979d6dd4e 100644 --- a/LiteDB/Shell/Commands/Others/Dump.cs +++ b/LiteDB/Shell/Commands/Others/Dump.cs @@ -21,6 +21,8 @@ public BsonValue Execute(LiteDatabase db, StringScanner s) var start = s.Scan(@"\d*").Trim(); var end = s.Scan(@"\s*\d*").Trim(); + if(start.Length > 0 && end.Length == 0) end = start; + return db.DumpPages( start.Length == 0 ? 0 : Convert.ToUInt32(start), end.Length == 0 ? uint.MaxValue : Convert.ToUInt32(end)); diff --git a/README.md b/README.md index cc46d971e..4ee649038 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,41 @@ # LiteDB - A .NET NoSQL Document Store in a single data file +> v2.0 beta (for last 1.x version, see `Tags`) + LiteDB is a small, fast and lightweight NoSQL embedded database. - Serverless NoSQL Document Store - Simple API similar to MongoDB - 100% C# code for .NET 4 in a single DLL (less than 200kb) -- Transaction control - ACID -- Recovery in writing failure (journal mode) -- Store POCO classes or `BsonDocument` +- Document ACID transaction +- Recovery data in writing failure (journal mode) +- Map your POCO class to `BsonDocument` - Store files and stream data (like GridFS in MongoDB) - Single data file storage (like SQLite) - Index document fields for fast search (up to 16 indexes per collection) - LINQ support for queries -- Shell command line - [try this online version](http://litedb.azurewebsites.net/) +- Shell command line - [try this online version](http://www.litedb.org/) - Open source and free for everyone - including commercial use - Install from NuGet: `Install-Package LiteDB` +## New features in v2.0 beta +- Generic data access - can use any `Stream` +- Better mapping class from your entity to `BsonDocument` (like EntityFramework) +- Better cross reference with `DbRef` mapping +- ThreadSafe / ProcessSafe +- Lazy engine load (open datafile only when run a command) +- Reduce your database size with shrink +- Support for `Initial Size` and `Limit Size` databases +- Complete re-write engine classes width full debug logger +- See more examples in http://www.litedb.org/ + ## Try online -[Try LiteDB Web Shell](http://litedb.azurewebsites.net/). For security reasons, in online version not all commands are available. Try offline version for full features tests. +[Try LiteDB Web Shell](http://www.litedb.org/). For security reasons, in online version not all commands are available. Try offline version for full features tests. ## Documentation -Visit [Wiki for full documentation](https://github.com/mbdavid/LiteDB/wiki) +Visit [Wiki for full documentation](https://github.com/mbdavid/LiteDB/wiki) (v1.x) ## Download @@ -89,4 +102,4 @@ Details changes for each release are documented in the [release notes](https://g [MIT](http://opensource.org/licenses/MIT) -Copyright (c) 2015 - Maurício David +Copyright (c) 2015 - Maurício David \ No newline at end of file From d777211e48ddf4f6b93a664a74bae456e792b725 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 15:45:45 -0200 Subject: [PATCH 81/91] Added .vs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 46d3fa5dd..3174f92a8 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ dlldata.c *.pidb *.svclog *.scc +.vs # Chutzpah Test files _Chutzpah* From e3dea53ecdd337b7f625cc61dc9d5f33d042f728 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 16:11:27 -0200 Subject: [PATCH 82/91] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ee649038..5cd499fb7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ LiteDB is a small, fast and lightweight NoSQL embedded database. - Single data file storage (like SQLite) - Index document fields for fast search (up to 16 indexes per collection) - LINQ support for queries -- Shell command line - [try this online version](http://www.litedb.org/) +- Shell command line - [try this online version](http://www.litedb.org/#shell) - Open source and free for everyone - including commercial use - Install from NuGet: `Install-Package LiteDB` @@ -31,7 +31,7 @@ LiteDB is a small, fast and lightweight NoSQL embedded database. ## Try online -[Try LiteDB Web Shell](http://www.litedb.org/). For security reasons, in online version not all commands are available. Try offline version for full features tests. +[Try LiteDB Web Shell](http://www.litedb.org/#shell). For security reasons, in online version not all commands are available. Try offline version for full features tests. ## Documentation From 733c7f58dc7b614ae6d3a2185c206c11d28434a7 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 20:08:36 -0200 Subject: [PATCH 83/91] Add delete auto index creation --- LiteDB/Core/Collections/Delete.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/LiteDB/Core/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs index af0d7eb7b..1f8112f60 100644 --- a/LiteDB/Core/Collections/Delete.cs +++ b/LiteDB/Core/Collections/Delete.cs @@ -16,7 +16,21 @@ public int Delete(Query query) { if(query == null) throw new ArgumentNullException("query"); - return _engine.DeleteDocuments(_name, query); + // keep trying execute query to auto-create indexes when not found + while (true) + { + try + { + return _engine.DeleteDocuments(_name, query); + } + catch (IndexNotFoundException ex) + { + // if query returns this exception, let's auto create using mapper (or using default options) + var options = _mapper.GetIndexFromMapper(ex.Field) ?? new IndexOptions(); + + _engine.EnsureIndex(ex.Collection, ex.Field, options); + } + } } /// From 811c4687dfc2b78b853c5c948a287ba05f945656 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 20:19:41 -0200 Subject: [PATCH 84/91] Remove bulk insert --- LiteDB/Core/Collections/InsertBulk.cs | 39 --------------------------- LiteDB/LiteDB.csproj | 1 - 2 files changed, 40 deletions(-) delete mode 100644 LiteDB/Core/Collections/InsertBulk.cs diff --git a/LiteDB/Core/Collections/InsertBulk.cs b/LiteDB/Core/Collections/InsertBulk.cs deleted file mode 100644 index 6aebbdee2..000000000 --- a/LiteDB/Core/Collections/InsertBulk.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace LiteDB -{ - public partial class LiteCollection - { - /// - /// Bulk documents to a collection - use data chunks for most efficient insert - /// - public int InsertBulk(IEnumerable docs, int buffer = 32768) - { - if (docs == null) throw new ArgumentNullException("docs"); - if (buffer <= 1) throw new ArgumentException("buffer must be bigger than 1"); - - var count = 0; - - while (true) - { - // get a slice of documents of docs - var slice = docs.Skip(count).Take(buffer); - - // insert this docs - var included = this.Insert(slice); - - // if included less than buffer, there is no more to insert - if (included < buffer) - { - return count + included; - } - - count += included; - } - } - } -} diff --git a/LiteDB/LiteDB.csproj b/LiteDB/LiteDB.csproj index 3b7861a7c..85659e513 100644 --- a/LiteDB/LiteDB.csproj +++ b/LiteDB/LiteDB.csproj @@ -51,7 +51,6 @@ - From c7ed11152bba61552a32a4c9e89a2a15dbd58c2c Mon Sep 17 00:00:00 2001 From: mbdavid Date: Wed, 18 Nov 2015 21:03:33 -0200 Subject: [PATCH 85/91] Insert bulk with buffer size --- LiteDB.Tests/Crud/BulkTest.cs | 2 +- LiteDB/Core/Collections/Insert.cs | 10 +-- LiteDB/DbEngine/Actions/Insert.cs | 99 +++++++++++++++++---------- LiteDB/DbEngine/Actions/Shrink.cs | 3 +- LiteDB/FileStorage/LiteFileStorage.cs | 4 +- 5 files changed, 72 insertions(+), 46 deletions(-) diff --git a/LiteDB.Tests/Crud/BulkTest.cs b/LiteDB.Tests/Crud/BulkTest.cs index 75393102f..b640bd8b6 100644 --- a/LiteDB.Tests/Crud/BulkTest.cs +++ b/LiteDB.Tests/Crud/BulkTest.cs @@ -17,7 +17,7 @@ public void Bulk_Test() { var col = db.GetCollection("b"); - col.InsertBulk(GetDocs(), 50); + col.Insert(GetDocs(), 50); Assert.AreEqual(220, col.Count()); } diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index 124a4d84e..a946eb266 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -8,6 +8,8 @@ namespace LiteDB { public partial class LiteCollection { + public const int DOCUMENT_BUFFER_SIZE = 1024; + /// /// Insert a new document to this collection. Document Id must be a new value in collection - Returns document Id /// @@ -19,19 +21,19 @@ public BsonValue Insert(T document) var doc = _mapper.ToDocument(document); - _engine.InsertDocuments(_name, new BsonDocument[] { doc }); + _engine.InsertDocuments(_name, new BsonDocument[] { doc }, 1); return doc["_id"]; } /// - /// Insert an array of new documents to this collection. Document Id must be a new value in collection + /// Insert an array of new documents to this collection. Document Id must be a new value in collection. Can be set buffer size to commit at each N documents /// - public int Insert(IEnumerable docs) + public int Insert(IEnumerable docs, int buffer = DOCUMENT_BUFFER_SIZE) { if (docs == null) throw new ArgumentNullException("docs"); - return _engine.InsertDocuments(_name, this.GetBsonDocs(docs)); + return _engine.InsertDocuments(_name, this.GetBsonDocs(docs), buffer); } /// diff --git a/LiteDB/DbEngine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs index f0d2bbf92..d05ebe7d9 100644 --- a/LiteDB/DbEngine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -10,63 +10,86 @@ namespace LiteDB internal partial class DbEngine : IDisposable { /// - /// Implements insert documents in a collection + /// Implements insert documents in a collection - use a buffer to commit transaction in each buffer count /// - public int InsertDocuments(string colName, IEnumerable docs) + public int InsertDocuments(string colName, IEnumerable docs, int bufferSize) { - return this.Transaction(colName, true, (col) => + var enumerator = docs.GetEnumerator(); + var count = 0; + var more = true; + + while (true) { - var count = 0; + var buffer = bufferSize; - foreach (var doc in docs) + this.Transaction(colName, true, (col) => { - BsonValue id; + more = true; - // add ObjectId to _id if _id not found - if (!doc.RawValue.TryGetValue("_id", out id)) + while (buffer > 0 && (more = enumerator.MoveNext())) { - id = doc["_id"] = ObjectId.NewObjectId(); + this.InsertDocument(col, enumerator.Current); + buffer--; + count++; } - // test if _id is a valid type - if (id.IsNull || id.IsMinValue || id.IsMaxValue) - { - throw LiteException.InvalidDataType("_id", id); - } + return 0; + }); + + if (more == false) + { + return count; + } + } + } + + /// + /// Insert a single document - must be used only inside "InsertDocuments" + /// + private void InsertDocument(CollectionPage col, BsonDocument doc) + { + BsonValue id; - _log.Write(Logger.COMMAND, "insert document on '{0}' :: _id = {1}", colName, id); + // add ObjectId to _id if _id not found + if (!doc.RawValue.TryGetValue("_id", out id)) + { + id = doc["_id"] = ObjectId.NewObjectId(); + } - // serialize object - var bytes = BsonSerializer.Serialize(doc); + // test if _id is a valid type + if (id.IsNull || id.IsMinValue || id.IsMaxValue) + { + throw LiteException.InvalidDataType("_id", id); + } - // storage in data pages - returns dataBlock address - var dataBlock = _data.Insert(col, bytes); + _log.Write(Logger.COMMAND, "insert document on '{0}' :: _id = {1}", col.CollectionName, id); - // store id in a PK index [0 array] - var pk = _indexer.AddNode(col.PK, id); + // serialize object + var bytes = BsonSerializer.Serialize(doc); - // do links between index <-> data block - pk.DataBlock = dataBlock.Position; - dataBlock.IndexRef[0] = pk.Position; + // storage in data pages - returns dataBlock address + var dataBlock = _data.Insert(col, bytes); - // for each index, insert new IndexNode - foreach (var index in col.GetIndexes(false)) - { - var key = doc.Get(index.Field); + // store id in a PK index [0 array] + var pk = _indexer.AddNode(col.PK, id); - var node = _indexer.AddNode(index, key); + // do links between index <-> data block + pk.DataBlock = dataBlock.Position; + dataBlock.IndexRef[0] = pk.Position; - // point my index to data object - node.DataBlock = dataBlock.Position; + // for each index, insert new IndexNode + foreach (var index in col.GetIndexes(false)) + { + var key = doc.Get(index.Field); - // point my dataBlock - dataBlock.IndexRef[index.Slot] = node.Position; - } - count++; - } + var node = _indexer.AddNode(index, key); + + // point my index to data object + node.DataBlock = dataBlock.Position; - return count++; - }); + // point my dataBlock + dataBlock.IndexRef[index.Slot] = node.Position; + } } } } diff --git a/LiteDB/DbEngine/Actions/Shrink.cs b/LiteDB/DbEngine/Actions/Shrink.cs index dc4a15fba..34f6ef0ff 100644 --- a/LiteDB/DbEngine/Actions/Shrink.cs +++ b/LiteDB/DbEngine/Actions/Shrink.cs @@ -43,7 +43,8 @@ public int Shrink() var docs = _indexer.FindAll(col.PK, Query.Ascending); tempEngine.InsertDocuments(col.CollectionName, - docs.Select(x => BsonSerializer.Deserialize(_data.Read(x.DataBlock, true).Buffer))); + docs.Select(x => BsonSerializer.Deserialize(_data.Read(x.DataBlock, true).Buffer)), + 1024); } // get final header from temp engine diff --git a/LiteDB/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs index e354e3598..163d3b496 100644 --- a/LiteDB/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -34,12 +34,12 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) file.UploadDate = DateTime.Now; // insert file in _files collections with 0 file length - _engine.InsertDocuments(FILES, new BsonDocument[] { file.AsDocument }); + _engine.InsertDocuments(FILES, new BsonDocument[] { file.AsDocument }, 1); // for each chunk, insert as a chunk document foreach (var chunk in file.CreateChunks(stream)) { - _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }); + _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }, 1024); } // update fileLength/chunks to confirm full file length stored in disk From c09b8a87e016ec81bbf2f14c38eaad6869f74af5 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 19 Nov 2015 14:25:05 -0200 Subject: [PATCH 86/91] Insert/Update/Deletel buffer size --- LiteDB/Core/Collections/Delete.cs | 2 +- LiteDB/Core/Collections/Insert.cs | 6 +- LiteDB/Core/Collections/LiteCollection.cs | 5 + LiteDB/Core/Collections/Update.cs | 35 ++----- LiteDB/DbEngine/Actions/Collection.cs | 5 +- LiteDB/DbEngine/Actions/Delete.cs | 37 +++----- LiteDB/DbEngine/Actions/Index.cs | 12 +-- LiteDB/DbEngine/Actions/Insert.cs | 91 +++++++------------ LiteDB/DbEngine/Actions/Shrink.cs | 8 +- LiteDB/DbEngine/Actions/Update.cs | 73 +++++++-------- LiteDB/DbEngine/DbEngine.cs | 57 +++++++++++- LiteDB/DbEngine/Pages/CollectionPage.cs | 2 +- LiteDB/DbEngine/Pages/HeaderPage.cs | 8 ++ LiteDB/DbEngine/Services/CollectionService.cs | 19 ++-- LiteDB/DbEngine/Services/DataService.cs | 30 +++--- LiteDB/DbEngine/Services/IndexService.cs | 22 ++--- LiteDB/DbEngine/Services/PageService.cs | 74 +++++++++------ .../DbEngine/Services/TransactionService.cs | 25 +++-- LiteDB/FileStorage/LiteFileStorage.cs | 11 ++- .../Serializer/Mapper/BsonMapper.Serialize.cs | 2 +- 20 files changed, 269 insertions(+), 255 deletions(-) diff --git a/LiteDB/Core/Collections/Delete.cs b/LiteDB/Core/Collections/Delete.cs index 1f8112f60..c715e043a 100644 --- a/LiteDB/Core/Collections/Delete.cs +++ b/LiteDB/Core/Collections/Delete.cs @@ -21,7 +21,7 @@ public int Delete(Query query) { try { - return _engine.DeleteDocuments(_name, query); + return _engine.DeleteDocuments(_name, query, BUFFER_SIZE); } catch (IndexNotFoundException ex) { diff --git a/LiteDB/Core/Collections/Insert.cs b/LiteDB/Core/Collections/Insert.cs index a946eb266..9eab05e1e 100644 --- a/LiteDB/Core/Collections/Insert.cs +++ b/LiteDB/Core/Collections/Insert.cs @@ -8,8 +8,6 @@ namespace LiteDB { public partial class LiteCollection { - public const int DOCUMENT_BUFFER_SIZE = 1024; - /// /// Insert a new document to this collection. Document Id must be a new value in collection - Returns document Id /// @@ -29,11 +27,11 @@ public BsonValue Insert(T document) /// /// Insert an array of new documents to this collection. Document Id must be a new value in collection. Can be set buffer size to commit at each N documents /// - public int Insert(IEnumerable docs, int buffer = DOCUMENT_BUFFER_SIZE) + public int Insert(IEnumerable docs, int bufferSize = BUFFER_SIZE) { if (docs == null) throw new ArgumentNullException("docs"); - return _engine.InsertDocuments(_name, this.GetBsonDocs(docs), buffer); + return _engine.InsertDocuments(_name, this.GetBsonDocs(docs), bufferSize); } /// diff --git a/LiteDB/Core/Collections/LiteCollection.cs b/LiteDB/Core/Collections/LiteCollection.cs index 2d87293ea..437ec9ce4 100644 --- a/LiteDB/Core/Collections/LiteCollection.cs +++ b/LiteDB/Core/Collections/LiteCollection.cs @@ -9,6 +9,11 @@ namespace LiteDB public sealed partial class LiteCollection where T : new() { + /// + /// Default buffer size for insert/update/delete loop operations - transaction will commit in every N docs + /// + private const int BUFFER_SIZE = 1024; + private string _name; private DbEngine _engine; private BsonMapper _mapper; diff --git a/LiteDB/Core/Collections/Update.cs b/LiteDB/Core/Collections/Update.cs index d79c10e6a..c481de432 100644 --- a/LiteDB/Core/Collections/Update.cs +++ b/LiteDB/Core/Collections/Update.cs @@ -19,7 +19,7 @@ public bool Update(T document) // get BsonDocument from object var doc = _mapper.ToDocument(document); - return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }) > 0; + return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }, 1) > 0; } /// @@ -36,40 +36,17 @@ public bool Update(BsonValue id, T document) // set document _id using id parameter doc["_id"] = id; - return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }) > 0; + return _engine.UpdateDocuments(_name, new BsonDocument[] { doc }, 1) > 0; } /// - /// Query documents and execute, for each document, action method. After action, update each document + /// Update all documents /// - public int Update(Query query, Action action) + public int Update(IEnumerable documents) { - if (query == null) throw new ArgumentNullException("query"); - if (action == null) throw new ArgumentNullException("action"); + if (documents == null) throw new ArgumentNullException("document"); - var docs = this.Find(query).ToArray(); // used to avoid changes during Action - var count = 0; - - foreach (var doc in docs) - { - action(doc); - - // get BsonDocument from object - var bson = _mapper.ToDocument(doc); - - throw new NotImplementedException(); - //count += _engine.UpdateDocuments(_name, id, bson) ? 1 : 0; - } - - return count; - } - - /// - /// Query documents and execute, for each document, action method. All data is locked during execution - /// - public void Update(Expression> predicate, Action action) - { - this.Update(_visitor.Visit(predicate), action); + return _engine.UpdateDocuments(_name, documents.Select(x => _mapper.ToDocument(x)), BUFFER_SIZE); } } } diff --git a/LiteDB/DbEngine/Actions/Collection.cs b/LiteDB/DbEngine/Actions/Collection.cs index 821e25a8e..b734fa3e4 100644 --- a/LiteDB/DbEngine/Actions/Collection.cs +++ b/LiteDB/DbEngine/Actions/Collection.cs @@ -50,11 +50,12 @@ public bool RenameCollection(string colName, string newName) _log.Write(Logger.COMMAND, "rename collection '{0}' -> '{1}'", colName, newName); + // set page as dirty before any change + _pager.SetDirty(col); + // change collection name and save col.CollectionName = newName; - _pager.SetDirty(col); - return true; }); } diff --git a/LiteDB/DbEngine/Actions/Delete.cs b/LiteDB/DbEngine/Actions/Delete.cs index c0007960a..0a6a904b7 100644 --- a/LiteDB/DbEngine/Actions/Delete.cs +++ b/LiteDB/DbEngine/Actions/Delete.cs @@ -12,38 +12,25 @@ internal partial class DbEngine : IDisposable /// /// Implements delete based on a query result /// - public int DeleteDocuments(string colName, Query query) + public int DeleteDocuments(string colName, Query query, int bufferSize) { - return this.Transaction(colName, false, (col) => - { - // no collection, no document - abort trans - if (col == null) return 0; + return this.TransactionLoop(colName, false, bufferSize, (c) => query.Run(c, _indexer), (col, node) => { - var count = 0; + _log.Write(Logger.COMMAND, "delete document on '{0}' :: _id = {1}", colName, node.Key); - // find nodes - var nodes = query.Run(col, _indexer); + // read dataBlock (do not read all extend pages, i will not use) + var dataBlock = _data.Read(node.DataBlock, false); - foreach (var node in nodes) + // lets remove all indexes that point to this in dataBlock + foreach (var index in col.GetIndexes(true)) { - _log.Write(Logger.COMMAND, "delete document on '{0}' :: _id = {1}", colName, node.Key); - - // read dataBlock (do not read all extend pages, i will not use) - var dataBlock = _data.Read(node.DataBlock, false); - - // lets remove all indexes that point to this in dataBlock - foreach (var index in col.GetIndexes(true)) - { - _indexer.Delete(index, dataBlock.IndexRef[index.Slot]); - } - - // remove object data - _data.Delete(col, node.DataBlock); - - count++; + _indexer.Delete(index, dataBlock.IndexRef[index.Slot]); } - return count; + // remove object data + _data.Delete(col, node.DataBlock); + + return true; }); } } diff --git a/LiteDB/DbEngine/Actions/Index.cs b/LiteDB/DbEngine/Actions/Index.cs index c3fc909eb..3fb255828 100644 --- a/LiteDB/DbEngine/Actions/Index.cs +++ b/LiteDB/DbEngine/Actions/Index.cs @@ -32,6 +32,9 @@ public bool EnsureIndex(string colName, string field, IndexOptions options) { var dataBlock = _data.Read(node.DataBlock, true); + // mark datablock page as dirty + _pager.SetDirty(dataBlock.Page); + // read object var doc = BsonSerializer.Deserialize(dataBlock.Buffer).AsDocument; @@ -45,9 +48,6 @@ public bool EnsureIndex(string colName, string field, IndexOptions options) // link index node to datablock newNode.DataBlock = dataBlock.Position; - - // mark datablock page as dirty - _pager.SetDirty(dataBlock.Page); } return true; @@ -66,6 +66,9 @@ public bool DropIndex(string colName, string field) // no collection, no index if (col == null) return false; + // mark collection page as dirty before changes + _pager.SetDirty(col); + // search for index reference var index = col.GetIndex(field); @@ -80,9 +83,6 @@ public bool DropIndex(string colName, string field) // clear index reference index.Clear(); - // save collection page - _pager.SetDirty(col); - return true; }); } diff --git a/LiteDB/DbEngine/Actions/Insert.cs b/LiteDB/DbEngine/Actions/Insert.cs index d05ebe7d9..76aa24f7e 100644 --- a/LiteDB/DbEngine/Actions/Insert.cs +++ b/LiteDB/DbEngine/Actions/Insert.cs @@ -14,82 +14,53 @@ internal partial class DbEngine : IDisposable /// public int InsertDocuments(string colName, IEnumerable docs, int bufferSize) { - var enumerator = docs.GetEnumerator(); - var count = 0; - var more = true; + return this.TransactionLoop(colName, true, bufferSize, (c) => docs, (col, doc) => { - while (true) - { - var buffer = bufferSize; + BsonValue id; - this.Transaction(colName, true, (col) => + // add ObjectId to _id if _id not found + if (!doc.RawValue.TryGetValue("_id", out id)) { - more = true; - - while (buffer > 0 && (more = enumerator.MoveNext())) - { - this.InsertDocument(col, enumerator.Current); - buffer--; - count++; - } - - return 0; - }); + id = doc["_id"] = ObjectId.NewObjectId(); + } - if (more == false) + // test if _id is a valid type + if (id.IsNull || id.IsMinValue || id.IsMaxValue) { - return count; + throw LiteException.InvalidDataType("_id", id); } - } - } - /// - /// Insert a single document - must be used only inside "InsertDocuments" - /// - private void InsertDocument(CollectionPage col, BsonDocument doc) - { - BsonValue id; - - // add ObjectId to _id if _id not found - if (!doc.RawValue.TryGetValue("_id", out id)) - { - id = doc["_id"] = ObjectId.NewObjectId(); - } - - // test if _id is a valid type - if (id.IsNull || id.IsMinValue || id.IsMaxValue) - { - throw LiteException.InvalidDataType("_id", id); - } + _log.Write(Logger.COMMAND, "insert document on '{0}' :: _id = {1}", col.CollectionName, id); - _log.Write(Logger.COMMAND, "insert document on '{0}' :: _id = {1}", col.CollectionName, id); + // serialize object + var bytes = BsonSerializer.Serialize(doc); - // serialize object - var bytes = BsonSerializer.Serialize(doc); + // storage in data pages - returns dataBlock address + var dataBlock = _data.Insert(col, bytes); - // storage in data pages - returns dataBlock address - var dataBlock = _data.Insert(col, bytes); + // store id in a PK index [0 array] + var pk = _indexer.AddNode(col.PK, id); - // store id in a PK index [0 array] - var pk = _indexer.AddNode(col.PK, id); + // do links between index <-> data block + pk.DataBlock = dataBlock.Position; + dataBlock.IndexRef[0] = pk.Position; - // do links between index <-> data block - pk.DataBlock = dataBlock.Position; - dataBlock.IndexRef[0] = pk.Position; + // for each index, insert new IndexNode + foreach (var index in col.GetIndexes(false)) + { + var key = doc.Get(index.Field); - // for each index, insert new IndexNode - foreach (var index in col.GetIndexes(false)) - { - var key = doc.Get(index.Field); + var node = _indexer.AddNode(index, key); - var node = _indexer.AddNode(index, key); + // point my index to data object + node.DataBlock = dataBlock.Position; - // point my index to data object - node.DataBlock = dataBlock.Position; + // point my dataBlock + dataBlock.IndexRef[index.Slot] = node.Position; + } - // point my dataBlock - dataBlock.IndexRef[index.Slot] = node.Position; - } + return true; + }); } } } diff --git a/LiteDB/DbEngine/Actions/Shrink.cs b/LiteDB/DbEngine/Actions/Shrink.cs index 34f6ef0ff..77449296d 100644 --- a/LiteDB/DbEngine/Actions/Shrink.cs +++ b/LiteDB/DbEngine/Actions/Shrink.cs @@ -42,13 +42,14 @@ public int Shrink() // then, read all documents and copy to new engine var docs = _indexer.FindAll(col.PK, Query.Ascending); + tempEngine.InsertDocuments(col.CollectionName, docs.Select(x => BsonSerializer.Deserialize(_data.Read(x.DataBlock, true).Buffer)), - 1024); + 100); } // get final header from temp engine - var tempHeader = tempEngine._pager.GetPage(0); + var tempHeader = tempEngine._pager.GetPage(0, true); // copy info from initial header to final header tempHeader.ChangeID = header.ChangeID; @@ -76,8 +77,9 @@ public int Shrink() diff = (int)((header.LastPageID - tempHeader.LastPageID) * BasePage.PAGE_SIZE); } - // unlock disk to continue + // unlock disk and ckar cache to continue _disk.Unlock(); + _cache.Clear(); // delete temporary disk _disk.DeleteTempDisk(); diff --git a/LiteDB/DbEngine/Actions/Update.cs b/LiteDB/DbEngine/Actions/Update.cs index 200b9b3b1..0e2a54067 100644 --- a/LiteDB/DbEngine/Actions/Update.cs +++ b/LiteDB/DbEngine/Actions/Update.cs @@ -12,64 +12,55 @@ internal partial class DbEngine : IDisposable /// /// Implement update command to a document inside a collection /// - public int UpdateDocuments(string colName, IEnumerable docs) + public int UpdateDocuments(string colName, IEnumerable docs, int bufferSize) { - return this.Transaction(colName, false, (col) => - { - var count = 0; + return this.TransactionLoop(colName, false, bufferSize, (c) => docs, (col, doc) => { - // if no collection, no updates - if (col == null) return 0; + // normalize id before find + var id = doc["_id"].Normalize(col.PK.Options); - foreach (var doc in docs) - { - // normalize id before find - var id = doc["_id"].Normalize(col.PK.Options); - - _log.Write(Logger.COMMAND, "update document on '{0}' :: _id = ", colName, id); + _log.Write(Logger.COMMAND, "update document on '{0}' :: _id = ", colName, id); - // find indexNode from pk index - var indexNode = _indexer.Find(col.PK, id, false, Query.Ascending); + // find indexNode from pk index + var indexNode = _indexer.Find(col.PK, id, false, Query.Ascending); - // if not found document, no updates - if (indexNode == null) continue; + // if not found document, no updates + if (indexNode == null) return false; - // serialize document in bytes - var bytes = BsonSerializer.Serialize(doc); + // serialize document in bytes + var bytes = BsonSerializer.Serialize(doc); - // update data storage - var dataBlock = _data.Update(col, indexNode.DataBlock, bytes); + // update data storage + var dataBlock = _data.Update(col, indexNode.DataBlock, bytes); - // delete/insert indexes - do not touch on PK - foreach (var index in col.GetIndexes(false)) - { - var key = doc.Get(index.Field); + // delete/insert indexes - do not touch on PK + foreach (var index in col.GetIndexes(false)) + { + var key = doc.Get(index.Field); - var node = _indexer.GetNode(dataBlock.IndexRef[index.Slot]); + var node = _indexer.GetNode(dataBlock.IndexRef[index.Slot]); - // check if my index node was changed - if (node.Key.CompareTo(key) != 0) - { - // remove old index node - _indexer.Delete(index, node.Position); + // check if my index node was changed + if (node.Key.CompareTo(key) != 0) + { + // remove old index node + _indexer.Delete(index, node.Position); - // and add a new one - var newNode = _indexer.AddNode(index, key); + // and add a new one + var newNode = _indexer.AddNode(index, key); - // point my index to data object - newNode.DataBlock = dataBlock.Position; + // point my index to data object + newNode.DataBlock = dataBlock.Position; - // point my dataBlock - dataBlock.IndexRef[index.Slot] = newNode.Position; + // set my block page as dirty before change + _pager.SetDirty(dataBlock.Page); - _pager.SetDirty(dataBlock.Page); - } + // point my dataBlock + dataBlock.IndexRef[index.Slot] = newNode.Position; } - - count++; } - return count; + return true; }); } } diff --git a/LiteDB/DbEngine/DbEngine.cs b/LiteDB/DbEngine/DbEngine.cs index 6c3ddef16..e17cc9234 100644 --- a/LiteDB/DbEngine/DbEngine.cs +++ b/LiteDB/DbEngine/DbEngine.cs @@ -96,7 +96,62 @@ private T Transaction(string colName, bool addIfNotExists, Func + /// Encapsulate all transaction commands in same data structure with a loop operation with foreach item. Keep all items in same lock and commit in every N buffer size + /// + private int TransactionLoop(string colName, bool addIfNotExists, int bufferSize, Func> getItems, Func action) + { + lock (_locker) + try + { + _transaction.Begin(); + + // get collection page + var col = this.GetCollectionPage(colName, addIfNotExists); + + if (addIfNotExists == false && col == null) + { + _transaction.Commit(); + return 0; + } + + // run getItems factory + var items = getItems(col); + var enumerator = items.GetEnumerator(); + var count = 0; + + // start main loop + while (true) + { + var buffer = bufferSize; + var more = true; + + // run buffer size loop + while (buffer > 0 && (more = enumerator.MoveNext())) + { + if (action(col, enumerator.Current)) count++; + buffer--; + } + + // end items? commit! no? just save + if (more == false) + { + _transaction.Commit(); + return count; + } + + _transaction.Save(); + } + } + catch (Exception ex) + { + _log.Write(Logger.ERROR, ex.Message); _transaction.Rollback(); throw; } diff --git a/LiteDB/DbEngine/Pages/CollectionPage.cs b/LiteDB/DbEngine/Pages/CollectionPage.cs index 0f67edfe8..8828dbeb6 100644 --- a/LiteDB/DbEngine/Pages/CollectionPage.cs +++ b/LiteDB/DbEngine/Pages/CollectionPage.cs @@ -12,7 +12,7 @@ namespace LiteDB /// internal class CollectionPage : BasePage { - public const int MAX_COLLECTIONS = 256; + public const ushort MAX_COLLECTIONS = 256; public static Regex NamePattern = new Regex(@"^[\w-]{1,30}$"); diff --git a/LiteDB/DbEngine/Pages/HeaderPage.cs b/LiteDB/DbEngine/Pages/HeaderPage.cs index c1568e7de..23836e19b 100644 --- a/LiteDB/DbEngine/Pages/HeaderPage.cs +++ b/LiteDB/DbEngine/Pages/HeaderPage.cs @@ -48,6 +48,11 @@ internal class HeaderPage : BasePage /// public uint FirstCollectionPageID; + /// + /// Get/Set count of collection in database + /// + public ushort CollectionCount { get; set; } + /// /// Get/Set a user version of database file /// @@ -62,6 +67,7 @@ public HeaderPage() this.LastPageID = 0; this.ItemCount = 1; // fixed for header this.FreeBytes = 0; // no free bytes on header + this.CollectionCount = 0; this.UserVersion = 0; } @@ -84,6 +90,7 @@ protected override void ReadContent(ByteReader reader) this.FreeEmptyPageID = reader.ReadUInt32(); this.FirstCollectionPageID = reader.ReadUInt32(); this.LastPageID = reader.ReadUInt32(); + this.CollectionCount = reader.ReadUInt16(); this.UserVersion = reader.ReadInt32(); if (info != HEADER_INFO) throw LiteException.InvalidDatabase(); @@ -98,6 +105,7 @@ protected override void WriteContent(ByteWriter writer) writer.Write(this.FreeEmptyPageID); writer.Write(this.FirstCollectionPageID); writer.Write(this.LastPageID); + writer.Write(this.CollectionCount); writer.Write(this.UserVersion); } diff --git a/LiteDB/DbEngine/Services/CollectionService.cs b/LiteDB/DbEngine/Services/CollectionService.cs index 7ae8a9371..282b741eb 100644 --- a/LiteDB/DbEngine/Services/CollectionService.cs +++ b/LiteDB/DbEngine/Services/CollectionService.cs @@ -46,23 +46,25 @@ public CollectionPage Add(string name) if(string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); if(!CollectionPage.NamePattern.IsMatch(name)) throw LiteException.InvalidFormat("CollectionName", name); - var header = _pager.GetPage(0); - - // test collection limit - var pages = _pager.GetSeqPages(header.FirstCollectionPageID); + // get header marked as dirty because I will use header after (and NewPage can get another header instance) + var header = _pager.GetPage(0, true); - if (pages.Count() >= CollectionPage.MAX_COLLECTIONS) + // check limit count + if (header.CollectionCount >= CollectionPage.MAX_COLLECTIONS) { throw LiteException.CollectionLimitExceeded(CollectionPage.MAX_COLLECTIONS); } + // increese collection + header.CollectionCount++; + + // get new collection page (marked as dirty) var col = _pager.NewPage(); // add page in collection list _pager.AddOrRemoveToFreeList(true, col, header, ref header.FirstCollectionPageID); col.CollectionName = name; - _pager.SetDirty(col); // create PK index var pk = _indexer.CreateIndex(col); @@ -126,7 +128,10 @@ public void Drop(CollectionPage col) _pager.DeletePage(pageID); } - var header = _pager.GetPage(0); + // get header and mark as dirty to decrese collection counter + var header = _pager.GetPage(0, true); + + header.CollectionCount--; // remove page from collection list _pager.AddOrRemoveToFreeList(false, col, header, ref header.FirstCollectionPageID); diff --git a/LiteDB/DbEngine/Services/DataService.cs b/LiteDB/DbEngine/Services/DataService.cs index d43603e1d..543c665ea 100644 --- a/LiteDB/DbEngine/Services/DataService.cs +++ b/LiteDB/DbEngine/Services/DataService.cs @@ -20,6 +20,9 @@ public DataService(PageService pager) /// public DataBlock Insert(CollectionPage col, byte[] data) { + // set collection page as dirty before changes + _pager.SetDirty(col); + // need to extend (data is bigger than 1 page) var extend = (data.Length + DataBlock.DATA_BLOCK_FIXED_SIZE) > BasePage.PAGE_AVAILABLE_BYTES; @@ -48,15 +51,11 @@ public DataBlock Insert(CollectionPage col, byte[] data) // update freebytes + items count dataPage.UpdateItemCount(); - _pager.SetDirty(dataPage); - // add/remove dataPage on freelist if has space _pager.AddOrRemoveToFreeList(dataPage.FreeBytes > DataPage.DATA_RESERVED_BYTES, dataPage, col, ref col.FreeDataPageID); col.DocumentCount++; - _pager.SetDirty(col); - return block; } @@ -65,7 +64,8 @@ public DataBlock Insert(CollectionPage col, byte[] data) /// public DataBlock Update(CollectionPage col, PageAddress blockAddress, byte[] data) { - var dataPage = _pager.GetPage(blockAddress.PageID); + // get datapage and mark as dirty + var dataPage = _pager.GetPage(blockAddress.PageID, true); var block = dataPage.DataBlocks[blockAddress.Index]; var extend = dataPage.FreeBytes + block.Data.Length - data.Length <= 0; @@ -86,7 +86,7 @@ public DataBlock Update(CollectionPage col, PageAddress blockAddress, byte[] dat } else { - extendPage = _pager.GetPage(block.ExtendPageID); + extendPage = _pager.GetPage(block.ExtendPageID, true); } this.StoreExtendData(extendPage, data); @@ -110,8 +110,6 @@ public DataBlock Update(CollectionPage col, PageAddress blockAddress, byte[] dat // add/remove dataPage on freelist if has space AND its on/off free list _pager.AddOrRemoveToFreeList(dataPage.FreeBytes > DataPage.DATA_RESERVED_BYTES, dataPage, col, ref col.FreeDataPageID); - _pager.SetDirty(dataPage); - return block; } @@ -154,9 +152,13 @@ public byte[] ReadExtendData(uint extendPageID) /// public DataBlock Delete(CollectionPage col, PageAddress blockAddress) { - var page = _pager.GetPage(blockAddress.PageID); + // get page and mark as dirty + var page = _pager.GetPage(blockAddress.PageID, true); var block = page.DataBlocks[blockAddress.Index]; + // mark collection page as dirty + _pager.SetDirty(col); + // if there a extended page, delete all if (block.ExtendPageID != uint.MaxValue) { @@ -181,14 +183,10 @@ public DataBlock Delete(CollectionPage col, PageAddress blockAddress) { // add or remove to free list _pager.AddOrRemoveToFreeList(page.FreeBytes > DataPage.DATA_RESERVED_BYTES, page, col, ref col.FreeDataPageID); - - _pager.SetDirty(page); } col.DocumentCount--; - _pager.SetDirty(col); - return block; } @@ -211,17 +209,15 @@ public void StoreExtendData(ExtendPage page, byte[] data) // updates free bytes + items count page.UpdateItemCount(); - _pager.SetDirty(page); - bytesLeft -= bytesToCopy; offset += bytesToCopy; // if has bytes left, let's get a new page if (bytesLeft > 0) { - // if i have a continuous page, get it... or create a new one + // if i have a continuous page, get it... or create a new one (set as dirty) page = page.NextPageID != uint.MaxValue ? - _pager.GetPage(page.NextPageID) : + _pager.GetPage(page.NextPageID, true) : _pager.NewPage(page); } } diff --git a/LiteDB/DbEngine/Services/IndexService.cs b/LiteDB/DbEngine/Services/IndexService.cs index aa0461ed9..2d8ac69c3 100644 --- a/LiteDB/DbEngine/Services/IndexService.cs +++ b/LiteDB/DbEngine/Services/IndexService.cs @@ -51,6 +51,8 @@ public CollectionIndex CreateIndex(CollectionPage col) // update freebytes + item count (for head) page.UpdateItemCount(); + _pager.SetDirty(index.Page); + // add indexPage on freelist if has space _pager.AddOrRemoveToFreeList(true, page, index.Page, ref index.FreeIndexPageID); @@ -62,9 +64,6 @@ public CollectionIndex CreateIndex(CollectionPage col) index.TailNode = tail.Position; - _pager.SetDirty(index.Page); - _pager.SetDirty(page); - return index; } @@ -141,6 +140,7 @@ private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) // cur = current (imediatte before - prev) // node = new inserted node // next = next node (where cur is poiting) + _pager.SetDirty(cur.Page); node.Next[i] = cur.Next[i]; node.Prev[i] = cur.Position; @@ -150,20 +150,15 @@ private IndexNode AddNode(CollectionIndex index, BsonValue key, byte level) if (next != null) { - next.Prev[i] = node.Position; - _pager.SetDirty(next.Page); + next.Prev[i] = node.Position; } - - _pager.SetDirty(cur.Page); } } // add/remove indexPage on freelist if has space _pager.AddOrRemoveToFreeList(page.FreeBytes > IndexPage.INDEX_RESERVED_BYTES, page, index.Page, ref index.FreeIndexPageID); - _pager.SetDirty(page); - return node; } @@ -175,6 +170,9 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) var node = this.GetNode(nodeAddress); var page = node.Page; + // mark page as dirty + _pager.SetDirty(page); + for (int i = node.Prev.Length - 1; i >= 0; i--) { // get previus and next nodes (between my deleted node) @@ -183,13 +181,13 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) if (prev != null) { - prev.Next[i] = node.Next[i]; _pager.SetDirty(prev.Page); + prev.Next[i] = node.Next[i]; } if (next != null) { - next.Prev[i] = node.Prev[i]; _pager.SetDirty(next.Page); + next.Prev[i] = node.Prev[i]; } } @@ -210,8 +208,6 @@ public void Delete(CollectionIndex index, PageAddress nodeAddress) { // add or remove page from free list _pager.AddOrRemoveToFreeList(page.FreeBytes > IndexPage.INDEX_RESERVED_BYTES, node.Page, index.Page, ref index.FreeIndexPageID); - - _pager.SetDirty(page); } } diff --git a/LiteDB/DbEngine/Services/PageService.cs b/LiteDB/DbEngine/Services/PageService.cs index bb2252dd4..19529fcfe 100644 --- a/LiteDB/DbEngine/Services/PageService.cs +++ b/LiteDB/DbEngine/Services/PageService.cs @@ -21,7 +21,7 @@ public PageService(IDiskService disk, CacheService cache) /// /// Get a page from cache or from disk (and put on cache) /// - public T GetPage(uint pageID) + public T GetPage(uint pageID, bool setDirty = false) where T : BasePage { var page = _cache.GetPage(pageID); @@ -41,12 +41,17 @@ public T GetPage(uint pageID) throw new SystemException("Pager.GetPage() never shuld happend"); } #endif + // set page as dirty if passing by param + if(setDirty) + { + this.SetDirty((T)page); + } return (T)page; } /// - /// Add a page to cache and mark them as dirty too. + /// Add a page to cache and mark them as dirty too. Must be marked as dirty BEFORE change page or just after create one /// public void SetDirty(BasePage page) { @@ -73,15 +78,17 @@ public IEnumerable GetSeqPages(uint firstPageID) /// /// Get a new empty page - can be a reused page (EmptyPage) or a clean one (extend datafile) + /// Set as Dirty /// public T NewPage(BasePage prevPage = null) where T : BasePage { - var header = this.GetPage(0); + // get header and mark as dirty + var header = this.GetPage(0, true); var pageID = (uint)0; // try get page from Empty free list - if(header.FreeEmptyPageID != uint.MaxValue) + if (header.FreeEmptyPageID != uint.MaxValue) { var free = this.GetPage(header.FreeEmptyPageID); @@ -97,18 +104,17 @@ public T NewPage(BasePage prevPage = null) var page = BasePage.CreateInstance(pageID); + // mark new page as dirty as soon as possible + this.SetDirty(page); + // if there a page before, just fix NextPageID pointer if (prevPage != null) { + this.SetDirty(prevPage); page.PrevPageID = prevPage.PageID; prevPage.NextPageID = page.PageID; - this.SetDirty(prevPage); } - // mark header and this new page as dirty, and then add to cache - this.SetDirty(page); - this.SetDirty(header); - return page; } @@ -116,6 +122,7 @@ public T NewPage(BasePage prevPage = null) /// Delete an page using pageID - transform them in Empty Page and add to EmptyPageList /// If you delete a page, you can using same old instance of page - page will be converter to EmptyPage /// If need deleted page, use GetPage again + /// [Set as Dirty] /// public void DeletePage(uint pageID, bool addSequence = false) { @@ -141,6 +148,7 @@ public void DeletePage(uint pageID, bool addSequence = false) /// /// Returns a page that contains space enouth to data to insert new object - if not exits, create a new Page + /// [Set as Dirty] /// public T GetFreePage(uint startPageID, int size) where T : BasePage @@ -148,7 +156,8 @@ public T GetFreePage(uint startPageID, int size) if(startPageID != uint.MaxValue) { // get the first page - var page = this.GetPage(startPageID); + //var page = this.GetPage(startPageID); + var page = this.GetPage(startPageID); // check if there space in this page var free = page.FreeBytes; @@ -156,7 +165,9 @@ public T GetFreePage(uint startPageID, int size) // first, test if there is space on this page if (free >= size) { - return this.GetPage(startPageID); + //return this.GetPage(startPageID); + this.SetDirty(page); + return page; } } @@ -168,6 +179,7 @@ public T GetFreePage(uint startPageID, int size) /// /// Add or Remove a page in a sequence + /// [CAN set "page" and "startPage" as Dirty] /// /// Indicate that will add or remove from FreeList /// Page to add or remove from FreeList @@ -207,6 +219,9 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage var nextPageID = fieldPageID; BasePage next = null; + // base must be marked as dirty + this.SetDirty(page); + // let's page in desc order while (nextPageID != uint.MaxValue) { @@ -214,30 +229,29 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage if (free >= next.FreeBytes) { + // mark next page as dirty + this.SetDirty(next); + // assume my page in place of next page page.PrevPageID = next.PrevPageID; page.NextPageID = next.PageID; // link next page to my page next.PrevPageID = page.PageID; - this.SetDirty(next); // my page is the new first page on list if (page.PrevPageID == 0) { - fieldPageID = page.PageID; this.SetDirty(startPage); + fieldPageID = page.PageID; } else { - // if not the first, ajust links from previous page - var prev = this.GetPage(page.PrevPageID); + // if not the first, ajust links from previous page (set as dirty) + var prev = this.GetPage(page.PrevPageID, true); prev.NextPageID = page.PageID; - this.SetDirty(prev); } - this.SetDirty(page); - return; // job done - exit } @@ -247,20 +261,20 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage // empty list, be the first if (next == null) { + this.SetDirty(startPage); + // it's first page on list page.PrevPageID = 0; fieldPageID = page.PageID; - this.SetDirty(startPage); } else { + this.SetDirty(next); + // it's last position on list (next = last page on list) page.PrevPageID = next.PageID; next.NextPageID = page.PageID; - this.SetDirty(next); } - - this.SetDirty(page); } /// @@ -268,30 +282,30 @@ private void AddToFreeList(BasePage page, BasePage startPage, ref uint fieldPage /// private void RemoveToFreeList(BasePage page, BasePage startPage, ref uint fieldPageID) { + // mark page that will be removed as dirty + this.SetDirty(page); + // this page is the first of list if (page.PrevPageID == 0) { - fieldPageID = page.NextPageID; this.SetDirty(startPage); + fieldPageID = page.NextPageID; } else { - // if not the first, get previous page to remove NextPageId - var prevPage = this.GetPage(page.PrevPageID); + // if not the first, get previous page to remove NextPageId (set as dirty) + var prevPage = this.GetPage(page.PrevPageID, true); prevPage.NextPageID = page.NextPageID; - this.SetDirty(prevPage); } - // if my page is not the last on sequence, ajust the last page + // if my page is not the last on sequence, ajust the last page (set as dirty) if (page.NextPageID != uint.MaxValue) { - var nextPage = this.GetPage(page.NextPageID); + var nextPage = this.GetPage(page.NextPageID, true); nextPage.PrevPageID = page.PrevPageID; - this.SetDirty(nextPage); } page.PrevPageID = page.NextPageID = uint.MaxValue; - this.SetDirty(page); } /// diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index f75641e28..937ae3903 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -44,16 +44,28 @@ public void Commit() { if (_trans == false) throw new SystemException("No begin transaction"); + // save dirty pages + this.Save(); + + // unlock datafile + _disk.Unlock(); + + _trans = false; + } + + /// + /// Save all dirty pages to disk - do not touch on lock disk + /// + public void Save() + { if (_cache.HasDirtyPages) { - var header = _pager.GetPage(0); + // get header and mark as dirty + var header = _pager.GetPage(0, true); // increase file changeID (back to 0 when overflow) header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); - // add header page as dirty to cache - _pager.SetDirty(header); - // commit journal file - it will be used if write operation fails _disk.CommitJournal((header.LastPageID + 1) * BasePage.PAGE_SIZE); @@ -69,11 +81,6 @@ public void Commit() // set all dirty pages as clear on cache _cache.Clear(); } - - // unlock datafile - _disk.Unlock(); - - _trans = false; } public void Rollback() diff --git a/LiteDB/FileStorage/LiteFileStorage.cs b/LiteDB/FileStorage/LiteFileStorage.cs index 163d3b496..02c28ef76 100644 --- a/LiteDB/FileStorage/LiteFileStorage.cs +++ b/LiteDB/FileStorage/LiteFileStorage.cs @@ -13,6 +13,7 @@ public partial class LiteFileStorage { internal const string FILES = "_files"; internal const string CHUNKS = "_chunks"; + internal const int BUFFER_SIZE = 1024; // BsonDocument.MAX_DOCUMENT_SIZE / BasePage.PAGE_AVAILABLE_BYTES; private DbEngine _engine; @@ -39,11 +40,11 @@ public LiteFileInfo Upload(LiteFileInfo file, Stream stream) // for each chunk, insert as a chunk document foreach (var chunk in file.CreateChunks(stream)) { - _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }, 1024); + _engine.InsertDocuments(CHUNKS, new BsonDocument[] { chunk }, BUFFER_SIZE); } // update fileLength/chunks to confirm full file length stored in disk - _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }); + _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }, 1); return file; } @@ -80,7 +81,7 @@ public bool SetMetadata(string id, BsonDocument metadata) var file = this.FindById(id); if (file == null) return false; file.Metadata = metadata; - _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }); + _engine.UpdateDocuments(FILES, new BsonDocument[] { file.AsDocument }, 1); return true; } @@ -189,7 +190,7 @@ public bool Delete(string id) if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id"); // remove file reference in _files - var d = _engine.DeleteDocuments(FILES, Query.EQ("_id", id)); + var d = _engine.DeleteDocuments(FILES, Query.EQ("_id", id), 1); // if not found, just return false if (d == 0) return false; @@ -198,7 +199,7 @@ public bool Delete(string id) while (true) { - var del = _engine.DeleteDocuments(CHUNKS, Query.EQ("_id", LiteFileInfo.GetChunckId(id, index++))); + var del = _engine.DeleteDocuments(CHUNKS, Query.EQ("_id", LiteFileInfo.GetChunckId(id, index++)), BUFFER_SIZE); if (del == 0) break; } diff --git a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs index 46198488b..e2f6ca61a 100644 --- a/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs +++ b/LiteDB/Serializer/Mapper/BsonMapper.Serialize.cs @@ -28,7 +28,7 @@ public BsonDocument ToDocument(object entity) /// /// Create a instance of a object convered in BsonValue object. /// - public BsonValue Serialize(object obj) + internal BsonValue Serialize(object obj) { if (obj == null) return BsonValue.Null; From 65d931a30aef4261b83ca4294d8429a724770df3 Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 19 Nov 2015 22:06:23 -0200 Subject: [PATCH 87/91] Remove commit journal - change to SetLength --- LiteDB/DbEngine/Actions/Shrink.cs | 2 +- LiteDB/DbEngine/Disks/FileDiskService.cs | 65 ++++++------------- LiteDB/DbEngine/Disks/IDiskService.cs | 2 +- LiteDB/DbEngine/Disks/StreamDiskService.cs | 11 ++-- .../DbEngine/Services/TransactionService.cs | 4 +- 5 files changed, 32 insertions(+), 52 deletions(-) diff --git a/LiteDB/DbEngine/Actions/Shrink.cs b/LiteDB/DbEngine/Actions/Shrink.cs index 77449296d..9b0dc1ce7 100644 --- a/LiteDB/DbEngine/Actions/Shrink.cs +++ b/LiteDB/DbEngine/Actions/Shrink.cs @@ -62,7 +62,7 @@ public int Shrink() } // commit journal + shrink data file - _disk.CommitJournal((tempHeader.LastPageID + 1) * BasePage.PAGE_SIZE); + _disk.SetLength((tempHeader.LastPageID + 1) * BasePage.PAGE_SIZE); // lets re-write all pages copying from new database for (uint pageID = 0; pageID <= tempHeader.LastPageID; pageID++) diff --git a/LiteDB/DbEngine/Disks/FileDiskService.cs b/LiteDB/DbEngine/Disks/FileDiskService.cs index a94becd4e..452c7efdb 100644 --- a/LiteDB/DbEngine/Disks/FileDiskService.cs +++ b/LiteDB/DbEngine/Disks/FileDiskService.cs @@ -10,11 +10,6 @@ namespace LiteDB { internal class FileDiskService : IDiskService { - /// - /// Position on disk to write a mark to know when journal is finish and valid (byte 19 is free header area) - /// - private const int JOURNAL_FINISH_POSITION = 19; - /// /// Position, on page, about page type /// @@ -185,6 +180,18 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } + /// + /// Set datafile length + /// + public void SetLength(long fileSize) + { + // checks if new fileSize will exceed limit size + if (_limitSize > 0 && fileSize > _limitSize) throw LiteException.FileSizeExceeds(_limitSize); + + // fileSize parameter tell me final size of data file - helpful to extend first datafile + _stream.SetLength(fileSize); + } + #endregion #region Journal file @@ -201,7 +208,11 @@ public void WriteJournal(uint pageID, byte[] buffer) { _log.Write(Logger.JOURNAL, "create journal file"); - _journal = new FileStream(_journalFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None, BasePage.PAGE_SIZE); + _journal = new FileStream(_journalFilename, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + BasePage.PAGE_SIZE); }); } @@ -211,29 +222,6 @@ public void WriteJournal(uint pageID, byte[] buffer) _journal.Write(buffer, 0, BasePage.PAGE_SIZE); } - public void CommitJournal(long fileSize) - { - // checks if new fileSize will exceed limit size - if(_limitSize > 0 && fileSize > _limitSize) throw LiteException.FileSizeExceeds(_limitSize); - - if (_journalEnabled == false) return; - - if(_journal != null) - { - _log.Write(Logger.JOURNAL, "commit journal file"); - - // write a mark (byte 1) to know when journal is finish - // after that, if found a non-exclusive-open journal file, must be recovery - _journal.WriteByte(JOURNAL_FINISH_POSITION, 1); - - // flush all journal file data to disk - _journal.Flush(); - } - - // fileSize parameter tell me final size of data file - helpful to extend first datafile - _stream.SetLength(fileSize); - } - public void DeleteJournal() { if (_journalEnabled == false) return; @@ -270,21 +258,13 @@ private void TryRecovery() { _log.Write(Logger.RECOVERY, "journal file detected"); - var finish = journal.ReadByte(JOURNAL_FINISH_POSITION); - - // test if journal was finish - if(finish == 1) - { - this.Recovery(journal); - } - else - { - _log.Write(Logger.RECOVERY, "journal file are not commited, no recovery"); - } + // copy journal pages to datafile + this.Recovery(journal); // close stream for delete file journal.Close(); + // delete journal - datafile finish File.Delete(_journalFilename); _log.Write(Logger.RECOVERY, "recovery finish"); @@ -360,10 +340,7 @@ private void TryExec(Action action) { try { - lock(_stream) - { - action(); - } + action(); return; } catch (UnauthorizedAccessException) diff --git a/LiteDB/DbEngine/Disks/IDiskService.cs b/LiteDB/DbEngine/Disks/IDiskService.cs index 65a1a33c9..9f29684a8 100644 --- a/LiteDB/DbEngine/Disks/IDiskService.cs +++ b/LiteDB/DbEngine/Disks/IDiskService.cs @@ -16,11 +16,11 @@ public interface IDiskService : IDisposable void Unlock(); void WriteJournal(uint pageID, byte[] original); - void CommitJournal(long fileSize); void DeleteJournal(); byte[] ReadPage(uint pageID); void WritePage(uint pageID, byte[] buffer); + void SetLength(long fileSize); ushort GetChangeID(); diff --git a/LiteDB/DbEngine/Disks/StreamDiskService.cs b/LiteDB/DbEngine/Disks/StreamDiskService.cs index 781bec056..da6d778ec 100644 --- a/LiteDB/DbEngine/Disks/StreamDiskService.cs +++ b/LiteDB/DbEngine/Disks/StreamDiskService.cs @@ -95,6 +95,13 @@ public void WritePage(uint pageID, byte[] buffer) _stream.Write(buffer, 0, BasePage.PAGE_SIZE); } + /// + /// Set datafile length (not used) + /// + public void SetLength(long fileSize) + { + } + #endregion #region Journal file @@ -103,10 +110,6 @@ public void WriteJournal(uint pageID, byte[] data) { } - public void CommitJournal(long fileSize) - { - } - public void DeleteJournal() { } diff --git a/LiteDB/DbEngine/Services/TransactionService.cs b/LiteDB/DbEngine/Services/TransactionService.cs index 937ae3903..7e7fd8bab 100644 --- a/LiteDB/DbEngine/Services/TransactionService.cs +++ b/LiteDB/DbEngine/Services/TransactionService.cs @@ -66,8 +66,8 @@ public void Save() // increase file changeID (back to 0 when overflow) header.ChangeID = header.ChangeID == ushort.MaxValue ? (ushort)0 : (ushort)(header.ChangeID + (ushort)1); - // commit journal file - it will be used if write operation fails - _disk.CommitJournal((header.LastPageID + 1) * BasePage.PAGE_SIZE); + // set final datafile length (optimize page writes) + _disk.SetLength((header.LastPageID + 1) * BasePage.PAGE_SIZE); // write all dirty pages in data file foreach (var page in _cache.GetDirtyPages()) From 4ab5153ddfe073fc75288ed7388b744da68fc18d Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 19 Nov 2015 22:34:39 -0200 Subject: [PATCH 88/91] Ajust header size in page for future uses --- LiteDB/DbEngine/Pages/BasePage.cs | 10 +++++----- LiteDB/DbEngine/Pages/HeaderPage.cs | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/LiteDB/DbEngine/Pages/BasePage.cs b/LiteDB/DbEngine/Pages/BasePage.cs index d3df9cb2b..2eddac88a 100644 --- a/LiteDB/DbEngine/Pages/BasePage.cs +++ b/LiteDB/DbEngine/Pages/BasePage.cs @@ -18,12 +18,12 @@ internal abstract class BasePage public const int PAGE_SIZE = 4096; /// - /// This size is used bytes in header pages 17 bytes (+3 free) = 20 bytes + /// This size is used bytes in header pages 17 bytes (+8 reserved to future use) = 25 bytes /// - public const int PAGE_HEADER_SIZE = 20; + public const int PAGE_HEADER_SIZE = 25; /// - /// Bytes avaiable to store data removing page header size - 4076 bytes + /// Bytes avaiable to store data removing page header size - 4071 bytes /// public const int PAGE_AVAILABLE_BYTES = PAGE_SIZE - PAGE_HEADER_SIZE; @@ -175,7 +175,7 @@ private void ReadHeader(ByteReader reader) this.NextPageID = reader.ReadUInt32(); this.ItemCount = reader.ReadUInt16(); this.FreeBytes = reader.ReadUInt16(); - reader.ReadBytes(3); // reserved 3 bytes + reader.ReadBytes(8); // reserved 8 bytes } private void WriteHeader(ByteWriter writer) @@ -187,7 +187,7 @@ private void WriteHeader(ByteWriter writer) writer.Write(this.NextPageID); writer.Write((UInt16)this.ItemCount); writer.Write((UInt16)this.FreeBytes); - writer.Write(new byte[3]); // reserved 3 bytes + writer.Write(new byte[8]); // reserved 8 bytes } protected abstract void ReadContent(ByteReader reader); diff --git a/LiteDB/DbEngine/Pages/HeaderPage.cs b/LiteDB/DbEngine/Pages/HeaderPage.cs index 23836e19b..a2b9cfa80 100644 --- a/LiteDB/DbEngine/Pages/HeaderPage.cs +++ b/LiteDB/DbEngine/Pages/HeaderPage.cs @@ -16,7 +16,9 @@ internal class HeaderPage : BasePage /// /// ChangeID in file position (can be calc?) /// - public const int CHANGE_ID_POSITION = 52; + public const int CHANGE_ID_POSITION = PAGE_HEADER_SIZE + + 27 // HEADER_INFO + + 1; // FILE_VERSION /// /// Header info the validate that datafile is a LiteDB file (27 bytes) @@ -84,7 +86,7 @@ public override void UpdateItemCount() protected override void ReadContent(ByteReader reader) { - var info = reader.ReadString(); + var info = reader.ReadString(HEADER_INFO.Length); var ver = reader.ReadByte(); this.ChangeID = reader.ReadUInt16(); this.FreeEmptyPageID = reader.ReadUInt32(); @@ -99,7 +101,7 @@ protected override void ReadContent(ByteReader reader) protected override void WriteContent(ByteWriter writer) { - writer.Write(HEADER_INFO); + writer.Write(HEADER_INFO, HEADER_INFO.Length); writer.Write(FILE_VERSION); writer.Write(this.ChangeID); writer.Write(this.FreeEmptyPageID); From adc772e5c4743f05b0466a92a1f14c6d61c62cfe Mon Sep 17 00:00:00 2001 From: mbdavid Date: Thu, 19 Nov 2015 23:18:26 -0200 Subject: [PATCH 89/91] Formula mapping first version (has bugs) --- LiteDB.Tests/Mapping/MapperTest.cs | 24 ++++++++++++-- LiteDB/Serializer/Mapper/EntityBuilder.cs | 40 +++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/LiteDB.Tests/Mapping/MapperTest.cs b/LiteDB.Tests/Mapping/MapperTest.cs index be4158af8..0cd7f6b7f 100644 --- a/LiteDB.Tests/Mapping/MapperTest.cs +++ b/LiteDB.Tests/Mapping/MapperTest.cs @@ -187,7 +187,7 @@ public void Mapper_Test() [TestMethod] public void MapperEntityBuilder_Test() { - var mapper = new BsonMapper(); + var mapper = BsonMapper.Global; var obj = new MyFluentEntity { MyPrimaryPk = 1, CustomName = "John", DoNotIncludeMe = true, MyDateIndexed = DateTime.Now }; @@ -195,18 +195,38 @@ public void MapperEntityBuilder_Test() .Key(x => x.MyPrimaryPk) .Map(x => x.CustomName, "custom_field_name") .Ignore(x => x.DoNotIncludeMe) - .Index(x => x.MyDateIndexed, true); + .Index(x => x.MyDateIndexed, true) + .Formula("cust_len", (c) => c.CustomName.Length) + .Index("cust_len"); var doc = mapper.ToDocument(obj); var json = JsonSerializer.Serialize(doc, true); + // test formula index + Assert.AreEqual(4, doc["cust_len"].AsInt32); + var nobj = mapper.ToObject(doc); // compare object to document Assert.AreEqual(doc["_id"].AsInt32, obj.MyPrimaryPk); Assert.AreEqual(doc["custom_field_name"].AsString, obj.CustomName); Assert.AreNotEqual(doc["DoNotIncludeMe"].AsBoolean, obj.DoNotIncludeMe); + + // test index on formula + using (var db = new LiteDatabase(new MemoryStream())) + { + var col = db.GetCollection("col1"); + + // TODO: works only if index are created before insert data + // if you EnsureIndex with data in collection, items are not indexed + col.EnsureIndex("cust_len"); + col.Insert(nobj); + + var q = col.FindOne(Query.EQ("cust_len", 4)); + + Assert.AreEqual("John", q.CustomName); + } } } } diff --git a/LiteDB/Serializer/Mapper/EntityBuilder.cs b/LiteDB/Serializer/Mapper/EntityBuilder.cs index c230bab5c..609e16292 100644 --- a/LiteDB/Serializer/Mapper/EntityBuilder.cs +++ b/LiteDB/Serializer/Mapper/EntityBuilder.cs @@ -60,7 +60,7 @@ public EntityBuilder Key(Expression> property, bool autoId = tr } /// - /// Define an index based in a field on entity + /// Define an index based in a property on entity /// public EntityBuilder Index(Expression> property, bool unique = false) { @@ -71,7 +71,7 @@ public EntityBuilder Index(Expression> property, bool unique = } /// - /// Define an index based in a field on entity + /// Define an index based in a property on entity /// public EntityBuilder Index(Expression> property, IndexOptions options) { @@ -81,6 +81,42 @@ public EntityBuilder Index(Expression> property, IndexOptions o }); } + /// + /// Define an index based in a field name on BsonDocument + /// + public EntityBuilder Index(string field, bool unique = false) + { + return this.Index(field, new IndexOptions { Unique = unique }); + } + + /// + /// Define an index based in a field name on BsonDocument + /// + public EntityBuilder Index(string field, IndexOptions options) + { + var p = _prop.Values.FirstOrDefault(x => x.FieldName == field); + + if(p == null) throw new ArgumentException("field not found"); + + p.IndexOptions = options; + + return this; + } + + public EntityBuilder Formula(string name, Func getter) + { + // add a new property using a custom getter function + var p = new PropertyMapper(); + p.FieldName = name; + p.Getter = new GenericGetter((obj) => getter((T)obj)); + p.PropertyType = typeof(BsonValue); + p.PropertyName = "__" + name; + p.Setter = null; // readonly field + + _prop[p.PropertyName] = p; + return this; + } + #region DbRef /// From c0e69a50aca80c18f81d088950f6ffe2e7f8f10c Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 20 Nov 2015 15:57:34 -0200 Subject: [PATCH 90/91] Fix auto index find --- LiteDB.Tests/Mapping/MapperTest.cs | 5 ++--- LiteDB/Core/Collections/Find.cs | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/LiteDB.Tests/Mapping/MapperTest.cs b/LiteDB.Tests/Mapping/MapperTest.cs index 0cd7f6b7f..2233547b3 100644 --- a/LiteDB.Tests/Mapping/MapperTest.cs +++ b/LiteDB.Tests/Mapping/MapperTest.cs @@ -218,11 +218,10 @@ public void MapperEntityBuilder_Test() { var col = db.GetCollection("col1"); - // TODO: works only if index are created before insert data - // if you EnsureIndex with data in collection, items are not indexed - col.EnsureIndex("cust_len"); col.Insert(nobj); + col.Insert(new MyFluentEntity { MyPrimaryPk = 2, CustomName = "Xiru" }); + // auto create index "cust_len" var q = col.FindOne(Query.EQ("cust_len", 4)); Assert.AreEqual("John", q.CustomName); diff --git a/LiteDB/Core/Collections/Find.cs b/LiteDB/Core/Collections/Find.cs index 3ea3f4d95..40b9b10e7 100644 --- a/LiteDB/Core/Collections/Find.cs +++ b/LiteDB/Core/Collections/Find.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,14 +19,21 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) { if (query == null) throw new ArgumentNullException("query"); - IEnumerable docs; + IEnumerator enumerator; + var more = true; // keep trying execute query to auto-create indexes when not found + // must try get first doc inside try/catch to get index not found (yield returns are not supported inside try/catch) while (true) { try { - docs = _engine.Find(_name, query, skip, limit); + var docs = _engine.Find(_name, query, skip, limit); + + enumerator = (IEnumerator)docs.GetEnumerator(); + + more = enumerator.MoveNext(); + break; } catch (IndexNotFoundException ex) @@ -37,19 +45,23 @@ public IEnumerable Find(Query query, int skip = 0, int limit = int.MaxValue) } } - foreach (var doc in docs) + if(more == false) yield break; + + // do...while + do { // executing all includes in BsonDocument foreach (var action in _includes) { - action(doc); + action(enumerator.Current); } // get object from BsonDocument - var obj = _mapper.ToObject(doc); + var obj = _mapper.ToObject(enumerator.Current); yield return obj; } + while(more = enumerator.MoveNext()); } /// From 1934c5d8967ba3cc4cfe2f2f9cd54e8d785a814c Mon Sep 17 00:00:00 2001 From: mbdavid Date: Fri, 20 Nov 2015 21:30:32 -0200 Subject: [PATCH 91/91] Beta release --- NuGet/LiteDB.1.0.4.nupkg | Bin 85860 -> 0 bytes NuGet/LiteDB.2.0.0-beta.nupkg | Bin 0 -> 90632 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 NuGet/LiteDB.1.0.4.nupkg create mode 100644 NuGet/LiteDB.2.0.0-beta.nupkg diff --git a/NuGet/LiteDB.1.0.4.nupkg b/NuGet/LiteDB.1.0.4.nupkg deleted file mode 100644 index 49535bae97bb44eedd11dc2b88d850388279716e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85860 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 P_;({dbaHn16TyE0K$eK4 literal 0 HcmV?d00001