diff --git a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj index 5d440394e..fbd225681 100644 --- a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj +++ b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj @@ -3,7 +3,6 @@ net462;netcoreapp3.1 - diff --git a/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs b/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs index 68f7d9fb5..0740fa7ca 100644 --- a/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/BasicRepositoryFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -10,34 +11,230 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore { [TestFixture] - public class - BasicRepositoryFixture : DbTest, ScheduledTask> + public class BasicRepositoryFixture : DbTest, ScheduledTask> { - private ScheduledTask _basicType; + private List _basicList; [SetUp] public void Setup() { - _basicType = Builder - .CreateNew() - .With(c => c.Id = 0) - .With(c => c.LastExecution = DateTime.UtcNow) - .Build(); + _basicList = Builder + .CreateListOfSize(5) + .All() + .With(x => x.Id = 0) + .BuildList(); } [Test] - public void should_be_able_to_add() + public void should_be_able_to_insert() { - Subject.Insert(_basicType); + Subject.Insert(_basicList[0]); Subject.All().Should().HaveCount(1); } + [Test] + public void should_be_able_to_insert_many() + { + Subject.InsertMany(_basicList); + Subject.All().Should().HaveCount(5); + } + + [Test] + public void insert_many_should_throw_if_id_not_zero() + { + _basicList[1].Id = 999; + Assert.Throws(() => Subject.InsertMany(_basicList)); + } + + [Test] + public void should_be_able_to_get_count() + { + Subject.InsertMany(_basicList); + Subject.Count().Should().Be(_basicList.Count); + } + + [Test] + public void should_be_able_to_find_by_id() + { + Subject.InsertMany(_basicList); + var storeObject = Subject.Get(_basicList[1].Id); + + storeObject.Should().BeEquivalentTo(_basicList[1], o => o.IncludingAllRuntimeProperties()); + } + + [Test] + public void should_be_able_to_update() + { + Subject.InsertMany(_basicList); + + var item = _basicList[1]; + item.Interval = 999; + + Subject.Update(item); + + Subject.All().Should().BeEquivalentTo(_basicList); + } + + [Test] + public void should_be_able_to_upsert_new() + { + Subject.Upsert(_basicList[0]); + Subject.All().Should().HaveCount(1); + } + + [Test] + public void should_be_able_to_upsert_existing() + { + Subject.InsertMany(_basicList); + + var item = _basicList[1]; + item.Interval = 999; + + Subject.Upsert(item); + + Subject.All().Should().BeEquivalentTo(_basicList); + } + + [Test] + public void should_be_able_to_update_single_field() + { + Subject.InsertMany(_basicList); + + var item = _basicList[1]; + var executionBackup = item.LastExecution; + item.Interval = 999; + item.LastExecution = DateTime.UtcNow; + + Subject.SetFields(item, x => x.Interval); + + item.LastExecution = executionBackup; + Subject.All().Should().BeEquivalentTo(_basicList); + } + + [Test] + public void set_fields_should_throw_if_id_zero() + { + Subject.InsertMany(_basicList); + _basicList[1].Id = 0; + _basicList[1].LastExecution = DateTime.UtcNow; + + Assert.Throws(() => Subject.SetFields(_basicList[1], x => x.Interval)); + } + + [Test] + public void should_be_able_to_delete_model_by_id() + { + Subject.InsertMany(_basicList); + Subject.All().Should().HaveCount(5); + + Subject.Delete(_basicList[0].Id); + Subject.All().Select(x => x.Id).Should().BeEquivalentTo(_basicList.Skip(1).Select(x => x.Id)); + } + + [Test] + public void should_be_able_to_delete_model_by_object() + { + Subject.InsertMany(_basicList); + Subject.All().Should().HaveCount(5); + + Subject.Delete(_basicList[0]); + Subject.All().Select(x => x.Id).Should().BeEquivalentTo(_basicList.Skip(1).Select(x => x.Id)); + } + + [Test] + public void get_many_should_return_empty_list_if_no_ids() + { + Subject.Get(new List()).Should().BeEquivalentTo(new List()); + } + + [Test] + public void get_many_should_throw_if_not_all_found() + { + Subject.InsertMany(_basicList); + Assert.Throws(() => Subject.Get(new[] { 999 })); + } + + [Test] + public void should_be_able_to_find_by_multiple_id() + { + Subject.InsertMany(_basicList); + var storeObject = Subject.Get(_basicList.Take(2).Select(x => x.Id)); + storeObject.Select(x => x.Id).Should().BeEquivalentTo(_basicList.Take(2).Select(x => x.Id)); + } + + [Test] + public void should_be_able_to_update_many() + { + Subject.InsertMany(_basicList); + _basicList.ForEach(x => x.Interval = 999); + + Subject.UpdateMany(_basicList); + Subject.All().Should().BeEquivalentTo(_basicList); + } + + [Test] + public void update_many_should_throw_if_id_zero() + { + Subject.InsertMany(_basicList); + _basicList[1].Id = 0; + Assert.Throws(() => Subject.UpdateMany(_basicList)); + } + + [Test] + public void should_be_able_to_update_many_single_field() + { + Subject.InsertMany(_basicList); + + var executionBackup = _basicList.Select(x => x.LastExecution).ToList(); + _basicList.ForEach(x => x.Interval = 999); + _basicList.ForEach(x => x.LastExecution = DateTime.UtcNow); + + Subject.SetFields(_basicList, x => x.Interval); + + for (int i = 0; i < _basicList.Count; i++) + { + _basicList[i].LastExecution = executionBackup[i]; + } + + Subject.All().Should().BeEquivalentTo(_basicList); + } + + [Test] + public void set_fields_should_throw_if_any_id_zero() + { + Subject.InsertMany(_basicList); + _basicList.ForEach(x => x.Interval = 999); + _basicList[1].Id = 0; + + Assert.Throws(() => Subject.SetFields(_basicList, x => x.Interval)); + } + + [Test] + public void should_be_able_to_delete_many_by_model() + { + Subject.InsertMany(_basicList); + Subject.All().Should().HaveCount(5); + + Subject.DeleteMany(_basicList.Take(2).ToList()); + Subject.All().Select(x => x.Id).Should().BeEquivalentTo(_basicList.Skip(2).Select(x => x.Id)); + } + + [Test] + public void should_be_able_to_delete_many_by_id() + { + Subject.InsertMany(_basicList); + Subject.All().Should().HaveCount(5); + + Subject.DeleteMany(_basicList.Take(2).Select(x => x.Id).ToList()); + Subject.All().Select(x => x.Id).Should().BeEquivalentTo(_basicList.Skip(2).Select(x => x.Id)); + } + [Test] public void purge_should_delete_all() { - Subject.InsertMany(Builder.CreateListOfSize(10).BuildListOfNew()); + Subject.InsertMany(_basicList); - AllStoredModels.Should().HaveCount(10); + AllStoredModels.Should().HaveCount(5); Subject.Purge(); @@ -45,29 +242,29 @@ namespace NzbDrone.Core.Test.Datastore } [Test] - public void should_be_able_to_delete_model() + public void has_items_should_return_false_with_no_items() { - Subject.Insert(_basicType); - Subject.All().Should().HaveCount(1); - - Subject.Delete(_basicType.Id); - Subject.All().Should().BeEmpty(); + Subject.HasItems().Should().BeFalse(); } [Test] - public void should_be_able_to_find_by_id() + public void has_items_should_return_true_with_items() { - Subject.Insert(_basicType); - var storeObject = Subject.Get(_basicType.Id); + Subject.InsertMany(_basicList); + Subject.HasItems().Should().BeTrue(); + } - storeObject.Should().BeEquivalentTo(_basicType, o => o.IncludingAllRuntimeProperties()); + [Test] + public void single_should_throw_on_empty() + { + Assert.Throws(() => Subject.Single()); } [Test] public void should_be_able_to_get_single() { - Subject.Insert(_basicType); - Subject.SingleOrDefault().Should().NotBeNull(); + Subject.Insert(_basicList[0]); + Subject.Single().Should().BeEquivalentTo(_basicList[0]); } [Test] @@ -89,9 +286,21 @@ namespace NzbDrone.Core.Test.Datastore } [Test] - public void should_be_able_to_call_ToList_on_empty_quariable() + public void should_be_able_to_call_ToList_on_empty_queryable() { Subject.All().ToList().Should().BeEmpty(); } + + [Test] + public void get_paged_should_work() + { + Subject.InsertMany(_basicList); + var data = Subject.GetPaged(new PagingSpec() { Page = 0, PageSize = 2, SortKey = "LastExecution", SortDirection = SortDirection.Descending }); + + data.Page.Should().Be(0); + data.PageSize.Should().Be(2); + data.TotalRecords.Should().Be(_basicList.Count); + data.Records.Should().BeEquivalentTo(_basicList.OrderByDescending(x => x.LastExecution).Take(2)); + } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs deleted file mode 100644 index 50e1b892b..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/BooleanIntConverterFixture.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using FluentAssertions; -using Marr.Data.Converters; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Converters -{ - [TestFixture] - public class BooleanIntConverterFixture : CoreTest - { - [TestCase(true, 1)] - [TestCase(false, 0)] - public void should_return_int_when_saving_boolean_to_db(bool input, int expected) - { - Subject.ToDB(input).Should().Be(expected); - } - - [Test] - public void should_return_db_null_for_null_value_when_saving_to_db() - { - Subject.ToDB(null).Should().Be(DBNull.Value); - } - - [TestCase(1, true)] - [TestCase(0, false)] - public void should_return_bool_when_getting_int_from_db(int input, bool expected) - { - var context = new ConverterContext - { - DbValue = (long)input - }; - - Subject.FromDB(context).Should().Be(expected); - } - - [Test] - public void should_return_db_null_for_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(DBNull.Value); - } - - [Test] - public void should_throw_for_non_boolean_equivalent_number_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = 2L - }; - - Assert.Throws(() => Subject.FromDB(context)); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs index 127116c33..e358b642c 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/CommandConverterFixture.cs @@ -1,10 +1,8 @@ -using System; using System.Data; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using Moq; using NUnit.Framework; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music.Commands; @@ -15,67 +13,50 @@ namespace NzbDrone.Core.Test.Datastore.Converters [TestFixture] public class CommandConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_json_string_when_saving_boolean_to_db() { var command = new RefreshArtistCommand(); - Subject.ToDB(command).Should().BeOfType(); + Subject.SetValue(_param, command); + _param.Value.Should().BeOfType(); } [Test] public void should_return_null_for_null_value_when_saving_to_db() { - Subject.ToDB(null).Should().Be(null); - } - - [Test] - public void should_return_db_null_for_db_null_value_when_saving_to_db() - { - Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value); + Subject.SetValue(_param, null); + _param.Value.Should().BeNull(); } [Test] public void should_return_command_when_getting_json_from_db() { - var dataRecordMock = new Mock(); - dataRecordMock.Setup(s => s.GetOrdinal("Name")).Returns(0); - dataRecordMock.Setup(s => s.GetString(0)).Returns("RefreshArtist"); + var data = "{\"name\": \"RefreshArtist\"}"; - var context = new ConverterContext - { - DataRecord = dataRecordMock.Object, - DbValue = new RefreshArtistCommand().ToJson() - }; - - Subject.FromDB(context).Should().BeOfType(); + Subject.Parse(data).Should().BeOfType(); } [Test] public void should_return_unknown_command_when_getting_json_from_db() { - var dataRecordMock = new Mock(); - dataRecordMock.Setup(s => s.GetOrdinal("Name")).Returns(0); - dataRecordMock.Setup(s => s.GetString(0)).Returns("MockRemovedCommand"); + var data = "{\"name\": \"EnsureMediaCovers\"}"; - var context = new ConverterContext - { - DataRecord = dataRecordMock.Object, - DbValue = new RefreshArtistCommand(2).ToJson() - }; - - Subject.FromDB(context).Should().BeOfType(); + Subject.Parse(data).Should().BeOfType(); } [Test] public void should_return_null_for_null_value_when_getting_from_db() { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(null); + Subject.Parse(null).Should().BeNull(); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/DictionaryConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/DictionaryConverterFixture.cs new file mode 100644 index 000000000..43144308e --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Converters/DictionaryConverterFixture.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Data.SQLite; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Converters +{ + [TestFixture] + public class DictionaryConverterFixture : CoreTest>> + { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + + [Test] + public void should_serialize_in_camel_case() + { + var dict = new Dictionary + { + { "Data", "Should be lowercased" }, + { "CamelCase", "Should be cameled" } + }; + + Subject.SetValue(_param, dict); + + var result = (string)_param.Value; + + result.Should().Contain("data"); + result.Should().NotContain("Data"); + + result.Should().Contain("camelCase"); + result.Should().NotContain("CamelCase"); + } + } +} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs deleted file mode 100644 index 1b5c09c7a..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/DoubleConverterFixture.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using FluentAssertions; -using Marr.Data.Converters; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Converters -{ - [TestFixture] - public class DoubleConverterFixture : CoreTest - { - [Test] - public void should_return_double_when_saving_double_to_db() - { - var input = 10.5D; - - Subject.ToDB(input).Should().Be(input); - } - - [Test] - public void should_return_null_for_null_value_when_saving_to_db() - { - Subject.ToDB(null).Should().Be(null); - } - - [Test] - public void should_return_db_null_for_db_null_value_when_saving_to_db() - { - Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value); - } - - [Test] - public void should_return_double_when_getting_double_from_db() - { - var expected = 10.5D; - - var context = new ConverterContext - { - DbValue = expected - }; - - Subject.FromDB(context).Should().Be(expected); - } - - [Test] - public void should_return_double_when_getting_string_from_db() - { - var expected = 10.5D; - - var context = new ConverterContext - { - DbValue = $"{expected}" - }; - - Subject.FromDB(context).Should().Be(expected); - } - - [Test] - public void should_return_null_for_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(DBNull.Value); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs deleted file mode 100644 index 8f9a23412..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/EnumIntConverterFixture.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Reflection; -using FluentAssertions; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Music; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Converters -{ - [TestFixture] - public class EnumIntConverterFixture : CoreTest - { - [Test] - public void should_return_int_when_saving_enum_to_db() - { - Subject.ToDB(ArtistStatusType.Continuing).Should().Be((int)ArtistStatusType.Continuing); - } - - [Test] - public void should_return_db_null_for_null_value_when_saving_to_db() - { - Subject.ToDB(null).Should().Be(DBNull.Value); - } - - [Test] - public void should_return_enum_when_getting_int_from_db() - { - var mockMemberInfo = new Mock(); - mockMemberInfo.SetupGet(s => s.DeclaringType).Returns(typeof(ArtistMetadata)); - mockMemberInfo.SetupGet(s => s.Name).Returns("Status"); - - var expected = ArtistStatusType.Continuing; - - var context = new ConverterContext - { - ColumnMap = new ColumnMap(mockMemberInfo.Object) { FieldType = typeof(ArtistStatusType) }, - DbValue = (long)expected - }; - - Subject.FromDB(context).Should().Be(expected); - } - - [Test] - public void should_return_null_for_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(null); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs index 2d4a8e530..2bf9ea3eb 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/GuidConverterFixture.cs @@ -1,6 +1,6 @@ using System; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using NUnit.Framework; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Test.Framework; @@ -10,18 +10,21 @@ namespace NzbDrone.Core.Test.Datastore.Converters [TestFixture] public class GuidConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_string_when_saving_guid_to_db() { var guid = Guid.NewGuid(); - Subject.ToDB(guid).Should().Be(guid.ToString()); - } - - [Test] - public void should_return_db_null_for_null_value_when_saving_to_db() - { - Subject.ToDB(null).Should().Be(DBNull.Value); + Subject.SetValue(_param, guid); + _param.Value.Should().Be(guid.ToString()); } [Test] @@ -29,23 +32,13 @@ namespace NzbDrone.Core.Test.Datastore.Converters { var guid = Guid.NewGuid(); - var context = new ConverterContext - { - DbValue = guid.ToString() - }; - - Subject.FromDB(context).Should().Be(guid); + Subject.Parse(guid.ToString()).Should().Be(guid); } [Test] public void should_return_empty_guid_for_db_null_value_when_getting_from_db() { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(Guid.Empty); + Subject.Parse(null).Should().Be(Guid.Empty); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs deleted file mode 100644 index db873b76c..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/Int32ConverterFixture.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using FluentAssertions; -using Marr.Data.Converters; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Converters -{ - [TestFixture] - public class Int32ConverterFixture : CoreTest - { - [Test] - public void should_return_int_when_saving_int_to_db() - { - Subject.ToDB(5).Should().Be(5); - } - - [Test] - public void should_return_int_when_getting_int_from_db() - { - var i = 5; - - var context = new ConverterContext - { - DbValue = i - }; - - Subject.FromDB(context).Should().Be(i); - } - - [Test] - public void should_return_int_when_getting_string_from_db() - { - var i = 5; - - var context = new ConverterContext - { - DbValue = i.ToString() - }; - - Subject.FromDB(context).Should().Be(i); - } - - [Test] - public void should_return_db_null_for_db_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(DBNull.Value); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs index 815430323..66688b540 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/OsPathConverterFixture.cs @@ -1,6 +1,5 @@ -using System; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.Datastore.Converters; @@ -12,38 +11,37 @@ namespace NzbDrone.Core.Test.Datastore.Converters [TestFixture] public class OsPathConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_string_when_saving_os_path_to_db() { - var path = @"C:\Test\Music".AsOsAgnostic(); + var path = @"C:\Test\TV".AsOsAgnostic(); var osPath = new OsPath(path); - Subject.ToDB(osPath).Should().Be(path); + Subject.SetValue(_param, osPath); + _param.Value.Should().Be(path); } [Test] public void should_return_os_path_when_getting_string_from_db() { - var path = @"C:\Test\Music".AsOsAgnostic(); + var path = @"C:\Test\TV".AsOsAgnostic(); var osPath = new OsPath(path); - var context = new ConverterContext - { - DbValue = path - }; - - Subject.FromDB(context).Should().Be(osPath); + Subject.Parse(path).Should().Be(osPath); } [Test] - public void should_return_db_null_for_db_null_value_when_getting_from_db() + public void should_return_empty_for_null_value_when_getting_from_db() { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(DBNull.Value); + Subject.Parse(null).IsEmpty.Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/ProviderSettingConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/ProviderSettingConverterFixture.cs index c36758be2..67a29c14a 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/ProviderSettingConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/ProviderSettingConverterFixture.cs @@ -1,6 +1,5 @@ -using System; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using NUnit.Framework; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Test.Framework; @@ -8,30 +7,29 @@ using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Test.Datastore.Converters { + [Ignore("To reinstate once dapper changes worked out")] [TestFixture] public class ProviderSettingConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_null_config_if_config_is_null() { - var result = Subject.FromDB(new ConverterContext() - { - DbValue = DBNull.Value - }); - - result.Should().Be(NullConfig.Instance); + Subject.Parse(null).Should().Be(NullConfig.Instance); } [TestCase(null)] [TestCase("")] public void should_return_null_config_if_config_is_empty(object dbValue) { - var result = Subject.FromDB(new ConverterContext() - { - DbValue = dbValue - }); - - result.Should().Be(NullConfig.Instance); + Subject.Parse(dbValue).Should().Be(NullConfig.Instance); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs index ea1a24d78..73d12d09a 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/QualityIntConverterFixture.cs @@ -1,6 +1,5 @@ -using System; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using NUnit.Framework; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Qualities; @@ -9,26 +8,30 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore.Converters { [TestFixture] - public class QualityIntConverterFixture : CoreTest + public class QualityIntConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_int_when_saving_quality_to_db() { var quality = Quality.FLAC; - Subject.ToDB(quality).Should().Be(quality.Id); + Subject.SetValue(_param, quality); + _param.Value.Should().Be(quality.Id); } [Test] public void should_return_0_when_saving_db_null_to_db() { - Subject.ToDB(DBNull.Value).Should().Be(0); - } - - [Test] - public void should_throw_when_saving_another_object_to_db() - { - Assert.Throws(() => Subject.ToDB("Not a quality")); + Subject.SetValue(_param, null); + _param.Value.Should().Be(0); } [Test] @@ -36,23 +39,13 @@ namespace NzbDrone.Core.Test.Datastore.Converters { var quality = Quality.FLAC; - var context = new ConverterContext - { - DbValue = quality.Id - }; - - Subject.FromDB(context).Should().Be(quality); + Subject.Parse(quality.Id).Should().Be(quality); } [Test] - public void should_return_db_null_for_db_null_value_when_getting_from_db() + public void should_return_unknown_for_null_value_when_getting_from_db() { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(Quality.Unknown); + Subject.Parse(null).Should().Be(Quality.Unknown); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs deleted file mode 100644 index a590bbe62..000000000 --- a/src/NzbDrone.Core.Test/Datastore/Converters/TimeSpanConverterFixture.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Globalization; -using FluentAssertions; -using Marr.Data.Converters; -using NUnit.Framework; -using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.Datastore.Converters -{ - [TestFixture] - public class TimeSpanConverterFixture : CoreTest - { - [Test] - public void should_return_string_when_saving_timespan_to_db() - { - var timeSpan = TimeSpan.FromMinutes(5); - - Subject.ToDB(timeSpan).Should().Be(timeSpan.ToString("c", CultureInfo.InvariantCulture)); - } - - [Test] - public void should_return_null_when_saving_empty_string_to_db() - { - Subject.ToDB("").Should().Be(null); - } - - [Test] - public void should_return_time_span_when_getting_time_span_from_db() - { - var timeSpan = TimeSpan.FromMinutes(5); - - var context = new ConverterContext - { - DbValue = timeSpan - }; - - Subject.FromDB(context).Should().Be(timeSpan); - } - - [Test] - public void should_return_time_span_when_getting_string_from_db() - { - var timeSpan = TimeSpan.FromMinutes(5); - - var context = new ConverterContext - { - DbValue = timeSpan.ToString("c", CultureInfo.InvariantCulture) - }; - - Subject.FromDB(context).Should().Be(timeSpan); - } - - [Test] - public void should_return_time_span_zero_for_db_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(TimeSpan.Zero); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs index 51c07a1b3..61b2697ce 100644 --- a/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Converters/UtcConverterFixture.cs @@ -1,6 +1,6 @@ using System; +using System.Data.SQLite; using FluentAssertions; -using Marr.Data.Converters; using NUnit.Framework; using NzbDrone.Core.Datastore.Converters; using NzbDrone.Core.Test.Framework; @@ -8,20 +8,23 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore.Converters { [TestFixture] - public class UtcConverterFixture : CoreTest + public class UtcConverterFixture : CoreTest { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + [Test] public void should_return_date_time_when_saving_date_time_to_db() { var dateTime = DateTime.Now; - Subject.ToDB(dateTime).Should().Be(dateTime.ToUniversalTime()); - } - - [Test] - public void should_return_db_null_when_saving_db_null_to_db() - { - Subject.ToDB(DBNull.Value).Should().Be(DBNull.Value); + Subject.SetValue(_param, dateTime); + _param.Value.Should().Be(dateTime.ToUniversalTime()); } [Test] @@ -29,23 +32,7 @@ namespace NzbDrone.Core.Test.Datastore.Converters { var dateTime = DateTime.Now.ToUniversalTime(); - var context = new ConverterContext - { - DbValue = dateTime - }; - - Subject.FromDB(context).Should().Be(dateTime); - } - - [Test] - public void should_return_db_null_for_db_null_value_when_getting_from_db() - { - var context = new ConverterContext - { - DbValue = DBNull.Value - }; - - Subject.FromDB(context).Should().Be(DBNull.Value); + Subject.Parse(dateTime).Should().Be(dateTime); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index b4dd4aefa..807057837 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Dapper; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; @@ -14,14 +15,14 @@ namespace NzbDrone.Core.Test.Datastore public void SingleOrDefault_should_return_null_on_empty_db() { Mocker.Resolve() - .GetDataMapper().Query() + .OpenConnection().Query("SELECT * FROM Artists") .SingleOrDefault(c => c.CleanName == "SomeTitle") .Should() .BeNull(); } [Test] - public void vaccume() + public void vacuum() { Mocker.Resolve().Vacuum(); } diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs similarity index 52% rename from src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs rename to src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs index 29ed98e2e..5241eefcd 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; +using Dapper; using FizzWare.NBuilder; -using Marr.Data.QGen; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -13,11 +13,13 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore { [TestFixture] - public class MarrDataLazyLoadingFixture : DbTest + public class LazyLoadingFixture : DbTest { [SetUp] public void Setup() { + SqlBuilderExtensions.LogSql = true; + var profile = new QualityProfile { Name = "Test", @@ -81,69 +83,12 @@ namespace NzbDrone.Core.Test.Datastore Db.InsertMany(tracks); } - [Test] - public void should_join_artist_when_query_for_albums() - { - var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var albums = dataMapper.Query() - .Join(JoinType.Inner, v => v.Artist, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .ToList(); - - foreach (var album in albums) - { - Assert.IsNotNull(album.Artist); - } - } - - [Test] - public void should_lazy_load_profile_if_not_joined() - { - var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .Join(JoinType.Inner, v => v.AlbumRelease, (l, r) => l.AlbumReleaseId == r.Id) - .Join(JoinType.Inner, v => v.Album, (l, r) => l.AlbumId == r.Id) - .Join(JoinType.Inner, v => v.Artist, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .ToList(); - - foreach (var track in tracks) - { - Assert.IsTrue(track.AlbumRelease.IsLoaded); - Assert.IsTrue(track.AlbumRelease.Value.Album.IsLoaded); - Assert.IsTrue(track.AlbumRelease.Value.Album.Value.Artist.IsLoaded); - Assert.IsNotNull(track.AlbumRelease.Value.Album.Value.Artist.Value); - Assert.IsFalse(track.AlbumRelease.Value.Album.Value.Artist.Value.QualityProfile.IsLoaded); - } - } - - [Test] - public void should_explicit_load_trackfile_if_joined() - { - var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .Join(JoinType.Inner, v => v.TrackFile, (l, r) => l.TrackFileId == r.Id) - .ToList(); - - foreach (var track in tracks) - { - Assert.IsFalse(track.Artist.IsLoaded); - Assert.IsTrue(track.TrackFile.IsLoaded); - } - } - [Test] public void should_lazy_load_artist_for_track() { - var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); + var db = Mocker.Resolve(); - var tracks = dataMapper.Query() - .ToList(); + var tracks = db.All(); Assert.IsNotEmpty(tracks); foreach (var track in tracks) @@ -159,10 +104,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_artist_for_trackfile() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .ToList(); + var tracks = db.Query(new SqlBuilder()).ToList(); Assert.IsNotEmpty(tracks); foreach (var track in tracks) @@ -178,10 +120,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_trackfile_if_not_joined() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .ToList(); + var tracks = db.Query(new SqlBuilder()).ToList(); foreach (var track in tracks) { @@ -195,14 +134,12 @@ namespace NzbDrone.Core.Test.Datastore public void should_explicit_load_everything_if_joined() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var files = dataMapper.Query() - .Join(JoinType.Inner, f => f.Tracks, (f, t) => f.Id == t.TrackFileId) - .Join(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id) - .Join(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId) - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .ToList(); + var files = MediaFileRepository.Query(db, + new SqlBuilder() + .Join((f, t) => f.Id == t.TrackFileId) + .Join((t, a) => t.AlbumId == a.Id) + .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .Join((a, m) => a.ArtistMetadataId == m.Id)); Assert.IsNotEmpty(files); foreach (var file in files) @@ -219,13 +156,18 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_tracks_if_not_joined_to_trackfile() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var files = dataMapper.Query() - .Join(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id) - .Join(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId) - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .ToList(); + var files = db.QueryJoined( + new SqlBuilder() + .Join((t, a) => t.AlbumId == a.Id) + .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .Join((a, m) => a.ArtistMetadataId == m.Id), + (file, album, artist, metadata) => + { + file.Album = album; + file.Artist = artist; + file.Artist.Value.Metadata = metadata; + return file; + }); Assert.IsNotEmpty(files); foreach (var file in files) @@ -244,9 +186,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_tracks_if_not_joined() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var release = dataMapper.Query().Where(x => x.Id == 1).SingleOrDefault(); + var release = db.Query(new SqlBuilder().Where(x => x.Id == 1)).SingleOrDefault(); Assert.IsFalse(release.Tracks.IsLoaded); Assert.IsNotNull(release.Tracks.Value); @@ -258,10 +198,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_track_if_not_joined() { var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .ToList(); + var tracks = db.Query(new SqlBuilder()).ToList(); foreach (var track in tracks) { @@ -270,28 +207,5 @@ namespace NzbDrone.Core.Test.Datastore Assert.IsTrue(track.Tracks.IsLoaded); } } - - [Test] - public void should_explicit_load_profile_if_joined() - { - var db = Mocker.Resolve(); - var dataMapper = db.GetDataMapper(); - - var tracks = dataMapper.Query() - .Join(JoinType.Inner, v => v.AlbumRelease, (l, r) => l.AlbumReleaseId == r.Id) - .Join(JoinType.Inner, v => v.Album, (l, r) => l.AlbumId == r.Id) - .Join(JoinType.Inner, v => v.Artist, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join(JoinType.Inner, v => v.QualityProfile, (l, r) => l.QualityProfileId == r.Id) - .ToList(); - - foreach (var track in tracks) - { - Assert.IsTrue(track.AlbumRelease.IsLoaded); - Assert.IsTrue(track.AlbumRelease.Value.Album.IsLoaded); - Assert.IsTrue(track.AlbumRelease.Value.Album.Value.Artist.IsLoaded); - Assert.IsNotNull(track.AlbumRelease.Value.Album.Value.Artist.Value); - Assert.IsTrue(track.AlbumRelease.Value.Album.Value.Artist.Value.QualityProfile.IsLoaded); - } - } } } diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs deleted file mode 100644 index f98a43623..000000000 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/PagingOffsetFixture.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests -{ - public class PagingOffsetFixture - { - [TestCase(1, 10, 0)] - [TestCase(2, 10, 10)] - [TestCase(3, 20, 40)] - [TestCase(1, 100, 0)] - public void should_calcuate_expected_offset(int page, int pageSize, int expected) - { - var pagingSpec = new PagingSpec - { - Page = page, - PageSize = pageSize, - SortDirection = SortDirection.Ascending, - SortKey = "ReleaseDate" - }; - - pagingSpec.PagingOffset().Should().Be(expected); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs b/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs deleted file mode 100644 index d4b312645..000000000 --- a/src/NzbDrone.Core.Test/Datastore/PagingSpecExtensionsTests/ToSortDirectionFixture.cs +++ /dev/null @@ -1,53 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extensions; -using NzbDrone.Core.Music; - -namespace NzbDrone.Core.Test.Datastore.PagingSpecExtensionsTests -{ - public class ToSortDirectionFixture - { - [Test] - public void should_convert_default_to_asc() - { - var pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortDirection = SortDirection.Default, - SortKey = "ReleaseDate" - }; - - pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Asc); - } - - [Test] - public void should_convert_ascending_to_asc() - { - var pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortDirection = SortDirection.Ascending, - SortKey = "ReleaseDate" - }; - - pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Asc); - } - - [Test] - public void should_convert_descending_to_desc() - { - var pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortDirection = SortDirection.Descending, - SortKey = "ReleaseDate" - }; - - pagingSpec.ToSortDirection().Should().Be(Marr.Data.QGen.SortDirection.Desc); - } - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/ReflectionStrategyFixture/Benchmarks.cs b/src/NzbDrone.Core.Test/Datastore/ReflectionStrategyFixture/Benchmarks.cs deleted file mode 100644 index 810ae5790..000000000 --- a/src/NzbDrone.Core.Test/Datastore/ReflectionStrategyFixture/Benchmarks.cs +++ /dev/null @@ -1,39 +0,0 @@ -using NUnit.Framework; - -namespace NzbDrone.Core.Test.Datastore.ReflectionStrategyFixture -{ - [TestFixture] - public class Benchmarks - { - /* private const int iterations = 5000000; - private object _target; - private IReflectionStrategy _simpleReflectionStrategy; - - [SetUp] - public void Setup() - { - // _simpleReflectionStrategy = new DelegateReflectionStrategy(); - } - - [Test] - public void clr_reflection_test() - { - _target = new Series(); - - var del = _simpleReflectionStrategy.BuildSetter(typeof(Series), "Title"); - - for (int i = 0; i < iterations; i++) - { - del(_target, "TestTile"); - //_simpleReflectionStrategy.SetFieldValue(_target, "Title", "TestTile"); - } - } - - - private void SetField() - { - - - }*/ - } -} diff --git a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs b/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs similarity index 70% rename from src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs rename to src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs index 6fdc962af..486394ed6 100644 --- a/src/NzbDrone.Core.Test/Datastore/MappingExtentionFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/TableMapperFixture.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; +using Dapper; using FluentAssertions; -using Marr.Data; using NUnit.Framework; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Music; namespace NzbDrone.Core.Test.Datastore { [TestFixture] - public class MappingExtensionFixture + public class TableMapperFixture { public class EmbeddedType : IEmbeddedDocument { @@ -37,9 +36,8 @@ namespace NzbDrone.Core.Test.Datastore [SetUp] public void Setup() { - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(EmbeddedType), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(int), new Int32Converter()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); } [Test] @@ -47,7 +45,7 @@ namespace NzbDrone.Core.Test.Datastore { var properties = typeof(TypeWithAllMappableProperties).GetProperties(); properties.Should().NotBeEmpty(); - properties.Should().OnlyContain(c => MappingExtensions.IsMappableProperty(c)); + properties.Should().OnlyContain(c => c.IsMappableProperty()); } [Test] @@ -55,7 +53,7 @@ namespace NzbDrone.Core.Test.Datastore { var properties = typeof(TypeWithNoMappableProperties).GetProperties(); properties.Should().NotBeEmpty(); - properties.Should().NotContain(c => MappingExtensions.IsMappableProperty(c)); + properties.Should().NotContain(c => c.IsMappableProperty()); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs b/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs new file mode 100644 index 000000000..ae0a62725 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Music; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore +{ + [TestFixture] + public class WhereBuilderFixture : CoreTest + { + private WhereBuilder _subject; + + [OneTimeSetUp] + public void MapTables() + { + // Generate table mapping + Mocker.Resolve(); + } + + private WhereBuilder Where(Expression> filter) + { + return new WhereBuilder(filter, true, 0); + } + + private WhereBuilder WhereMetadata(Expression> filter) + { + return new WhereBuilder(filter, true, 0); + } + + [Test] + public void where_equal_const() + { + _subject = Where(x => x.Id == 10); + + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(10); + } + + [Test] + public void where_equal_variable() + { + var id = 10; + _subject = Where(x => x.Id == id); + + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(id); + } + + [Test] + public void where_equal_property() + { + var artist = new Artist { Id = 10 }; + _subject = Where(x => x.Id == artist.Id); + + _subject.Parameters.ParameterNames.Should().HaveCount(1); + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(artist.Id); + } + + [Test] + public void where_equal_lazy_property() + { + _subject = Where(x => x.QualityProfile.Value.Id == 1); + + _subject.Parameters.ParameterNames.Should().HaveCount(1); + _subject.ToString().Should().Be($"(\"QualityProfiles\".\"Id\" = @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(1); + } + + [Test] + public void where_throws_without_concrete_condition_if_requiresConcreteCondition() + { + Expression> filter = (x, y) => x.Id == y.Id; + _subject = new WhereBuilder(filter, true, 0); + Assert.Throws(() => _subject.ToString()); + } + + [Test] + public void where_allows_abstract_condition_if_not_requiresConcreteCondition() + { + Expression> filter = (x, y) => x.Id == y.Id; + _subject = new WhereBuilder(filter, false, 0); + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = \"Artists\".\"Id\")"); + } + + [Test] + public void where_string_is_null() + { + _subject = Where(x => x.CleanName == null); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + } + + [Test] + public void where_string_is_null_value() + { + string imdb = null; + _subject = Where(x => x.CleanName == imdb); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + } + + [Test] + public void where_equal_null_property() + { + var artist = new Artist { CleanName = null }; + _subject = Where(x => x.CleanName == artist.CleanName); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IS NULL)"); + } + + [Test] + public void where_column_contains_string() + { + var test = "small"; + _subject = Where(x => x.CleanName.Contains(test)); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1 || '%')"); + _subject.Parameters.Get("Clause1_P1").Should().Be(test); + } + + [Test] + public void where_string_contains_column() + { + var test = "small"; + _subject = Where(x => test.Contains(x.CleanName)); + + _subject.ToString().Should().Be($"(@Clause1_P1 LIKE '%' || \"Artists\".\"CleanName\" || '%')"); + _subject.Parameters.Get("Clause1_P1").Should().Be(test); + } + + [Test] + public void where_column_starts_with_string() + { + var test = "small"; + _subject = Where(x => x.CleanName.StartsWith(test)); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE @Clause1_P1 || '%')"); + _subject.Parameters.Get("Clause1_P1").Should().Be(test); + } + + [Test] + public void where_column_ends_with_string() + { + var test = "small"; + _subject = Where(x => x.CleanName.EndsWith(test)); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(test); + } + + [Test] + public void where_in_list() + { + var list = new List { 1, 2, 3 }; + _subject = Where(x => list.Contains(x.Id)); + + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" IN (1, 2, 3))"); + } + + [Test] + public void where_in_list_2() + { + var list = new List { 1, 2, 3 }; + _subject = Where(x => x.CleanName == "test" && list.Contains(x.Id)); + + _subject.ToString().Should().Be($"((\"Artists\".\"CleanName\" = @Clause1_P1) AND (\"Artists\".\"Id\" IN (1, 2, 3)))"); + } + + [Test] + public void where_in_string_list() + { + var list = new List { "first", "second", "third" }; + + _subject = Where(x => list.Contains(x.CleanName)); + + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IN @Clause1_P1)"); + } + + [Test] + public void where_in_string_list_column() + { + _subject = WhereMetadata(x => x.OldForeignArtistIds.Contains("foreignId")); + + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"OldForeignArtistIds\" LIKE '%' || @Clause1_P1 || '%')"); + } + + [Test] + public void enum_as_int() + { + _subject = WhereMetadata(x => x.Status == ArtistStatusType.Continuing); + + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" = @Clause1_P1)"); + } + + [Test] + public void enum_in_list() + { + var allowed = new List { ArtistStatusType.Continuing, ArtistStatusType.Ended }; + _subject = WhereMetadata(x => allowed.Contains(x.Status)); + + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + } + + [Test] + public void enum_in_array() + { + var allowed = new ArtistStatusType[] { ArtistStatusType.Continuing, ArtistStatusType.Ended }; + _subject = WhereMetadata(x => allowed.Contains(x.Status)); + + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 8b277d35f..0f5254933 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; -using Marr.Data; using Moq; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.IndexerSearch.Definitions; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index 90c2cf878..f6aa5b3ff 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -1,6 +1,5 @@ using FizzWare.NBuilder; using FluentAssertions; -using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index 2fdf45413..f628c86ac 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -2,10 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; -using Marr.Data; using Moq; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index e62ca5fa3..04ff61b74 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; -using Marr.Data; using Moq; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 91cb93c2b..7875651a6 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using FizzWare.NBuilder; -using Marr.Data; using Moq; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; diff --git a/src/NzbDrone.Core.Test/Framework/DbTest.cs b/src/NzbDrone.Core.Test/Framework/DbTest.cs index f548c2c41..afb6dbdb6 100644 --- a/src/NzbDrone.Core.Test/Framework/DbTest.cs +++ b/src/NzbDrone.Core.Test/Framework/DbTest.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Data.SQLite; using System.Linq; -using Marr.Data; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; @@ -112,7 +111,7 @@ namespace NzbDrone.Core.Test.Framework Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); - MapRepository.Instance.EnableTraceLogging = true; + SqlBuilderExtensions.LogSql = true; } [SetUp] diff --git a/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs b/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs index 481d46fb1..124edb565 100644 --- a/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs +++ b/src/NzbDrone.Core.Test/Framework/DirectDataMapper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.Common; using System.Linq; using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore; @@ -13,32 +12,20 @@ namespace NzbDrone.Core.Test.Framework List> Query(string sql); List Query(string sql) where T : new(); - T QueryScalar(string sql); } public class DirectDataMapper : IDirectDataMapper { - private readonly DbProviderFactory _providerFactory; - private readonly string _connectionString; + private readonly IDatabase _database; public DirectDataMapper(IDatabase database) { - var dataMapper = database.GetDataMapper(); - _providerFactory = dataMapper.ProviderFactory; - _connectionString = dataMapper.ConnectionString; - } - - private DbConnection OpenConnection() - { - var connection = _providerFactory.CreateConnection(); - connection.ConnectionString = _connectionString; - connection.Open(); - return connection; + _database = database; } public DataTable GetDataTable(string sql) { - using (var connection = OpenConnection()) + using (var connection = _database.OpenConnection()) { using (var cmd = connection.CreateCommand()) { @@ -65,13 +52,6 @@ namespace NzbDrone.Core.Test.Framework return dataTable.Rows.Cast().Select(MapToObject).ToList(); } - public T QueryScalar(string sql) - { - var dataTable = GetDataTable(sql); - - return dataTable.Rows.Cast().Select(d => MapValue(d, 0, typeof(T))).Cast().FirstOrDefault(); - } - protected Dictionary MapToDictionary(DataRow dataRow) { var item = new Dictionary(); @@ -118,28 +98,24 @@ namespace NzbDrone.Core.Test.Framework propertyType = propertyType.GetGenericArguments()[0]; } - object value = MapValue(dataRow, i, propertyType); + object value; + if (dataRow.ItemArray[i] == DBNull.Value) + { + value = null; + } + else if (dataRow.Table.Columns[i].DataType == typeof(string) && propertyType != typeof(string)) + { + value = Json.Deserialize((string)dataRow.ItemArray[i], propertyType); + } + else + { + value = Convert.ChangeType(dataRow.ItemArray[i], propertyType); + } propertyInfo.SetValue(item, value, null); } return item; } - - private object MapValue(DataRow dataRow, int i, Type targetType) - { - if (dataRow.ItemArray[i] == DBNull.Value) - { - return null; - } - else if (dataRow.Table.Columns[i].DataType == typeof(string) && targetType != typeof(string)) - { - return Json.Deserialize((string)dataRow.ItemArray[i], targetType); - } - else - { - return Convert.ChangeType(dataRow.ItemArray[i], targetType); - } - } } } diff --git a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs index 4215a66c6..3fbfdf028 100644 --- a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs +++ b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Data; using System.Linq; using Moq; using NzbDrone.Core.Datastore; @@ -21,6 +22,7 @@ namespace NzbDrone.Core.Test.Framework void Delete(T childModel) where T : ModelBase, new(); IDirectDataMapper GetDirectDataMapper(); + IDbConnection OpenConnection(); } public class TestDatabase : ITestDatabase @@ -74,5 +76,10 @@ namespace NzbDrone.Core.Test.Framework { return new DirectDataMapper(_dbConnection); } + + public IDbConnection OpenConnection() + { + return _dbConnection.OpenConnection(); + } } } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs index cb2297989..24a49157a 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs @@ -14,7 +14,11 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_delete_unused_tags() { - var tags = Builder.CreateListOfSize(2).BuildList(); + var tags = Builder + .CreateListOfSize(2) + .All() + .With(x => x.Id = 0) + .BuildList(); Db.InsertMany(tags); Subject.Clean(); @@ -24,11 +28,17 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers [Test] public void should_not_delete_used_tags() { - var tags = Builder.CreateListOfSize(2).BuildList(); + var tags = Builder + .CreateListOfSize(2) + .All() + .With(x => x.Id = 0) + .BuildList(); + Db.InsertMany(tags); var restrictions = Builder.CreateListOfSize(2) .All() + .With(x => x.Id = 0) .With(v => v.Tags.Add(tags[0].Id)) .BuildList(); Db.InsertMany(restrictions); diff --git a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs index ba5c84236..f08b3e6b2 100644 --- a/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs +++ b/src/NzbDrone.Core.Test/Instrumentation/DatabaseTargetFixture.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using FluentAssertions; -using Marr.Data; using NLog; using NUnit.Framework; using NzbDrone.Common.Instrumentation; @@ -64,22 +63,6 @@ namespace NzbDrone.Core.Test.Instrumentation VerifyLog(StoredModel, LogLevel.Info); } - [Test] - [Explicit] - [ManualTest] - public void perf_test() - { - MapRepository.Instance.EnableTraceLogging = false; - for (int i = 0; i < 1000; i++) - { - _logger.Info(Guid.NewGuid()); - } - - Thread.Sleep(1000); - - MapRepository.Instance.EnableTraceLogging = true; - } - [Test] public void write_log_exception() { diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs index f5ff93d7a..920fec899 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs @@ -1,8 +1,8 @@ using System.Linq; using FizzWare.NBuilder; using FluentAssertions; -using Marr.Data; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Specifications; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs index 61991b465..557596f74 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs @@ -1,9 +1,9 @@ using System.Linq; using FizzWare.NBuilder; using FluentAssertions; -using Marr.Data; using NUnit.Framework; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Specifications; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index de69f06de..da23bf1f9 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -2,10 +2,10 @@ using System.IO; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; -using Marr.Data; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs index 78878fc77..c6bc5cbb9 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs @@ -29,10 +29,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests Monitored = true, ForeignArtistId = "this is a fake id", Id = 1, - Metadata = new ArtistMetadata - { - Id = 1 - } + ArtistMetadataId = 1 }; _albumRepo = Mocker.Resolve(); diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs index d26fa1ca6..0fe2cdcd7 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistMetadataRepositoryTests/ArtistMetadataRepositoryFixture.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests public void Setup() { _artistMetadataRepo = Mocker.Resolve(); - _metadataList = Builder.CreateListOfSize(10).BuildList(); + _metadataList = Builder.CreateListOfSize(10).All().With(x => x.Id = 0).BuildList(); } [Test] diff --git a/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs index b0a7819b8..a7d0dbbbf 100644 --- a/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs @@ -4,9 +4,9 @@ using System.Reflection; using AutoFixture; using Equ; using FluentAssertions; -using Marr.Data; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Music; using NzbDrone.Test.Common; diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs index 84a13448e..29ec139ba 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using Dapper; using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Music; namespace NzbDrone.Core.ArtistStats { @@ -13,6 +16,8 @@ namespace NzbDrone.Core.ArtistStats public class ArtistStatisticsRepository : IArtistStatisticsRepository { + private const string _selectTemplate = "SELECT /**select**/ FROM Tracks /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + private readonly IMainDatabase _database; public ArtistStatisticsRepository(IMainDatabase database) @@ -22,57 +27,41 @@ namespace NzbDrone.Core.ArtistStats public List ArtistStatistics() { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("currentDate", DateTime.UtcNow); - - var sb = new StringBuilder(); - sb.AppendLine(GetSelectClause()); - sb.AppendLine("AND Albums.ReleaseDate < @currentDate"); - sb.AppendLine(GetGroupByClause()); - var queryText = sb.ToString(); - - return mapper.Query(queryText); + var time = DateTime.UtcNow; + return Query(Builder().Where(x => x.ReleaseDate < time)); } public List ArtistStatistics(int artistId) { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("currentDate", DateTime.UtcNow); - mapper.AddParameter("artistId", artistId); - - var sb = new StringBuilder(); - sb.AppendLine(GetSelectClause()); - sb.AppendLine("AND Artists.Id = @artistId"); - sb.AppendLine("AND Albums.ReleaseDate < @currentDate"); - sb.AppendLine(GetGroupByClause()); - var queryText = sb.ToString(); - - return mapper.Query(queryText); + var time = DateTime.UtcNow; + return Query(Builder().Where(x => x.ReleaseDate < time) + .Where(x => x.Id == artistId)); } - private string GetSelectClause() + private List Query(SqlBuilder builder) { - return @"SELECT - Artists.Id AS ArtistId, + var sql = builder.AddTemplate(_selectTemplate).LogQuery(); + + using (var conn = _database.OpenConnection()) + { + return conn.Query(sql.RawSql, sql.Parameters).ToList(); + } + } + + private SqlBuilder Builder() => new SqlBuilder() + .Select(@"Artists.Id AS ArtistId, Albums.Id AS AlbumId, SUM(COALESCE(TrackFiles.Size, 0)) AS SizeOnDisk, COUNT(Tracks.Id) AS TotalTrackCount, SUM(CASE WHEN Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount, SUM(CASE WHEN Albums.Monitored = 1 OR Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount, - SUM(CASE WHEN TrackFiles.Id IS NULL THEN 0 ELSE 1 END) AS TrackFileCount - FROM Tracks - JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id - JOIN Albums ON AlbumReleases.AlbumId = Albums.Id - JOIN Artists on Albums.ArtistMetadataId = Artists.ArtistMetadataId - LEFT OUTER JOIN TrackFiles ON Tracks.TrackFileId = TrackFiles.Id - WHERE AlbumReleases.Monitored = 1"; - } - - private string GetGroupByClause() - { - return "GROUP BY Artists.Id, Albums.Id"; - } + SUM(CASE WHEN TrackFiles.Id IS NULL THEN 0 ELSE 1 END) AS TrackFileCount") + .Join((t, r) => t.AlbumReleaseId == r.Id) + .Join((r, a) => r.AlbumId == a.Id) + .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .LeftJoin((t, f) => t.TrackFileId == f.Id) + .Where(x => x.Monitored == true) + .GroupBy(x => x.Id) + .GroupBy(x => x.Id); } } diff --git a/src/NzbDrone.Core/Authentication/UserRepository.cs b/src/NzbDrone.Core/Authentication/UserRepository.cs index 0c6539dd7..fac37bc73 100644 --- a/src/NzbDrone.Core/Authentication/UserRepository.cs +++ b/src/NzbDrone.Core/Authentication/UserRepository.cs @@ -20,12 +20,12 @@ namespace NzbDrone.Core.Authentication public User FindUser(string username) { - return Query.Where(u => u.Username == username).SingleOrDefault(); + return Query(x => x.Username == username).SingleOrDefault(); } public User FindUser(Guid identifier) { - return Query.Where(u => u.Identifier == identifier).SingleOrDefault(); + return Query(x => x.Identifier == identifier).SingleOrDefault(); } } } diff --git a/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs b/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs index b45280c0e..b5bf80bf1 100644 --- a/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs +++ b/src/NzbDrone.Core/Backup/MakeDatabaseBackup.cs @@ -21,7 +21,12 @@ namespace NzbDrone.Core.Backup public void BackupDatabase(IDatabase database, string targetDirectory) { - var sourceConnectionString = database.GetDataMapper().ConnectionString; + var sourceConnectionString = ""; + using (var db = database.OpenConnection()) + { + sourceConnectionString = db.ConnectionString; + } + var backupConnectionStringBuilder = new SQLiteConnectionStringBuilder(sourceConnectionString); backupConnectionStringBuilder.DataSource = Path.Combine(targetDirectory, Path.GetFileName(backupConnectionStringBuilder.DataSource)); diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 369664df6..63cb0eaf2 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; @@ -22,26 +22,24 @@ namespace NzbDrone.Core.Blacklisting public List BlacklistedByTitle(int artistId, string sourceTitle) { - return Query.Where(e => e.ArtistId == artistId) - .AndWhere(e => e.SourceTitle.Contains(sourceTitle)); + return Query(e => e.ArtistId == artistId && e.SourceTitle.Contains(sourceTitle)); } public List BlacklistedByTorrentInfoHash(int artistId, string torrentInfoHash) { - return Query.Where(e => e.ArtistId == artistId) - .AndWhere(e => e.TorrentInfoHash.Contains(torrentInfoHash)); + return Query(e => e.ArtistId == artistId && e.TorrentInfoHash.Contains(torrentInfoHash)); } public List BlacklistedByArtist(int artistId) { - return Query.Where(b => b.ArtistId == artistId); + return Query(b => b.ArtistId == artistId); } - protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) - { - var baseQuery = query.Join(JoinType.Inner, h => h.Artist, (h, s) => h.ArtistId == s.Id); - - return base.GetPagedQuery(baseQuery, pagingSpec); - } + protected override SqlBuilder PagedBuilder() => new SqlBuilder().Join((b, m) => b.ArtistId == m.Id); + protected override IEnumerable PagedQuery(SqlBuilder builder) => _database.QueryJoined(builder, (bl, artist) => + { + bl.Artist = artist; + return bl; + }); } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 99d6f7d13..7119871f0 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -32,6 +32,8 @@ namespace NzbDrone.Core.Configuration bool AnalyticsEnabled { get; } string LogLevel { get; } string ConsoleLogLevel { get; } + bool LogSql { get; } + int LogRotate { get; } bool FilterSentryEvents { get; } string Branch { get; } string ApiKey { get; } @@ -177,6 +179,8 @@ namespace NzbDrone.Core.Configuration public string LogLevel => GetValue("LogLevel", "info"); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); + public bool LogSql => GetValueBoolean("LogSql", false, persist: false); + public int LogRotate => GetValueInt("LogRotate", 50, persist: false); public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); public string SslCertPath => GetValue("SslCertPath", ""); public string SslCertPassword => GetValue("SslCertPassword", ""); @@ -204,9 +208,9 @@ namespace NzbDrone.Core.Configuration public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false); - public int GetValueInt(string key, int defaultValue) + public int GetValueInt(string key, int defaultValue, bool persist = true) { - return Convert.ToInt32(GetValue(key, defaultValue)); + return Convert.ToInt32(GetValue(key, defaultValue, persist)); } public bool GetValueBoolean(string key, bool defaultValue, bool persist = true) diff --git a/src/NzbDrone.Core/Configuration/ConfigRepository.cs b/src/NzbDrone.Core/Configuration/ConfigRepository.cs index c8ae80c21..7a6721bb6 100644 --- a/src/NzbDrone.Core/Configuration/ConfigRepository.cs +++ b/src/NzbDrone.Core/Configuration/ConfigRepository.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Configuration public Config Get(string key) { - return Query.Where(c => c.Key == key).SingleOrDefault(); + return Query(c => c.Key == key).SingleOrDefault(); } public Config Upsert(string key, string value) diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index b2c88f314..3f2b02a35 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; -using Marr.Data; -using Marr.Data.QGen; +using System.Reflection; +using System.Text; +using Dapper; using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Datastore @@ -17,59 +17,79 @@ namespace NzbDrone.Core.Datastore IEnumerable All(); int Count(); TModel Get(int id); - IEnumerable Get(IEnumerable ids); - TModel SingleOrDefault(); TModel Insert(TModel model); TModel Update(TModel model); TModel Upsert(TModel model); - void Delete(int id); + void SetFields(TModel model, params Expression>[] properties); void Delete(TModel model); + void Delete(int id); + IEnumerable Get(IEnumerable ids); void InsertMany(IList model); void UpdateMany(IList model); + void SetFields(IList models, params Expression>[] properties); void DeleteMany(List model); + void DeleteMany(IEnumerable ids); void Purge(bool vacuum = false); bool HasItems(); - void DeleteMany(IEnumerable ids); - void SetFields(TModel model, params Expression>[] properties); - void SetFields(IEnumerable models, params Expression>[] properties); TModel Single(); + TModel SingleOrDefault(); PagingSpec GetPaged(PagingSpec pagingSpec); } public class BasicRepository : IBasicRepository where TModel : ModelBase, new() { - private readonly IDatabase _database; private readonly IEventAggregator _eventAggregator; + private readonly PropertyInfo _keyProperty; + private readonly List _properties; + private readonly string _updateSql; + private readonly string _insertSql; - protected IDataMapper DataMapper => _database.GetDataMapper(); + protected readonly IDatabase _database; + protected readonly string _table; public BasicRepository(IDatabase database, IEventAggregator eventAggregator) { _database = database; _eventAggregator = eventAggregator; + + var type = typeof(TModel); + + _table = TableMapping.Mapper.TableNameMapping(type); + _keyProperty = type.GetProperty(nameof(ModelBase.Id)); + + var excluded = TableMapping.Mapper.ExcludeProperties(type).Select(x => x.Name).ToList(); + excluded.Add(_keyProperty.Name); + _properties = type.GetProperties().Where(x => x.IsMappableProperty() && !excluded.Contains(x.Name)).ToList(); + + _insertSql = GetInsertSql(); + _updateSql = GetUpdateSql(_properties); } - protected virtual QueryBuilder Query => DataMapper.Query(); + protected virtual SqlBuilder Builder() => new SqlBuilder(); - protected void Delete(Expression> filter) - { - DataMapper.Delete(filter); - } + protected virtual List Query(SqlBuilder builder) => _database.Query(builder).ToList(); - public IEnumerable All() - { - return Query.ToList(); - } + protected List Query(Expression> where) => Query(Builder().Where(where)); + + protected virtual List QueryDistinct(SqlBuilder builder) => _database.QueryDistinct(builder).ToList(); public int Count() { - return DataMapper.Query().GetRowCount(); + using (var conn = _database.OpenConnection()) + { + return conn.ExecuteScalar($"SELECT COUNT(*) FROM {_table}"); + } + } + + public virtual IEnumerable All() + { + return Query(Builder()); } public TModel Get(int id) { - var model = Query.Where(c => c.Id == id).SingleOrDefault(); + var model = Query(x => x.Id == id).FirstOrDefault(); if (model == null) { @@ -81,13 +101,16 @@ namespace NzbDrone.Core.Datastore public IEnumerable Get(IEnumerable ids) { - var idList = ids.ToList(); - var query = string.Format("[t0].[Id] IN ({0})", string.Join(",", idList)); - var result = Query.Where(query).ToList(); - - if (result.Count != idList.Count()) + if (!ids.Any()) { - throw new ApplicationException($"Expected query to return {idList.Count} rows but returned {result.Count}"); + return new List(); + } + + var result = Query(x => ids.Contains(x.Id)); + + if (result.Count != ids.Count()) + { + throw new ApplicationException($"Expected query to return {ids.Count()} rows but returned {result.Count}"); } return result; @@ -110,13 +133,74 @@ namespace NzbDrone.Core.Datastore throw new InvalidOperationException("Can't insert model with existing ID " + model.Id); } - DataMapper.Insert(model); + using (var conn = _database.OpenConnection()) + { + model = Insert(conn, null, model); + } ModelCreated(model); return model; } + private string GetInsertSql() + { + var sbColumnList = new StringBuilder(null); + for (var i = 0; i < _properties.Count; i++) + { + var property = _properties[i]; + sbColumnList.AppendFormat("\"{0}\"", property.Name); + if (i < _properties.Count - 1) + { + sbColumnList.Append(", "); + } + } + + var sbParameterList = new StringBuilder(null); + for (var i = 0; i < _properties.Count; i++) + { + var property = _properties[i]; + sbParameterList.AppendFormat("@{0}", property.Name); + if (i < _properties.Count - 1) + { + sbParameterList.Append(", "); + } + } + + return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id"; + } + + private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model) + { + SqlBuilderExtensions.LogQuery(_insertSql, model); + var multi = connection.QueryMultiple(_insertSql, model, transaction); + var id = (int)multi.Read().First().id; + _keyProperty.SetValue(model, id); + + return model; + } + + public void InsertMany(IList models) + { + if (models.Any(x => x.Id != 0)) + { + throw new InvalidOperationException("Can't insert model with existing ID != 0"); + } + + using (var conn = _database.OpenConnection()) + { + using (IDbTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) + { + foreach (var model in models) + { + Insert(conn, tran, model); + } + + tran.Commit(); + } + } + } + public TModel Update(TModel model) { if (model.Id == 0) @@ -124,52 +208,59 @@ namespace NzbDrone.Core.Datastore throw new InvalidOperationException("Can't update model with ID 0"); } - DataMapper.Update(model, c => c.Id == model.Id); + using (var conn = _database.OpenConnection()) + { + UpdateFields(conn, null, model, _properties); + } ModelUpdated(model); return model; } + public void UpdateMany(IList models) + { + if (models.Any(x => x.Id == 0)) + { + throw new InvalidOperationException("Can't update model with ID 0"); + } + + using (var conn = _database.OpenConnection()) + { + UpdateFields(conn, null, models, _properties); + } + } + + protected void Delete(Expression> where) + { + Delete(Builder().Where(where)); + } + + protected void Delete(SqlBuilder builder) + { + var sql = builder.AddDeleteTemplate(typeof(TModel)).LogQuery(); + + using (var conn = _database.OpenConnection()) + { + conn.Execute(sql.RawSql, sql.Parameters); + } + } + public void Delete(TModel model) { Delete(model.Id); } - public void InsertMany(IList models) + public void Delete(int id) { - using (var unitOfWork = new UnitOfWork(() => DataMapper)) - { - unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted); - - foreach (var model in models) - { - unitOfWork.DB.Insert(model); - } - - unitOfWork.Commit(); - } + Delete(x => x.Id == id); } - public void UpdateMany(IList models) + public void DeleteMany(IEnumerable ids) { - using (var unitOfWork = new UnitOfWork(() => DataMapper)) + if (ids.Any()) { - unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted); - - foreach (var model in models) - { - var localModel = model; - - if (model.Id == 0) - { - throw new InvalidOperationException("Can't update model with ID 0"); - } - - unitOfWork.DB.Update(model, c => c.Id == localModel.Id); - } - - unitOfWork.Commit(); + Delete(x => ids.Contains(x.Id)); } } @@ -190,31 +281,13 @@ namespace NzbDrone.Core.Datastore return model; } - public void Delete(int id) - { - DataMapper.Delete(c => c.Id == id); - } - - public void DeleteMany(IEnumerable ids) - { - using (var unitOfWork = new UnitOfWork(() => DataMapper)) - { - unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted); - - foreach (var id in ids) - { - var localId = id; - - unitOfWork.DB.Delete(c => c.Id == localId); - } - - unitOfWork.Commit(); - } - } - public void Purge(bool vacuum = false) { - DataMapper.Delete(c => c.Id > -1); + using (var conn = _database.OpenConnection()) + { + conn.Execute($"DELETE FROM [{_table}]"); + } + if (vacuum) { Vacuum(); @@ -235,67 +308,130 @@ namespace NzbDrone.Core.Datastore { if (model.Id == 0) { - throw new InvalidOperationException("Attempted to updated model without ID"); + throw new InvalidOperationException("Attempted to update model without ID"); } - DataMapper.Update() - .Where(c => c.Id == model.Id) - .ColumnsIncluding(properties) - .Entity(model) - .Execute(); + var propertiesToUpdate = properties.Select(x => x.GetMemberName()).ToList(); + + using (var conn = _database.OpenConnection()) + { + UpdateFields(conn, null, model, propertiesToUpdate); + } ModelUpdated(model); } - public void SetFields(IEnumerable models, params Expression>[] properties) + public void SetFields(IList models, params Expression>[] properties) { - using (var unitOfWork = new UnitOfWork(() => DataMapper)) + if (models.Any(x => x.Id == 0)) { - unitOfWork.BeginTransaction(IsolationLevel.ReadCommitted); + throw new InvalidOperationException("Attempted to update model without ID"); + } - foreach (var model in models) - { - if (model.Id == 0) - { - throw new InvalidOperationException("Can't update model with ID 0"); - } + var propertiesToUpdate = properties.Select(x => x.GetMemberName()).ToList(); - unitOfWork.DB.Update() - .Where(c => c.Id == model.Id) - .ColumnsIncluding(properties) - .Entity(model) - .Execute(); - } + using (var conn = _database.OpenConnection()) + { + UpdateFields(conn, null, models, propertiesToUpdate); + } - unitOfWork.Commit(); + foreach (var model in models) + { + ModelUpdated(model); } } + private string GetUpdateSql(List propertiesToUpdate) + { + var sb = new StringBuilder(); + sb.AppendFormat("UPDATE {0} SET ", _table); + + for (var i = 0; i < propertiesToUpdate.Count; i++) + { + var property = propertiesToUpdate[i]; + sb.AppendFormat("\"{0}\" = @{1}", property.Name, property.Name); + if (i < propertiesToUpdate.Count - 1) + { + sb.Append(", "); + } + } + + sb.Append($" WHERE \"{_keyProperty.Name}\" = @{_keyProperty.Name}"); + + return sb.ToString(); + } + + private void UpdateFields(IDbConnection connection, IDbTransaction transaction, TModel model, List propertiesToUpdate) + { + var sql = propertiesToUpdate == _properties ? _updateSql : GetUpdateSql(propertiesToUpdate); + + SqlBuilderExtensions.LogQuery(sql, model); + + connection.Execute(sql, model, transaction: transaction); + } + + private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList models, List propertiesToUpdate) + { + var sql = propertiesToUpdate == _properties ? _updateSql : GetUpdateSql(propertiesToUpdate); + + foreach (var model in models) + { + SqlBuilderExtensions.LogQuery(sql, model); + } + + connection.Execute(sql, models, transaction: transaction); + } + + protected virtual SqlBuilder PagedBuilder() => Builder(); + protected virtual IEnumerable PagedQuery(SqlBuilder sql) => Query(sql); + public virtual PagingSpec GetPaged(PagingSpec pagingSpec) { - pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList(); - pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount(); + pagingSpec.Records = GetPagedRecords(PagedBuilder(), pagingSpec, PagedQuery); + pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder().SelectCount(), pagingSpec); return pagingSpec; } - protected virtual SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) + private void AddFilters(SqlBuilder builder, PagingSpec pagingSpec) { - var filterExpressions = pagingSpec.FilterExpressions; - var sortQuery = query.Where(filterExpressions.FirstOrDefault()); + var filters = pagingSpec.FilterExpressions; - if (filterExpressions.Count > 1) + foreach (var filter in filters) { - // Start at the second item for the AndWhere clauses - for (var i = 1; i < filterExpressions.Count; i++) - { - sortQuery.AndWhere(filterExpressions[i]); - } + builder.Where(filter); + } + } + + protected List GetPagedRecords(SqlBuilder builder, PagingSpec pagingSpec, Func> queryFunc) + { + AddFilters(builder, pagingSpec); + + var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC"; + var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize; + builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}"); + + return queryFunc(builder).ToList(); + } + + protected int GetPagedRecordCount(SqlBuilder builder, PagingSpec pagingSpec, string template = null) + { + AddFilters(builder, pagingSpec); + + SqlBuilder.Template sql; + if (template != null) + { + sql = builder.AddTemplate(template).LogQuery(); + } + else + { + sql = builder.AddPageCountTemplate(typeof(TModel)); } - return sortQuery.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); + using (var conn = _database.OpenConnection()) + { + return conn.ExecuteScalar(sql.RawSql, sql.Parameters); + } } protected void ModelCreated(TModel model) diff --git a/src/NzbDrone.Core/Datastore/Converters/BooleanIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/BooleanIntConverter.cs deleted file mode 100644 index bf68e751a..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/BooleanIntConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class BooleanIntConverter : IConverter - { - public object FromDB(ConverterContext context) - { - if (context.DbValue == DBNull.Value) - { - return DBNull.Value; - } - - var val = (long)context.DbValue; - - switch (val) - { - case 1: - return true; - case 0: - return false; - default: - throw new ConversionException(string.Format("The BooleanCharConverter could not convert the value '{0}' to a Boolean.", context.DbValue)); - } - } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - var val = (bool?)clrValue; - - switch (val) - { - case true: - return 1; - case false: - return 0; - default: - return DBNull.Value; - } - } - - public Type DbType => typeof(int); - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs b/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs index 8d592b775..e2f77c6f0 100644 --- a/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs @@ -1,42 +1,47 @@ -using System; -using Marr.Data.Converters; +using System.Data; +using System.Text.Json; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Datastore.Converters { - public class CommandConverter : EmbeddedDocumentConverter + public class CommandConverter : EmbeddedDocumentConverter { - public override object FromDB(ConverterContext context) + public override Command Parse(object value) { - if (context.DbValue == DBNull.Value) - { - return null; - } - - var stringValue = (string)context.DbValue; + var stringValue = (string)value; if (stringValue.IsNullOrWhiteSpace()) { return null; } - var ordinal = context.DataRecord.GetOrdinal("Name"); - var contract = context.DataRecord.GetString(ordinal); + string contract; + using (JsonDocument body = JsonDocument.Parse(stringValue)) + { + contract = body.RootElement.GetProperty("name").GetString(); + } + var impType = typeof(Command).Assembly.FindTypeByName(contract + "Command"); if (impType == null) { - var result = Json.Deserialize(stringValue); + var result = JsonSerializer.Deserialize(stringValue, SerializerSettings); result.ContractName = contract; return result; } - return Json.Deserialize(stringValue, impType); + return (Command)JsonSerializer.Deserialize(stringValue, impType, SerializerSettings); + } + + public override void SetValue(IDbDataParameter parameter, Command value) + { + // Cast to object to get all properties written out + // https://github.com/dotnet/corefx/issues/38650 + parameter.Value = value == null ? null : JsonSerializer.Serialize((object)value, SerializerSettings); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/DoubleConverter.cs b/src/NzbDrone.Core/Datastore/Converters/DoubleConverter.cs deleted file mode 100644 index 82f50e326..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/DoubleConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class DoubleConverter : IConverter - { - public object FromDB(ConverterContext context) - { - if (context.DbValue == DBNull.Value) - { - return DBNull.Value; - } - - if (context.DbValue is double) - { - return context.DbValue; - } - - return Convert.ToDouble(context.DbValue); - } - - public object FromDB(ColumnMap map, object dbValue) - { - if (dbValue == DBNull.Value) - { - return DBNull.Value; - } - - if (dbValue is double) - { - return dbValue; - } - - return Convert.ToDouble(dbValue); - } - - public object ToDB(object clrValue) - { - return clrValue; - } - - public Type DbType { get; private set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs index 045ba64e2..6b6744110 100644 --- a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs @@ -1,73 +1,52 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; +using System.Data; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapper; namespace NzbDrone.Core.Datastore.Converters { - public class EmbeddedDocumentConverter : IConverter + public class EmbeddedDocumentConverter : SqlMapper.TypeHandler { - private readonly JsonSerializerSettings _serializerSetting; + protected readonly JsonSerializerOptions SerializerSettings; - public EmbeddedDocumentConverter(params JsonConverter[] converters) + public EmbeddedDocumentConverter() { - _serializerSetting = new JsonSerializerSettings + var serializerSettings = new JsonSerializerOptions { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented, - DefaultValueHandling = DefaultValueHandling.Include, - ContractResolver = new CamelCasePropertyNamesContractResolver() + AllowTrailingCommas = true, + IgnoreNullValues = true, + PropertyNameCaseInsensitive = true, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true }; - _serializerSetting.Converters.Add(new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() }); - _serializerSetting.Converters.Add(new VersionConverter()); + serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); + serializerSettings.Converters.Add(new TimeSpanConverter()); + serializerSettings.Converters.Add(new UtcConverter()); + SerializerSettings = serializerSettings; + } + + public EmbeddedDocumentConverter(params JsonConverter[] converters) + : this() + { foreach (var converter in converters) { - _serializerSetting.Converters.Add(converter); + SerializerSettings.Converters.Add(converter); } } - public virtual object FromDB(ConverterContext context) + public override void SetValue(IDbDataParameter parameter, T value) { - if (context.DbValue == DBNull.Value) - { - return DBNull.Value; - } - - var stringValue = (string)context.DbValue; - - if (string.IsNullOrWhiteSpace(stringValue)) - { - return null; - } - - return JsonConvert.DeserializeObject(stringValue, context.ColumnMap.FieldType, _serializerSetting); + // Cast to object to get all properties written out + // https://github.com/dotnet/corefx/issues/38650 + parameter.Value = JsonSerializer.Serialize((object)value, SerializerSettings); } - public object FromDB(ColumnMap map, object dbValue) + public override T Parse(object value) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + return JsonSerializer.Deserialize((string)value, SerializerSettings); } - - public object ToDB(object clrValue) - { - if (clrValue == null) - { - return null; - } - - if (clrValue == DBNull.Value) - { - return DBNull.Value; - } - - return JsonConvert.SerializeObject(clrValue, _serializerSetting); - } - - public Type DbType => typeof(string); } } diff --git a/src/NzbDrone.Core/Datastore/Converters/EnumIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EnumIntConverter.cs deleted file mode 100644 index ae1e8c46d..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/EnumIntConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class EnumIntConverter : IConverter - { - public Type DbType => typeof(int); - - public object FromDB(ConverterContext context) - { - if (context.DbValue != null && context.DbValue != DBNull.Value) - { - return Enum.ToObject(context.ColumnMap.FieldType, (long)context.DbValue); - } - - return null; - } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue != null) - { - return (int)clrValue; - } - - return DBNull.Value; - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs index 5cab866b2..256ab6502 100644 --- a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs @@ -1,40 +1,24 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; +using System; +using System.Data; +using Dapper; namespace NzbDrone.Core.Datastore.Converters { - public class GuidConverter : IConverter + public class GuidConverter : SqlMapper.TypeHandler { - public object FromDB(ConverterContext context) + public override Guid Parse(object value) { - if (context.DbValue == DBNull.Value) + if (value == null) { return Guid.Empty; } - var value = (string)context.DbValue; - - return new Guid(value); + return new Guid((string)value); } - public object FromDB(ColumnMap map, object dbValue) + public override void SetValue(IDbDataParameter parameter, Guid value) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + parameter.Value = value.ToString(); } - - public object ToDB(object clrValue) - { - if (clrValue == null) - { - return DBNull.Value; - } - - var value = clrValue; - - return value.ToString(); - } - - public Type DbType => typeof(string); } } diff --git a/src/NzbDrone.Core/Datastore/Converters/Int32Converter.cs b/src/NzbDrone.Core/Datastore/Converters/Int32Converter.cs deleted file mode 100644 index 443cd1767..000000000 --- a/src/NzbDrone.Core/Datastore/Converters/Int32Converter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; - -namespace NzbDrone.Core.Datastore.Converters -{ - public class Int32Converter : IConverter - { - public object FromDB(ConverterContext context) - { - if (context.DbValue == DBNull.Value) - { - return DBNull.Value; - } - - if (context.DbValue is int) - { - return context.DbValue; - } - - return Convert.ToInt32(context.DbValue); - } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - return clrValue; - } - - public Type DbType { get; private set; } - } -} diff --git a/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs b/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs index ba2b239fd..068c421fc 100644 --- a/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs @@ -1,36 +1,25 @@ using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; +using System.Data; +using Dapper; using NzbDrone.Common.Disk; namespace NzbDrone.Core.Datastore.Converters { - public class OsPathConverter : IConverter + public class OsPathConverter : SqlMapper.TypeHandler { - public object FromDB(ConverterContext context) + public override void SetValue(IDbDataParameter parameter, OsPath value) { - if (context.DbValue == DBNull.Value) + parameter.Value = value.FullPath; + } + + public override OsPath Parse(object value) + { + if (value == null || value is DBNull) { - return DBNull.Value; + return new OsPath(null); } - var value = (string)context.DbValue; - - return new OsPath(value); + return new OsPath((string)value); } - - public object FromDB(ColumnMap map, object dbValue) - { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - var value = (OsPath)clrValue; - - return value.FullPath; - } - - public Type DbType => typeof(string); } } diff --git a/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs index e84b50e5f..fde20de8a 100644 --- a/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/PrimaryAlbumTypeIntConverter.cs @@ -1,62 +1,21 @@ using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.Music; namespace NzbDrone.Core.Datastore.Converters { - public class PrimaryAlbumTypeIntConverter : JsonConverter, IConverter + public class PrimaryAlbumTypeIntConverter : JsonConverter { - public object FromDB(ConverterContext context) + public override PrimaryAlbumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (context.DbValue == DBNull.Value) - { - return PrimaryAlbumType.Album; - } - - var val = Convert.ToInt32(context.DbValue); - - return (PrimaryAlbumType)val; + var item = reader.GetInt32(); + return (PrimaryAlbumType)item; } - public object FromDB(ColumnMap map, object dbValue) + public override void Write(Utf8JsonWriter writer, PrimaryAlbumType value, JsonSerializerOptions options) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue == DBNull.Value) - { - return 0; - } - - if (clrValue as PrimaryAlbumType == null) - { - throw new InvalidOperationException("Attempted to save an album type that isn't really an album type"); - } - - var primType = (PrimaryAlbumType)clrValue; - return (int)primType; - } - - public Type DbType => typeof(int); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(PrimaryAlbumType); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var item = reader.Value; - return (PrimaryAlbumType)Convert.ToInt32(item); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(ToDB(value)); + writer.WriteNumberValue((int)value); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/ProviderSettingConverter.cs b/src/NzbDrone.Core/Datastore/Converters/ProviderSettingConverter.cs index 17590c4f1..99c9bef1d 100644 --- a/src/NzbDrone.Core/Datastore/Converters/ProviderSettingConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/ProviderSettingConverter.cs @@ -1,38 +1,22 @@ -using System; -using Marr.Data.Converters; -using NzbDrone.Common.Reflection; -using NzbDrone.Common.Serializer; +using System.Data; +using System.Text.Json; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Datastore.Converters { - public class ProviderSettingConverter : EmbeddedDocumentConverter + public class ProviderSettingConverter : EmbeddedDocumentConverter { - public override object FromDB(ConverterContext context) + public override IProviderConfig Parse(object value) { - if (context.DbValue == DBNull.Value) - { - return NullConfig.Instance; - } + // We can't deserialize based on another column, happens in ProviderRepository instead + return null; + } - var stringValue = (string)context.DbValue; - - if (string.IsNullOrWhiteSpace(stringValue)) - { - return NullConfig.Instance; - } - - var ordinal = context.DataRecord.GetOrdinal("ConfigContract"); - var contract = context.DataRecord.GetString(ordinal); - - var impType = typeof(IProviderConfig).Assembly.FindTypeByName(contract); - - if (impType == null) - { - throw new ConfigContractNotFoundException(contract); - } - - return Json.Deserialize(stringValue, impType); + public override void SetValue(IDbDataParameter parameter, IProviderConfig value) + { + // Cast to object to get all properties written out + // https://github.com/dotnet/corefx/issues/38650 + parameter.Value = JsonSerializer.Serialize((object)value, SerializerSettings); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs index e8b72b206..13b595d35 100644 --- a/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/QualityIntConverter.cs @@ -1,62 +1,36 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Newtonsoft.Json; +using System; +using System.Data; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapper; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Datastore.Converters { - public class QualityIntConverter : JsonConverter, IConverter + public class QualityIntConverter : JsonConverter { - public object FromDB(ConverterContext context) + public override Quality Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (context.DbValue == DBNull.Value) - { - return Quality.Unknown; - } - - var val = Convert.ToInt32(context.DbValue); - - return (Quality)val; + var item = reader.GetInt32(); + return (Quality)item; } - public object FromDB(ColumnMap map, object dbValue) + public override void Write(Utf8JsonWriter writer, Quality value, JsonSerializerOptions options) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + writer.WriteNumberValue((int)value); + } + } + + public class DapperQualityIntConverter : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, Quality value) + { + parameter.Value = value == null ? 0 : (int)value; } - public object ToDB(object clrValue) + public override Quality Parse(object value) { - if (clrValue == DBNull.Value) - { - return 0; - } - - if (clrValue as Quality == null) - { - throw new InvalidOperationException("Attempted to save a quality that isn't really a quality"); - } - - var quality = clrValue as Quality; - return (int)quality; - } - - public Type DbType => typeof(int); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Quality); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var item = reader.Value; - return (Quality)Convert.ToInt32(item); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(ToDB(value)); + return (Quality)Convert.ToInt32(value); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs index 1805d9f27..68faafb85 100644 --- a/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/ReleaseStatusIntConverter.cs @@ -1,62 +1,21 @@ using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.Music; namespace NzbDrone.Core.Datastore.Converters { - public class ReleaseStatusIntConverter : JsonConverter, IConverter + public class ReleaseStatusIntConverter : JsonConverter { - public object FromDB(ConverterContext context) + public override ReleaseStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (context.DbValue == DBNull.Value) - { - return ReleaseStatus.Official; - } - - var val = Convert.ToInt32(context.DbValue); - - return (ReleaseStatus)val; + var item = reader.GetInt32(); + return (ReleaseStatus)item; } - public object FromDB(ColumnMap map, object dbValue) + public override void Write(Utf8JsonWriter writer, ReleaseStatus value, JsonSerializerOptions options) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue == DBNull.Value) - { - return 0; - } - - if (clrValue as ReleaseStatus == null) - { - throw new InvalidOperationException("Attempted to save a release status that isn't really a release status"); - } - - var releaseStatus = (ReleaseStatus)clrValue; - return (int)releaseStatus; - } - - public Type DbType => typeof(int); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ReleaseStatus); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var item = reader.Value; - return (ReleaseStatus)Convert.ToInt32(item); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(ToDB(value)); + writer.WriteNumberValue((int)value); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs index f21f66f7a..7811327a1 100644 --- a/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/SecondaryAlbumTypeIntConverter.cs @@ -1,65 +1,21 @@ using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using NzbDrone.Core.Music; namespace NzbDrone.Core.Datastore.Converters { - public class SecondaryAlbumTypeIntConverter : JsonConverter, IConverter + public class SecondaryAlbumTypeIntConverter : JsonConverter { - public object FromDB(ConverterContext context) + public override SecondaryAlbumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (context.DbValue == DBNull.Value) - { - return SecondaryAlbumType.Studio; - } - - var val = Convert.ToInt32(context.DbValue); - - return (SecondaryAlbumType)val; + var item = reader.GetInt32(); + return (SecondaryAlbumType)item; } - public object FromDB(ColumnMap map, object dbValue) + public override void Write(Utf8JsonWriter writer, SecondaryAlbumType value, JsonSerializerOptions options) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); - } - - public object ToDB(object clrValue) - { - if (clrValue == DBNull.Value) - { - return 0; - } - - if (clrValue as SecondaryAlbumType == null) - { - throw new InvalidOperationException("Attempted to save an album type that isn't really an album type"); - } - - var secType = (SecondaryAlbumType)clrValue; - return (int)secType; - } - - public Type DbType => typeof(int); - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(SecondaryAlbumType); - } - - public override object ReadJson(JsonReader reader, - Type objectType, - object existingValue, - JsonSerializer serializer) - { - var item = reader.Value; - return (SecondaryAlbumType)Convert.ToInt32(item); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(ToDB(value)); + writer.WriteNumberValue((int)value); } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs index f8080fdff..07f2d9314 100644 --- a/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs @@ -1,43 +1,19 @@ -using System; -using System.Globalization; -using Marr.Data.Converters; -using Marr.Data.Mapping; -using NzbDrone.Common.Extensions; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace NzbDrone.Core.Datastore.Converters { - public class TimeSpanConverter : IConverter + public class TimeSpanConverter : JsonConverter { - public object FromDB(ConverterContext context) + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (context.DbValue == DBNull.Value) - { - return TimeSpan.Zero; - } - - if (context.DbValue is TimeSpan) - { - return context.DbValue; - } - - return TimeSpan.Parse(context.DbValue.ToString(), CultureInfo.InvariantCulture); + return TimeSpan.Parse(reader.GetString()); } - public object FromDB(ColumnMap map, object dbValue) + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + writer.WriteStringValue(value.ToString()); } - - public object ToDB(object clrValue) - { - if (clrValue.ToString().IsNullOrWhiteSpace()) - { - return null; - } - - return ((TimeSpan)clrValue).ToString("c", CultureInfo.InvariantCulture); - } - - public Type DbType { get; private set; } } } diff --git a/src/NzbDrone.Core/Datastore/Converters/UtcConverter.cs b/src/NzbDrone.Core/Datastore/Converters/UtcConverter.cs index 3fd28d8a8..db70f2359 100644 --- a/src/NzbDrone.Core/Datastore/Converters/UtcConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/UtcConverter.cs @@ -1,32 +1,34 @@ -using System; -using Marr.Data.Converters; -using Marr.Data.Mapping; +using System; +using System.Data; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapper; namespace NzbDrone.Core.Datastore.Converters { - public class UtcConverter : IConverter + public class DapperUtcConverter : SqlMapper.TypeHandler { - public object FromDB(ConverterContext context) + public override void SetValue(IDbDataParameter parameter, DateTime value) { - return context.DbValue; + parameter.Value = value.ToUniversalTime(); } - public object FromDB(ColumnMap map, object dbValue) + public override DateTime Parse(object value) { - return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc); + } + } + + public class UtcConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return DateTime.Parse(reader.GetString()); } - public object ToDB(object clrValue) + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - if (clrValue == DBNull.Value) - { - return clrValue; - } - - var dateTime = (DateTime)clrValue; - return dateTime.ToUniversalTime(); + writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ")); } - - public Type DbType => typeof(DateTime); } } diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index 9cc9665e9..a9b6e807f 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -1,5 +1,6 @@ -using System; -using Marr.Data; +using System; +using System.Data; +using Dapper; using NLog; using NzbDrone.Common.Instrumentation; @@ -7,7 +8,7 @@ namespace NzbDrone.Core.Datastore { public interface IDatabase { - IDataMapper GetDataMapper(); + IDbConnection OpenConnection(); Version Version { get; } int Migration { get; } void Vacuum(); @@ -16,17 +17,17 @@ namespace NzbDrone.Core.Datastore public class Database : IDatabase { private readonly string _databaseName; - private readonly Func _datamapperFactory; + private readonly Func _datamapperFactory; private readonly Logger _logger = NzbDroneLogger.GetLogger(typeof(Database)); - public Database(string databaseName, Func datamapperFactory) + public Database(string databaseName, Func datamapperFactory) { _databaseName = databaseName; _datamapperFactory = datamapperFactory; } - public IDataMapper GetDataMapper() + public IDbConnection OpenConnection() { return _datamapperFactory(); } @@ -35,8 +36,11 @@ namespace NzbDrone.Core.Datastore { get { - var version = _datamapperFactory().ExecuteScalar("SELECT sqlite_version()").ToString(); - return new Version(version); + using (var db = _datamapperFactory()) + { + var version = db.QueryFirstOrDefault("SELECT sqlite_version()"); + return new Version(version); + } } } @@ -44,9 +48,10 @@ namespace NzbDrone.Core.Datastore { get { - var migration = _datamapperFactory() - .ExecuteScalar("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1").ToString(); - return Convert.ToInt32(migration); + using (var db = _datamapperFactory()) + { + return db.QueryFirstOrDefault("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1"); + } } } @@ -55,7 +60,11 @@ namespace NzbDrone.Core.Datastore try { _logger.Info("Vacuuming {0} database", _databaseName); - _datamapperFactory().ExecuteNonQuery("Vacuum;"); + using (var db = _datamapperFactory()) + { + db.Execute("Vacuum;"); + } + _logger.Info("{0} database compressed", _databaseName); } catch (Exception e) diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index 742384c4f..a23b75757 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -1,7 +1,5 @@ using System; using System.Data.SQLite; -using Marr.Data; -using Marr.Data.Reflection; using NLog; using NzbDrone.Common.Composition; using NzbDrone.Common.Disk; @@ -30,7 +28,6 @@ namespace NzbDrone.Core.Datastore { InitializeEnvironment(); - MapRepository.Instance.ReflectionStrategy = new SimpleReflectionStrategy(); TableMapping.Map(); } @@ -99,12 +96,11 @@ namespace NzbDrone.Core.Datastore var db = new Database(migrationContext.MigrationType.ToString(), () => { - var dataMapper = new DataMapper(SQLiteFactory.Instance, connectionString) - { - SqlMode = SqlModes.Text, - }; + var conn = SQLiteFactory.Instance.CreateConnection(); + conn.ConnectionString = connectionString; + conn.Open(); - return dataMapper; + return conn; }); return db; @@ -123,7 +119,7 @@ namespace NzbDrone.Core.Datastore if (OsInfo.IsOsx) { - throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Lidarr/Lidarr/wiki/FAQ#i-use-lidarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName); + throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName); } throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Lidarr/Lidarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName); diff --git a/src/NzbDrone.Core/Datastore/ExpressionVisitor.cs b/src/NzbDrone.Core/Datastore/ExpressionVisitor.cs new file mode 100644 index 000000000..bcb977af1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/ExpressionVisitor.cs @@ -0,0 +1,148 @@ +/* This class was copied from Mehfuz's LinqExtender project, which is available from github. + * http://mehfuzh.github.com/LinqExtender/ + */ + +using System; +using System.Linq.Expressions; + +namespace NzbDrone.Core.Datastore +{ + /// + /// Expression visitor + /// + public class ExpressionVisitor + { + /// + /// Visits expression and delegates call to different to branch. + /// + /// + /// + protected virtual Expression Visit(Expression expression) + { + if (expression == null) + { + return null; + } + + switch (expression.NodeType) + { + case ExpressionType.Lambda: + return VisitLamda((LambdaExpression)expression); + case ExpressionType.ArrayLength: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.Negate: + case ExpressionType.UnaryPlus: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + return VisitUnary((UnaryExpression)expression); + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.ArrayIndex: + case ExpressionType.Coalesce: + case ExpressionType.Divide: + case ExpressionType.Equal: + case ExpressionType.ExclusiveOr: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LeftShift: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.Modulo: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.NotEqual: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.Power: + case ExpressionType.RightShift: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + return VisitBinary((BinaryExpression)expression); + case ExpressionType.Call: + return VisitMethodCall((MethodCallExpression)expression); + case ExpressionType.Constant: + return VisitConstant((ConstantExpression)expression); + case ExpressionType.MemberAccess: + return VisitMemberAccess((MemberExpression)expression); + case ExpressionType.Parameter: + return VisitParameter((ParameterExpression)expression); + } + + throw new ArgumentOutOfRangeException("expression", expression.NodeType.ToString()); + } + + /// + /// Visits the constance expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitConstant(ConstantExpression expression) + { + return expression; + } + + /// + /// Visits the memeber access expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitMemberAccess(MemberExpression expression) + { + return expression; + } + + /// + /// Visits the method call expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitMethodCall(MethodCallExpression expression) + { + throw new NotImplementedException(); + } + + /// + /// Visits the binary expression. + /// + /// + /// + protected virtual Expression VisitBinary(BinaryExpression expression) + { + Visit(expression.Left); + Visit(expression.Right); + return expression; + } + + /// + /// Visits the unary expression. + /// + /// + /// + protected virtual Expression VisitUnary(UnaryExpression expression) + { + Visit(expression.Operand); + return expression; + } + + /// + /// Visits the lamda expression. + /// + /// + /// + protected virtual Expression VisitLamda(LambdaExpression lambdaExpression) + { + Visit(lambdaExpression.Body); + return lambdaExpression; + } + + private Expression VisitParameter(ParameterExpression expression) + { + return expression; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs new file mode 100644 index 000000000..cba52d2cb --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Dapper; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Datastore +{ + public static class SqlBuilderExtensions + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(SqlBuilderExtensions)); + + public static bool LogSql { get; set; } + + public static SqlBuilder Select(this SqlBuilder builder, params Type[] types) + { + return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", ")); + } + + public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types) + { + return builder.Select("DISTINCT " + types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", ")); + } + + public static SqlBuilder SelectCount(this SqlBuilder builder) + { + return builder.Select("COUNT(*)"); + } + + public static SqlBuilder SelectCountDistinct(this SqlBuilder builder, Expression> property) + { + var table = TableMapping.Mapper.TableNameMapping(typeof(TModel)); + var propName = property.GetMemberName().Name; + return builder.Select($"COUNT(DISTINCT \"{table}\".\"{propName}\")"); + } + + public static SqlBuilder Where(this SqlBuilder builder, Expression> filter) + { + var wb = new WhereBuilder(filter, true, builder.Sequence); + + return builder.Where(wb.ToString(), wb.Parameters); + } + + public static SqlBuilder OrWhere(this SqlBuilder builder, Expression> filter) + { + var wb = new WhereBuilder(filter, true, builder.Sequence); + + return builder.OrWhere(wb.ToString(), wb.Parameters); + } + + public static SqlBuilder Join(this SqlBuilder builder, Expression> filter) + { + var wb = new WhereBuilder(filter, false, builder.Sequence); + + var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); + + return builder.Join($"{rightTable} ON {wb.ToString()}"); + } + + public static SqlBuilder LeftJoin(this SqlBuilder builder, Expression> filter) + { + var wb = new WhereBuilder(filter, false, builder.Sequence); + + var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); + + return builder.LeftJoin($"{rightTable} ON {wb.ToString()}"); + } + + public static SqlBuilder GroupBy(this SqlBuilder builder, Expression> property) + { + var table = TableMapping.Mapper.TableNameMapping(typeof(TModel)); + var propName = property.GetMemberName().Name; + return builder.GroupBy($"{table}.{propName}"); + } + + public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type) + { + return builder.AddTemplate(TableMapping.Mapper.SelectTemplate(type)).LogQuery(); + } + + public static SqlBuilder.Template AddPageCountTemplate(this SqlBuilder builder, Type type) + { + return builder.AddTemplate(TableMapping.Mapper.PageCountTemplate(type)).LogQuery(); + } + + public static SqlBuilder.Template AddDeleteTemplate(this SqlBuilder builder, Type type) + { + return builder.AddTemplate(TableMapping.Mapper.DeleteTemplate(type)).LogQuery(); + } + + public static SqlBuilder.Template LogQuery(this SqlBuilder.Template template) + { + if (LogSql) + { + LogQuery(template.RawSql, (DynamicParameters)template.Parameters); + } + + return template; + } + + public static void LogQuery(string sql, object parameters) + { + if (LogSql) + { + LogQuery(sql, new DynamicParameters(parameters)); + } + } + + private static void LogQuery(string sql, DynamicParameters parameters) + { + var sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine("==== Begin Query Trace ===="); + sb.AppendLine(); + sb.AppendLine("QUERY TEXT:"); + sb.AppendLine(sql); + sb.AppendLine(); + sb.AppendLine("PARAMETERS:"); + foreach (var p in parameters.ToDictionary()) + { + var val = (p.Value is string) ? string.Format("\"{0}\"", p.Value) : p.Value; + sb.AppendFormat("{0} = [{1}]", p.Key, val.ToJson() ?? "NULL").AppendLine(); + } + + sb.AppendLine(); + sb.AppendLine("==== End Query Trace ===="); + sb.AppendLine(); + + Logger.Trace(sb.ToString()); + } + + private static Dictionary ToDictionary(this DynamicParameters dynamicParams) + { + var argsDictionary = new Dictionary(); + var iLookup = (SqlMapper.IParameterLookup)dynamicParams; + + foreach (var paramName in dynamicParams.ParameterNames) + { + var value = iLookup[paramName]; + argsDictionary.Add(paramName, value); + } + + var templates = dynamicParams.GetType().GetField("templates", BindingFlags.NonPublic | BindingFlags.Instance); + if (templates != null && templates.GetValue(dynamicParams) is List list) + { + foreach (var objProps in list.Select(obj => obj.GetPropertyValuePairs().ToList())) + { + objProps.ForEach(p => argsDictionary.Add(p.Key, p.Value)); + } + } + + return argsDictionary; + } + + private static Dictionary GetPropertyValuePairs(this object obj) + { + var type = obj.GetType(); + var pairs = type.GetProperties().Where(x => x.IsMappableProperty()) + .DistinctBy(propertyInfo => propertyInfo.Name) + .ToDictionary( + propertyInfo => propertyInfo.Name, + propertyInfo => propertyInfo.GetValue(obj, null)); + return pairs; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs index 96cdd9c0d..7be628d96 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs @@ -1,48 +1,24 @@ -using System.Reflection; -using Marr.Data; -using Marr.Data.Mapping; +using System; +using System.Linq.Expressions; +using System.Reflection; +using Dapper; using NzbDrone.Common.Reflection; -using NzbDrone.Core.ThingiProvider; -namespace NzbDrone.Core.Datastore.Extensions +namespace NzbDrone.Core.Datastore { public static class MappingExtensions { - public static ColumnMapBuilder MapResultSet(this FluentMappings.MappingsFluentEntity mapBuilder) - where T : ResultSet, new() + public static PropertyInfo GetMemberName(this Expression> member) { - return mapBuilder - .Columns - .AutoMapPropertiesWhere(IsMappableProperty); + if (!(member.Body is MemberExpression memberExpression)) + { + memberExpression = (member.Body as UnaryExpression).Operand as MemberExpression; + } + + return (PropertyInfo)memberExpression.Member; } - public static ColumnMapBuilder RegisterDefinition(this FluentMappings.MappingsFluentEntity mapBuilder, string tableName = null) - where T : ProviderDefinition, new() - { - return RegisterModel(mapBuilder, tableName).Ignore(c => c.ImplementationName); - } - - public static ColumnMapBuilder RegisterModel(this FluentMappings.MappingsFluentEntity mapBuilder, string tableName = null) - where T : ModelBase, new() - { - return mapBuilder.Table.MapTable(tableName) - .Columns - .AutoMapPropertiesWhere(IsMappableProperty) - .PrefixAltNames(string.Format("{0}_", typeof(T).Name)) - .For(c => c.Id) - .SetPrimaryKey() - .SetReturnValue() - .SetAutoIncrement(); - } - - public static RelationshipBuilder AutoMapChildModels(this ColumnMapBuilder mapBuilder) - { - return mapBuilder.Relationships.AutoMapPropertiesWhere(m => - m.MemberType == MemberTypes.Property && - typeof(ModelBase).IsAssignableFrom(((PropertyInfo)m).PropertyType)); - } - - public static bool IsMappableProperty(MemberInfo memberInfo) + public static bool IsMappableProperty(this MemberInfo memberInfo) { var propertyInfo = memberInfo as PropertyInfo; @@ -56,7 +32,11 @@ namespace NzbDrone.Core.Datastore.Extensions return false; } - if (propertyInfo.PropertyType.IsSimpleType() || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType)) + // This is a bit of a hack but is the only way to see if a type has a handler set in Dapper +#pragma warning disable 618 + SqlMapper.LookupDbType(propertyInfo.PropertyType, "", false, out var handler); +#pragma warning restore 618 + if (propertyInfo.PropertyType.IsSimpleType() || handler != null) { return true; } diff --git a/src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs deleted file mode 100644 index 46d217585..000000000 --- a/src/NzbDrone.Core/Datastore/Extensions/PagingSpecExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; - -namespace NzbDrone.Core.Datastore.Extensions -{ - public static class PagingSpecExtensions - { - public static Expression> OrderByClause(this PagingSpec pagingSpec) - { - return CreateExpression(pagingSpec.SortKey); - } - - public static int PagingOffset(this PagingSpec pagingSpec) - { - return (pagingSpec.Page - 1) * pagingSpec.PageSize; - } - - public static Marr.Data.QGen.SortDirection ToSortDirection(this PagingSpec pagingSpec) - { - if (pagingSpec.SortDirection == SortDirection.Descending) - { - return Marr.Data.QGen.SortDirection.Desc; - } - - return Marr.Data.QGen.SortDirection.Asc; - } - - private static Expression> CreateExpression(string propertyName) - { - Type type = typeof(TModel); - ParameterExpression parameterExpression = Expression.Parameter(type, "x"); - Expression expressionBody = parameterExpression; - - var splitPropertyName = propertyName.Split('.').ToList(); - - foreach (var property in splitPropertyName) - { - expressionBody = Expression.Property(expressionBody, property); - } - - expressionBody = Expression.Convert(expressionBody, typeof(object)); - return Expression.Lambda>(expressionBody, parameterExpression); - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs deleted file mode 100644 index 98df74317..000000000 --- a/src/NzbDrone.Core/Datastore/Extensions/RelationshipExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using Marr.Data; -using Marr.Data.Mapping; - -namespace NzbDrone.Core.Datastore.Extensions -{ - public static class RelationshipExtensions - { - public static RelationshipBuilder HasOne(this RelationshipBuilder relationshipBuilder, Expression>> portalExpression, Func childIdSelector) - where TParent : ModelBase - where TChild : ModelBase - { - return relationshipBuilder.For(portalExpression.GetMemberName()) - .LazyLoad( - condition: parent => childIdSelector(parent) > 0, - query: (db, parent) => - { - var id = childIdSelector(parent); - return db.Query().Where(c => c.Id == id).SingleOrDefault(); - }); - } - - public static RelationshipBuilder Relationship(this ColumnMapBuilder mapBuilder) - { - return mapBuilder.Relationships.MapProperties(); - } - - private static string GetMemberName(this Expression> member) - { - var expression = member.Body as MemberExpression; - - if (expression == null) - { - expression = (MemberExpression)((UnaryExpression)member.Body).Operand; - } - - return expression.Member.Name; - } - } -} diff --git a/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs new file mode 100644 index 000000000..801961c30 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Extensions/SqlMapperExtensions.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Dapper; + +namespace NzbDrone.Core.Datastore +{ + public static class SqlMapperExtensions + { + public static IEnumerable Query(this IDatabase db, string sql, object param = null) + { + using (var conn = db.OpenConnection()) + { + var items = SqlMapper.Query(conn, sql, param); + if (TableMapping.Mapper.LazyLoadList.TryGetValue(typeof(T), out var lazyProperties)) + { + foreach (var item in items) + { + ApplyLazyLoad(db, item, lazyProperties); + } + } + + return items; + } + } + + public static IEnumerable Query(this IDatabase db, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + TReturn MapWithLazy(TFirst first, TSecond second) + { + ApplyLazyLoad(db, first); + ApplyLazyLoad(db, second); + return map(first, second); + } + + IEnumerable result = null; + using (var conn = db.OpenConnection()) + { + result = SqlMapper.Query(conn, sql, MapWithLazy, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + return result; + } + + public static IEnumerable Query(this IDatabase db, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + TReturn MapWithLazy(TFirst first, TSecond second, TThird third) + { + ApplyLazyLoad(db, first); + ApplyLazyLoad(db, second); + ApplyLazyLoad(db, third); + return map(first, second, third); + } + + IEnumerable result = null; + using (var conn = db.OpenConnection()) + { + result = SqlMapper.Query(conn, sql, MapWithLazy, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + return result; + } + + public static IEnumerable Query(this IDatabase db, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + TReturn MapWithLazy(TFirst first, TSecond second, TThird third, TFourth fourth) + { + ApplyLazyLoad(db, first); + ApplyLazyLoad(db, second); + ApplyLazyLoad(db, third); + ApplyLazyLoad(db, fourth); + return map(first, second, third, fourth); + } + + IEnumerable result = null; + using (var conn = db.OpenConnection()) + { + result = SqlMapper.Query(conn, sql, MapWithLazy, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + return result; + } + + public static IEnumerable Query(this IDatabase db, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + TReturn MapWithLazy(TFirst first, TSecond second, TThird third, TFourth fourth, TFifth fifth) + { + ApplyLazyLoad(db, first); + ApplyLazyLoad(db, second); + ApplyLazyLoad(db, third); + ApplyLazyLoad(db, fourth); + ApplyLazyLoad(db, fifth); + return map(first, second, third, fourth, fifth); + } + + IEnumerable result = null; + using (var conn = db.OpenConnection()) + { + result = SqlMapper.Query(conn, sql, MapWithLazy, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + return result; + } + + public static IEnumerable Query(this IDatabase db, SqlBuilder builder) + { + var type = typeof(T); + var sql = builder.Select(type).AddSelectTemplate(type); + + return db.Query(sql.RawSql, sql.Parameters); + } + + public static IEnumerable QueryDistinct(this IDatabase db, SqlBuilder builder) + { + var type = typeof(T); + var sql = builder.SelectDistinct(type).AddSelectTemplate(type); + + return db.Query(sql.RawSql, sql.Parameters); + } + + public static IEnumerable QueryJoined(this IDatabase db, SqlBuilder builder, Func mapper) + { + var type = typeof(T); + var sql = builder.Select(type, typeof(T2)).AddSelectTemplate(type); + + return db.Query(sql.RawSql, mapper, sql.Parameters); + } + + public static IEnumerable QueryJoined(this IDatabase db, SqlBuilder builder, Func mapper) + { + var type = typeof(T); + var sql = builder.Select(type, typeof(T2), typeof(T3)).AddSelectTemplate(type); + + return db.Query(sql.RawSql, mapper, sql.Parameters); + } + + public static IEnumerable QueryJoined(this IDatabase db, SqlBuilder builder, Func mapper) + { + var type = typeof(T); + var sql = builder.Select(type, typeof(T2), typeof(T3), typeof(T4)).AddSelectTemplate(type); + + return db.Query(sql.RawSql, mapper, sql.Parameters); + } + + public static IEnumerable QueryJoined(this IDatabase db, SqlBuilder builder, Func mapper) + { + var type = typeof(T); + var sql = builder.Select(type, typeof(T2), typeof(T3), typeof(T4), typeof(T5)).AddSelectTemplate(type); + + return db.Query(sql.RawSql, mapper, sql.Parameters); + } + + private static void ApplyLazyLoad(IDatabase db, TModel model) + { + if (TableMapping.Mapper.LazyLoadList.TryGetValue(typeof(TModel), out var lazyProperties)) + { + ApplyLazyLoad(db, model, lazyProperties); + } + } + + private static void ApplyLazyLoad(IDatabase db, TModel model, List lazyProperties) + { + if (model == null) + { + return; + } + + foreach (var lazyProperty in lazyProperties) + { + var lazy = (ILazyLoaded)lazyProperty.LazyLoad.Clone(); + lazy.Prepare(db, model); + lazyProperty.Property.SetValue(model, lazy); + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/LazyList.cs b/src/NzbDrone.Core/Datastore/LazyList.cs deleted file mode 100644 index 193a11812..000000000 --- a/src/NzbDrone.Core/Datastore/LazyList.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using Marr.Data; - -namespace NzbDrone.Core.Datastore -{ - public class LazyList : LazyLoaded> - { - public LazyList() - : this(new List()) - { - } - - public LazyList(IEnumerable items) - : base(new List(items)) - { - } - - public static implicit operator LazyList(List val) - { - return new LazyList(val); - } - - public static implicit operator List(LazyList lazy) - { - return lazy.Value; - } - } -} diff --git a/src/NzbDrone.Core/Datastore/LazyLoaded.cs b/src/NzbDrone.Core/Datastore/LazyLoaded.cs new file mode 100644 index 000000000..c5dbde360 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/LazyLoaded.cs @@ -0,0 +1,129 @@ +using System; + +namespace NzbDrone.Core.Datastore +{ + public interface ILazyLoaded : ICloneable + { + bool IsLoaded { get; } + void Prepare(IDatabase database, object parent); + void LazyLoad(); + } + + /// + /// Allows a field to be lazy loaded. + /// + /// + public class LazyLoaded : ILazyLoaded + { + protected TChild _value; + + public LazyLoaded() + { + } + + public LazyLoaded(TChild val) + { + _value = val; + IsLoaded = true; + } + + public TChild Value + { + get + { + LazyLoad(); + return _value; + } + } + + public bool IsLoaded { get; protected set; } + + public static implicit operator LazyLoaded(TChild val) + { + return new LazyLoaded(val); + } + + public static implicit operator TChild(LazyLoaded lazy) + { + return lazy.Value; + } + + public virtual void Prepare(IDatabase database, object parent) + { + } + + public virtual void LazyLoad() + { + } + + public object Clone() + { + return MemberwiseClone(); + } + + public bool ShouldSerializeValue() + { + return IsLoaded; + } + } + + /// + /// This is the lazy loading proxy. + /// + /// The parent entity that contains the lazy loaded entity. + /// The child entity that is being lazy loaded. + internal class LazyLoaded : LazyLoaded + { + private readonly Func _query; + private readonly Func _condition; + + private IDatabase _database; + private TParent _parent; + + public LazyLoaded(TChild val) + : base(val) + { + _value = val; + IsLoaded = true; + } + + internal LazyLoaded(Func query, Func condition = null) + { + _query = query; + _condition = condition; + } + + public static implicit operator LazyLoaded(TChild val) + { + return new LazyLoaded(val); + } + + public static implicit operator TChild(LazyLoaded lazy) + { + return lazy.Value; + } + + public override void Prepare(IDatabase database, object parent) + { + _database = database; + _parent = (TParent)parent; + } + + public override void LazyLoad() + { + if (!IsLoaded) + { + if (_condition != null && _condition(_parent)) + { + _value = _query(_database, _parent); + } + else + { + _value = default; + } + + IsLoaded = true; + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/LogDatabase.cs b/src/NzbDrone.Core/Datastore/LogDatabase.cs index 7e15d1549..f992c8bbe 100644 --- a/src/NzbDrone.Core/Datastore/LogDatabase.cs +++ b/src/NzbDrone.Core/Datastore/LogDatabase.cs @@ -1,5 +1,5 @@ -using System; -using Marr.Data; +using System; +using System.Data; namespace NzbDrone.Core.Datastore { @@ -16,9 +16,9 @@ namespace NzbDrone.Core.Datastore _database = database; } - public IDataMapper GetDataMapper() + public IDbConnection OpenConnection() { - return _database.GetDataMapper(); + return _database.OpenConnection(); } public Version Version => _database.Version; diff --git a/src/NzbDrone.Core/Datastore/MainDatabase.cs b/src/NzbDrone.Core/Datastore/MainDatabase.cs index 4e5cbd780..4a9d3298c 100644 --- a/src/NzbDrone.Core/Datastore/MainDatabase.cs +++ b/src/NzbDrone.Core/Datastore/MainDatabase.cs @@ -1,5 +1,5 @@ -using System; -using Marr.Data; +using System; +using System.Data; namespace NzbDrone.Core.Datastore { @@ -16,9 +16,9 @@ namespace NzbDrone.Core.Datastore _database = database; } - public IDataMapper GetDataMapper() + public IDbConnection OpenConnection() { - return _database.GetDataMapper(); + return _database.OpenConnection(); } public Version Version => _database.Version; diff --git a/src/NzbDrone.Core/Datastore/SqlBuilder.cs b/src/NzbDrone.Core/Datastore/SqlBuilder.cs new file mode 100644 index 000000000..e686d4852 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/SqlBuilder.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Dapper; + +namespace NzbDrone.Core.Datastore +{ + public class SqlBuilder + { + private readonly Dictionary _data = new Dictionary(); + + public int Sequence { get; private set; } + + public Template AddTemplate(string sql, dynamic parameters = null) => + new Template(this, sql, parameters); + + public SqlBuilder Intersect(string sql, dynamic parameters = null) => + AddClause("intersect", sql, parameters, "\nINTERSECT\n ", "\n ", "\n", false); + + public SqlBuilder InnerJoin(string sql, dynamic parameters = null) => + AddClause("innerjoin", sql, parameters, "\nINNER JOIN ", "\nINNER JOIN ", "\n", false); + + public SqlBuilder LeftJoin(string sql, dynamic parameters = null) => + AddClause("leftjoin", sql, parameters, "\nLEFT JOIN ", "\nLEFT JOIN ", "\n", false); + + public SqlBuilder RightJoin(string sql, dynamic parameters = null) => + AddClause("rightjoin", sql, parameters, "\nRIGHT JOIN ", "\nRIGHT JOIN ", "\n", false); + + public SqlBuilder Where(string sql, dynamic parameters = null) => + AddClause("where", sql, parameters, " AND ", "WHERE ", "\n", false); + + public SqlBuilder OrWhere(string sql, dynamic parameters = null) => + AddClause("where", sql, parameters, " OR ", "WHERE ", "\n", true); + + public SqlBuilder OrderBy(string sql, dynamic parameters = null) => + AddClause("orderby", sql, parameters, " , ", "ORDER BY ", "\n", false); + + public SqlBuilder Select(string sql, dynamic parameters = null) => + AddClause("select", sql, parameters, " , ", "", "\n", false); + + public SqlBuilder AddParameters(dynamic parameters) => + AddClause("--parameters", "", parameters, "", "", "", false); + + public SqlBuilder Join(string sql, dynamic parameters = null) => + AddClause("join", sql, parameters, "\nJOIN ", "\nJOIN ", "\n", false); + + public SqlBuilder GroupBy(string sql, dynamic parameters = null) => + AddClause("groupby", sql, parameters, " , ", "\nGROUP BY ", "\n", false); + + public SqlBuilder Having(string sql, dynamic parameters = null) => + AddClause("having", sql, parameters, "\nAND ", "HAVING ", "\n", false); + + protected SqlBuilder AddClause(string name, string sql, object parameters, string joiner, string prefix = "", string postfix = "", bool isInclusive = false) + { + if (!_data.TryGetValue(name, out var clauses)) + { + clauses = new Clauses(joiner, prefix, postfix); + _data[name] = clauses; + } + + clauses.Add(new Clause { Sql = sql, Parameters = parameters, IsInclusive = isInclusive }); + Sequence++; + return this; + } + + public class Template + { + private static readonly Regex _regex = new Regex(@"\/\*\*.+?\*\*\/", RegexOptions.Compiled | RegexOptions.Multiline); + + private readonly string _sql; + private readonly SqlBuilder _builder; + private readonly object _initParams; + + private int _dataSeq = -1; // Unresolved + private string _rawSql; + private object _parameters; + + public Template(SqlBuilder builder, string sql, dynamic parameters) + { + _initParams = parameters; + _sql = sql; + _builder = builder; + } + + public string RawSql + { + get + { + ResolveSql(); + return _rawSql; + } + } + + public object Parameters + { + get + { + ResolveSql(); + return _parameters; + } + } + + private void ResolveSql() + { + if (_dataSeq != _builder.Sequence) + { + var p = new DynamicParameters(_initParams); + + _rawSql = _sql; + + foreach (var pair in _builder._data) + { + _rawSql = _rawSql.Replace("/**" + pair.Key + "**/", pair.Value.ResolveClauses(p)); + } + + _parameters = p; + + // replace all that is left with empty + _rawSql = _regex.Replace(_rawSql, ""); + + _dataSeq = _builder.Sequence; + } + } + } + + private class Clause + { + public string Sql { get; set; } + public object Parameters { get; set; } + public bool IsInclusive { get; set; } + } + + private class Clauses : List + { + private readonly string _joiner; + private readonly string _prefix; + private readonly string _postfix; + + public Clauses(string joiner, string prefix = "", string postfix = "") + { + _joiner = joiner; + _prefix = prefix; + _postfix = postfix; + } + + public string ResolveClauses(DynamicParameters p) + { + foreach (var item in this) + { + p.AddDynamicParams(item.Parameters); + } + + return this.Any(a => a.IsInclusive) + ? _prefix + + string.Join(_joiner, + this.Where(a => !a.IsInclusive) + .Select(c => c.Sql) + .Union(new[] + { + " ( " + + string.Join(" OR ", this.Where(a => a.IsInclusive).Select(c => c.Sql).ToArray()) + + " ) " + }).ToArray()) + _postfix + : _prefix + string.Join(_joiner, this.Select(c => c.Sql).ToArray()) + _postfix; + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapper.cs b/src/NzbDrone.Core/Datastore/TableMapper.cs new file mode 100644 index 000000000..0dbfca678 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/TableMapper.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace NzbDrone.Core.Datastore +{ + public class TableMapper + { + public TableMapper() + { + IgnoreList = new Dictionary>(); + LazyLoadList = new Dictionary>(); + TableMap = new Dictionary(); + } + + public Dictionary> IgnoreList { get; set; } + public Dictionary> LazyLoadList { get; set; } + public Dictionary TableMap { get; set; } + + public ColumnMapper Entity(string tableName) + where TEntity : ModelBase + { + var type = typeof(TEntity); + TableMap.Add(type, tableName); + + if (IgnoreList.TryGetValue(type, out var list)) + { + return new ColumnMapper(list, LazyLoadList[type]); + } + + IgnoreList[type] = new List(); + LazyLoadList[type] = new List(); + return new ColumnMapper(IgnoreList[type], LazyLoadList[type]); + } + + public List ExcludeProperties(Type x) + { + return IgnoreList.ContainsKey(x) ? IgnoreList[x] : new List(); + } + + public string TableNameMapping(Type x) + { + return TableMap.ContainsKey(x) ? TableMap[x] : null; + } + + public string SelectTemplate(Type x) + { + return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + } + + public string DeleteTemplate(Type x) + { + return $"DELETE FROM {TableMap[x]} /**where**/"; + } + + public string PageCountTemplate(Type x) + { + return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/"; + } + } + + public class LazyLoadedProperty + { + public PropertyInfo Property { get; set; } + public ILazyLoaded LazyLoad { get; set; } + } + + public class ColumnMapper + where T : ModelBase + { + private readonly List _ignoreList; + private readonly List _lazyLoadList; + + public ColumnMapper(List ignoreList, List lazyLoadList) + { + _ignoreList = ignoreList; + _lazyLoadList = lazyLoadList; + } + + public ColumnMapper AutoMapPropertiesWhere(Func predicate) + { + var properties = typeof(T).GetProperties(); + _ignoreList.AddRange(properties.Where(x => !predicate(x))); + + return this; + } + + public ColumnMapper RegisterModel() + { + return AutoMapPropertiesWhere(x => x.IsMappableProperty()); + } + + public ColumnMapper Ignore(Expression> property) + { + _ignoreList.Add(property.GetMemberName()); + return this; + } + + public ColumnMapper LazyLoad(Expression>> property, Func query, Func condition) + { + var lazyLoad = new LazyLoaded(query, condition); + + var item = new LazyLoadedProperty + { + Property = property.GetMemberName(), + LazyLoad = lazyLoad + }; + + _lazyLoadList.Add(item); + + return this; + } + + public ColumnMapper HasOne(Expression>> portalExpression, Func childIdSelector) + where TChild : ModelBase + { + return LazyLoad(portalExpression, + (db, parent) => + { + var id = childIdSelector(parent); + return db.Query(new SqlBuilder().Where(x => x.Id == id)).SingleOrDefault(); + }, + parent => childIdSelector(parent) > 0); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 7de8046fd..f8e773da0 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -1,18 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using Marr.Data; -using Marr.Data.Mapping; -using Marr.Data.QGen; -using NzbDrone.Common.Disk; +using Dapper; using NzbDrone.Common.Reflection; -using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Authentication; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.CustomFilters; using NzbDrone.Core.Datastore.Converters; -using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Extras.Lyrics; @@ -39,38 +34,47 @@ using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; +using static Dapper.SqlMapper; namespace NzbDrone.Core.Datastore { public static class TableMapping { - private static readonly FluentMappings Mapper = new FluentMappings(true); + static TableMapping() + { + Mapper = new TableMapper(); + } + + public static TableMapper Mapper { get; private set; } public static void Map() { RegisterMappers(); - Mapper.Entity().RegisterModel("Config"); + Mapper.Entity("Config").RegisterModel(); - Mapper.Entity().RegisterModel("RootFolders") + Mapper.Entity("RootFolders").RegisterModel() .Ignore(r => r.Accessible) .Ignore(r => r.FreeSpace) .Ignore(r => r.TotalSpace); - Mapper.Entity().RegisterModel("ScheduledTasks"); + Mapper.Entity("ScheduledTasks").RegisterModel(); - Mapper.Entity().RegisterDefinition("Indexers") + Mapper.Entity("Indexers").RegisterModel() + .Ignore(x => x.ImplementationName) .Ignore(i => i.Enable) .Ignore(i => i.Protocol) .Ignore(i => i.SupportsRss) .Ignore(i => i.SupportsSearch) .Ignore(d => d.Tags); - Mapper.Entity().RegisterDefinition("ImportLists") + Mapper.Entity("ImportLists").RegisterModel() + .Ignore(x => x.ImplementationName) .Ignore(i => i.Enable) .Ignore(i => i.ListType); - Mapper.Entity().RegisterDefinition("Notifications") + Mapper.Entity("Notifications").RegisterModel() + .Ignore(x => x.ImplementationName) .Ignore(i => i.SupportsOnGrab) .Ignore(i => i.SupportsOnReleaseImport) .Ignore(i => i.SupportsOnUpgrade) @@ -80,118 +84,110 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.SupportsOnImportFailure) .Ignore(i => i.SupportsOnTrackRetag); - Mapper.Entity().RegisterDefinition("Metadata") + Mapper.Entity("Metadata").RegisterModel() + .Ignore(x => x.ImplementationName) .Ignore(d => d.Tags); - Mapper.Entity().RegisterDefinition("DownloadClients") + Mapper.Entity("DownloadClients").RegisterModel() + .Ignore(x => x.ImplementationName) .Ignore(d => d.Protocol) .Ignore(d => d.Tags); - Mapper.Entity().RegisterModel("History") - .AutoMapChildModels(); + Mapper.Entity("History").RegisterModel(); - Mapper.Entity().RegisterModel("Artists") + Mapper.Entity("Artists") .Ignore(s => s.RootFolderPath) .Ignore(s => s.Name) .Ignore(s => s.ForeignArtistId) - .Relationship() .HasOne(a => a.Metadata, a => a.ArtistMetadataId) .HasOne(a => a.QualityProfile, a => a.QualityProfileId) .HasOne(s => s.MetadataProfile, s => s.MetadataProfileId) - .For(a => a.Albums) - .LazyLoad(condition: a => a.Id > 0, query: (db, a) => db.Query().Where(rg => rg.ArtistMetadataId == a.Id).ToList()); + .LazyLoad(a => a.Albums, (db, a) => db.Query(new SqlBuilder().Where(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0); - Mapper.Entity().RegisterModel("ArtistMetadata"); + Mapper.Entity("ArtistMetadata").RegisterModel(); - Mapper.Entity().RegisterModel("Albums") - .Ignore(r => r.ArtistId) - .Relationship() + Mapper.Entity("Albums").RegisterModel() + .Ignore(x => x.ArtistId) .HasOne(r => r.ArtistMetadata, r => r.ArtistMetadataId) - .For(rg => rg.AlbumReleases) - .LazyLoad(condition: rg => rg.Id > 0, query: (db, rg) => db.Query().Where(r => r.AlbumId == rg.Id).ToList()) - .For(rg => rg.Artist) - .LazyLoad(condition: rg => rg.ArtistMetadataId > 0, - query: (db, rg) => db.Query() - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .Where(a => a.ArtistMetadataId == rg.ArtistMetadataId).SingleOrDefault()); + .LazyLoad(a => a.AlbumReleases, (db, album) => db.Query(new SqlBuilder().Where(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0) + .LazyLoad(a => a.Artist, + (db, album) => ArtistRepository.Query(db, + new SqlBuilder() + .Join((a, m) => a.ArtistMetadataId == m.Id) + .Where(a => a.ArtistMetadataId == album.ArtistMetadataId)).SingleOrDefault(), + a => a.ArtistMetadataId > 0); - Mapper.Entity().RegisterModel("AlbumReleases") - .Relationship() + Mapper.Entity("AlbumReleases").RegisterModel() .HasOne(r => r.Album, r => r.AlbumId) - .For(r => r.Tracks) - .LazyLoad(condition: r => r.Id > 0, query: (db, r) => db.Query().Where(t => t.AlbumReleaseId == r.Id).ToList()); + .LazyLoad(x => x.Tracks, (db, release) => db.Query(new SqlBuilder().Where(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0); - Mapper.Entity().RegisterModel("Tracks") + Mapper.Entity("Tracks").RegisterModel() .Ignore(t => t.HasFile) .Ignore(t => t.AlbumId) - .Ignore(t => t.Album) - .Relationship() .HasOne(track => track.AlbumRelease, track => track.AlbumReleaseId) .HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId) - .For(track => track.TrackFile) - .LazyLoad(condition: track => track.TrackFileId > 0, - query: (db, track) => db.Query() - .Join(JoinType.Inner, t => t.Tracks, (t, x) => t.Id == x.TrackFileId) - .Join(JoinType.Inner, t => t.Album, (t, a) => t.AlbumId == a.Id) - .Join(JoinType.Inner, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId) - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .Where(t => t.Id == track.TrackFileId) - .SingleOrDefault()) - .For(t => t.Artist) - .LazyLoad(condition: t => t.AlbumReleaseId > 0, query: (db, t) => db.Query() - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .Join(JoinType.Inner, a => a.Albums, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Join(JoinType.Inner, a => a.AlbumReleases, (l, r) => l.Id == r.AlbumId) - .Where(r => r.Id == t.AlbumReleaseId) - .SingleOrDefault()); + .LazyLoad(t => t.TrackFile, + (db, track) => MediaFileRepository.Query(db, + new SqlBuilder() + .Join((l, r) => l.Id == r.TrackFileId) + .Join((l, r) => l.AlbumId == r.Id) + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Join((l, r) => l.ArtistMetadataId == r.Id) + .Where(t => t.Id == track.TrackFileId)).SingleOrDefault(), + t => t.TrackFileId > 0) + .LazyLoad(x => x.Artist, + (db, t) => ArtistRepository.Query(db, + new SqlBuilder() + .Join((a, m) => a.ArtistMetadataId == m.Id) + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Join((l, r) => l.Id == r.AlbumId) + .Where(r => r.Id == t.AlbumReleaseId)).SingleOrDefault(), + t => t.Id > 0); - Mapper.Entity().RegisterModel("TrackFiles") - .Relationship() + Mapper.Entity("TrackFiles").RegisterModel() .HasOne(f => f.Album, f => f.AlbumId) - .For(f => f.Tracks) - .LazyLoad(condition: f => f.Id > 0, query: (db, f) => db.Query() - .Where(x => x.TrackFileId == f.Id) - .ToList()) - .For(t => t.Artist) - .LazyLoad(condition: f => f.Id > 0, query: (db, f) => db.Query() - .Join(JoinType.Inner, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id) - .Join(JoinType.Inner, a => a.Albums, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Where(r => r.Id == f.AlbumId) - .SingleOrDefault()); + .LazyLoad(x => x.Tracks, (db, file) => db.Query(new SqlBuilder().Where(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0) + .LazyLoad(x => x.Artist, + (db, f) => ArtistRepository.Query(db, + new SqlBuilder() + .Join((a, m) => a.ArtistMetadataId == m.Id) + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Where(a => a.Id == f.AlbumId)).SingleOrDefault(), + t => t.Id > 0); - Mapper.Entity().RegisterModel("QualityDefinitions") + Mapper.Entity("QualityDefinitions").RegisterModel() .Ignore(d => d.GroupName) .Ignore(d => d.GroupWeight) .Ignore(d => d.Weight); - Mapper.Entity().RegisterModel("QualityProfiles"); - Mapper.Entity().RegisterModel("MetadataProfiles"); - Mapper.Entity().RegisterModel("Logs"); - Mapper.Entity().RegisterModel("NamingConfig"); - Mapper.Entity().MapResultSet(); - Mapper.Entity().RegisterModel("Blacklist"); - Mapper.Entity().RegisterModel("MetadataFiles"); - Mapper.Entity().RegisterModel("LyricFiles"); - Mapper.Entity().RegisterModel("ExtraFiles"); + Mapper.Entity("QualityProfiles").RegisterModel(); + Mapper.Entity("MetadataProfiles").RegisterModel(); + Mapper.Entity("Logs").RegisterModel(); + Mapper.Entity("NamingConfig").RegisterModel(); - Mapper.Entity().RegisterModel("PendingReleases") + Mapper.Entity("Blacklist").RegisterModel(); + Mapper.Entity("MetadataFiles").RegisterModel(); + Mapper.Entity("LyricFiles").RegisterModel(); + Mapper.Entity("ExtraFiles").RegisterModel(); + + Mapper.Entity("PendingReleases").RegisterModel() .Ignore(e => e.RemoteAlbum); - Mapper.Entity().RegisterModel("RemotePathMappings"); - Mapper.Entity().RegisterModel("Tags"); - Mapper.Entity().RegisterModel("ReleaseProfiles"); + Mapper.Entity("RemotePathMappings").RegisterModel(); + Mapper.Entity("Tags").RegisterModel(); + Mapper.Entity("ReleaseProfiles").RegisterModel(); - Mapper.Entity().RegisterModel("DelayProfiles"); - Mapper.Entity().RegisterModel("Users"); - Mapper.Entity().RegisterModel("Commands") - .Ignore(c => c.Message); + Mapper.Entity("DelayProfiles").RegisterModel(); + Mapper.Entity("Users").RegisterModel(); + Mapper.Entity("Commands").RegisterModel() + .Ignore(c => c.Message); - Mapper.Entity().RegisterModel("IndexerStatus"); - Mapper.Entity().RegisterModel("DownloadClientStatus"); - Mapper.Entity().RegisterModel("ImportListStatus"); + Mapper.Entity("IndexerStatus").RegisterModel(); + Mapper.Entity("DownloadClientStatus").RegisterModel(); + Mapper.Entity("ImportListStatus").RegisterModel(); - Mapper.Entity().RegisterModel("CustomFilters"); - Mapper.Entity().RegisterModel("ImportListExclusions"); + Mapper.Entity("CustomFilters").RegisterModel(); + Mapper.Entity("ImportListExclusions").RegisterModel(); } private static void RegisterMappers() @@ -199,40 +195,40 @@ namespace NzbDrone.Core.Datastore RegisterEmbeddedConverter(); RegisterProviderSettingConverter(); - MapRepository.Instance.RegisterTypeConverter(typeof(int), new Int32Converter()); - MapRepository.Instance.RegisterTypeConverter(typeof(double), new DoubleConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(DateTime), new UtcConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(bool), new BooleanIntConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new QualityIntConverter())); - MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); - MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List>), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new PrimaryAlbumTypeIntConverter())); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new SecondaryAlbumTypeIntConverter())); - MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new ReleaseStatusIntConverter())); - MapRepository.Instance.RegisterTypeConverter(typeof(ParsedAlbumInfo), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(ParsedTrackInfo), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(HashSet), new EmbeddedDocumentConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(Command), new CommandConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan), new TimeSpanConverter()); - MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan?), new TimeSpanConverter()); + SqlMapper.RemoveTypeMap(typeof(DateTime)); + SqlMapper.AddTypeHandler(new DapperUtcConverter()); + SqlMapper.AddTypeHandler(new DapperQualityIntConverter()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new QualityIntConverter())); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter(new QualityIntConverter())); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new PrimaryAlbumTypeIntConverter())); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new SecondaryAlbumTypeIntConverter())); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new ReleaseStatusIntConverter())); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new OsPathConverter()); + SqlMapper.RemoveTypeMap(typeof(Guid)); + SqlMapper.RemoveTypeMap(typeof(Guid?)); + SqlMapper.AddTypeHandler(new GuidConverter()); + SqlMapper.AddTypeHandler(new CommandConverter()); } private static void RegisterProviderSettingConverter() { - var settingTypes = typeof(IProviderConfig).Assembly.ImplementationsOf(); + var settingTypes = typeof(IProviderConfig).Assembly.ImplementationsOf() + .Where(x => !x.ContainsGenericParameters); var providerSettingConverter = new ProviderSettingConverter(); foreach (var embeddedType in settingTypes) { - MapRepository.Instance.RegisterTypeConverter(embeddedType, providerSettingConverter); + SqlMapper.AddTypeHandler(embeddedType, providerSettingConverter); } } @@ -240,16 +236,24 @@ namespace NzbDrone.Core.Datastore { var embeddedTypes = typeof(IEmbeddedDocument).Assembly.ImplementationsOf(); - var embeddedConvertor = new EmbeddedDocumentConverter(); + var embeddedConverterDefinition = typeof(EmbeddedDocumentConverter<>).GetGenericTypeDefinition(); var genericListDefinition = typeof(List<>).GetGenericTypeDefinition(); foreach (var embeddedType in embeddedTypes) { var embeddedListType = genericListDefinition.MakeGenericType(embeddedType); - MapRepository.Instance.RegisterTypeConverter(embeddedType, embeddedConvertor); - MapRepository.Instance.RegisterTypeConverter(embeddedListType, embeddedConvertor); + RegisterEmbeddedConverter(embeddedType, embeddedConverterDefinition); + RegisterEmbeddedConverter(embeddedListType, embeddedConverterDefinition); } } + + private static void RegisterEmbeddedConverter(Type embeddedType, Type embeddedConverterDefinition) + { + var embeddedConverterType = embeddedConverterDefinition.MakeGenericType(embeddedType); + var converter = (ITypeHandler)Activator.CreateInstance(embeddedConverterType); + + SqlMapper.AddTypeHandler(embeddedType, converter); + } } } diff --git a/src/NzbDrone.Core/Datastore/WhereBuilder.cs b/src/NzbDrone.Core/Datastore/WhereBuilder.cs new file mode 100644 index 000000000..4524ab289 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/WhereBuilder.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Dapper; + +namespace NzbDrone.Core.Datastore +{ + public class WhereBuilder : ExpressionVisitor + { + protected StringBuilder _sb; + + private const DbType EnumerableMultiParameter = (DbType)(-1); + private readonly string _paramNamePrefix; + private readonly bool _requireConcreteValue = false; + private int _paramCount = 0; + private bool _gotConcreteValue = false; + + public WhereBuilder(Expression filter, bool requireConcreteValue, int seq) + { + _paramNamePrefix = string.Format("Clause{0}", seq + 1); + _requireConcreteValue = requireConcreteValue; + _sb = new StringBuilder(); + + Parameters = new DynamicParameters(); + + if (filter != null) + { + Visit(filter); + } + } + + public DynamicParameters Parameters { get; private set; } + + private string AddParameter(object value, DbType? dbType = null) + { + _gotConcreteValue = true; + _paramCount++; + var name = _paramNamePrefix + "_P" + _paramCount; + Parameters.Add(name, value, dbType); + return '@' + name; + } + + protected override Expression VisitBinary(BinaryExpression expression) + { + _sb.Append("("); + + Visit(expression.Left); + + _sb.AppendFormat(" {0} ", Decode(expression)); + + Visit(expression.Right); + + _sb.Append(")"); + + return expression; + } + + protected override Expression VisitMethodCall(MethodCallExpression expression) + { + var method = expression.Method.Name; + + switch (expression.Method.Name) + { + case "Contains": + ParseContainsExpression(expression); + break; + + case "StartsWith": + ParseStartsWith(expression); + break; + + case "EndsWith": + ParseEndsWith(expression); + break; + + default: + var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method); + throw new NotImplementedException(msg); + } + + return expression; + } + + protected override Expression VisitMemberAccess(MemberExpression expression) + { + var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null; + var gotValue = TryGetRightValue(expression, out var value); + + // Only use the SQL condition if the expression didn't resolve to an actual value + if (tableName != null && !gotValue) + { + _sb.Append($"\"{tableName}\".\"{expression.Member.Name}\""); + } + else + { + if (value != null) + { + // string is IEnumerable but we don't want to pick up that case + var type = value.GetType(); + var typeInfo = type.GetTypeInfo(); + var isEnumerable = + type != typeof(string) && ( + typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || + (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>))); + + var paramName = isEnumerable ? AddParameter(value, EnumerableMultiParameter) : AddParameter(value); + _sb.Append(paramName); + } + else + { + _gotConcreteValue = true; + _sb.Append("NULL"); + } + } + + return expression; + } + + protected override Expression VisitConstant(ConstantExpression expression) + { + if (expression.Value != null) + { + var paramName = AddParameter(expression.Value); + _sb.Append(paramName); + } + else + { + _gotConcreteValue = true; + _sb.Append("NULL"); + } + + return expression; + } + + private bool TryGetConstantValue(Expression expression, out object result) + { + result = null; + + if (expression is ConstantExpression constExp) + { + result = constExp.Value; + return true; + } + + return false; + } + + private bool TryGetPropertyValue(MemberExpression expression, out object result) + { + result = null; + + if (expression.Expression is MemberExpression nested) + { + // Value is passed in as a property on a parent entity + var container = (nested.Expression as ConstantExpression)?.Value; + + if (container == null) + { + return false; + } + + var entity = GetFieldValue(container, nested.Member); + result = GetFieldValue(entity, expression.Member); + return true; + } + + return false; + } + + private bool TryGetVariableValue(MemberExpression expression, out object result) + { + result = null; + + // Value is passed in as a variable + if (expression.Expression is ConstantExpression nested) + { + result = GetFieldValue(nested.Value, expression.Member); + return true; + } + + return false; + } + + private bool TryGetRightValue(Expression expression, out object value) + { + value = null; + + if (TryGetConstantValue(expression, out value)) + { + return true; + } + + var memberExp = expression as MemberExpression; + + if (TryGetPropertyValue(memberExp, out value)) + { + return true; + } + + if (TryGetVariableValue(memberExp, out value)) + { + return true; + } + + return false; + } + + private object GetFieldValue(object entity, MemberInfo member) + { + if (member.MemberType == MemberTypes.Field) + { + return (member as FieldInfo).GetValue(entity); + } + + if (member.MemberType == MemberTypes.Property) + { + return (member as PropertyInfo).GetValue(entity); + } + + throw new ArgumentException(string.Format("WhereBuilder could not get the value for {0}.{1}.", entity.GetType().Name, member.Name)); + } + + private bool IsNullVariable(Expression expression) + { + if (expression.NodeType == ExpressionType.Constant && + TryGetConstantValue(expression, out var constResult) && + constResult == null) + { + return true; + } + + if (expression.NodeType == ExpressionType.MemberAccess && + expression is MemberExpression member && + ((TryGetPropertyValue(member, out var result) && result == null) || + (TryGetVariableValue(member, out result) && result == null))) + { + return true; + } + + return false; + } + + private string Decode(BinaryExpression expression) + { + if (IsNullVariable(expression.Right)) + { + switch (expression.NodeType) + { + case ExpressionType.Equal: return "IS"; + case ExpressionType.NotEqual: return "IS NOT"; + } + } + + switch (expression.NodeType) + { + case ExpressionType.AndAlso: return "AND"; + case ExpressionType.And: return "AND"; + case ExpressionType.Equal: return "="; + case ExpressionType.GreaterThan: return ">"; + case ExpressionType.GreaterThanOrEqual: return ">="; + case ExpressionType.LessThan: return "<"; + case ExpressionType.LessThanOrEqual: return "<="; + case ExpressionType.NotEqual: return "<>"; + case ExpressionType.OrElse: return "OR"; + case ExpressionType.Or: return "OR"; + default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString())); + } + } + + private void ParseContainsExpression(MethodCallExpression expression) + { + var list = expression.Object; + + if (list != null && + (list.Type == typeof(string) || + (list.Type == typeof(List) && !TryGetRightValue(list, out var _)))) + { + ParseStringContains(expression); + return; + } + + ParseEnumerableContains(expression); + } + + private void ParseEnumerableContains(MethodCallExpression body) + { + // Fish out the list and the item to compare + // It's in a different form for arrays and Lists + var list = body.Object; + Expression item; + + if (list != null) + { + // Generic collection + item = body.Arguments[0]; + } + else + { + // Static method + // Must be Enumerable.Contains(source, item) + if (body.Method.DeclaringType != typeof(Enumerable) || body.Arguments.Count != 2) + { + throw new NotSupportedException("Unexpected form of Enumerable.Contains"); + } + + list = body.Arguments[0]; + item = body.Arguments[1]; + } + + _sb.Append("("); + + Visit(item); + + _sb.Append(" IN "); + + // hardcode the integer list if it exists to bypass parameter limit + if (item.Type == typeof(int) && TryGetRightValue(list, out var value)) + { + var items = (IEnumerable)value; + _sb.Append("("); + _sb.Append(string.Join(", ", items)); + _sb.Append(")"); + + _gotConcreteValue = true; + } + else + { + Visit(list); + } + + _sb.Append(")"); + } + + private void ParseStringContains(MethodCallExpression body) + { + _sb.Append("("); + + Visit(body.Object); + + _sb.Append(" LIKE '%' || "); + + Visit(body.Arguments[0]); + + _sb.Append(" || '%')"); + } + + private void ParseStartsWith(MethodCallExpression body) + { + _sb.Append("("); + + Visit(body.Object); + + _sb.Append(" LIKE "); + + Visit(body.Arguments[0]); + + _sb.Append(" || '%')"); + } + + private void ParseEndsWith(MethodCallExpression body) + { + _sb.Append("("); + + Visit(body.Object); + + _sb.Append(" LIKE '%' || "); + + Visit(body.Arguments[0]); + + _sb.Append(")"); + } + + public override string ToString() + { + var sql = _sb.ToString(); + + if (_requireConcreteValue && !_gotConcreteValue) + { + var e = new InvalidOperationException("WhereBuilder requires a concrete condition"); + e.Data.Add("sql", sql); + throw e; + } + + return sql; + } + } +} diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs index ded73e2a6..8052ea01b 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseRepository.cs @@ -20,17 +20,17 @@ namespace NzbDrone.Core.Download.Pending public void DeleteByArtistId(int artistId) { - Delete(r => r.ArtistId == artistId); + Delete(artistId); } public List AllByArtistId(int artistId) { - return Query.Where(p => p.ArtistId == artistId); + return Query(p => p.ArtistId == artistId); } public List WithoutFallback() { - return Query.Where(p => p.Reason != PendingReleaseReason.Fallback); + return Query(p => p.Reason != PendingReleaseReason.Fallback); } } } diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index dd3c7ae90..1b3969d31 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -173,7 +173,7 @@ namespace NzbDrone.Core.Extras foreach (var trackFile in trackFiles) { var localTrackFile = trackFile; - trackFile.Tracks = new LazyList(tracks.Where(e => e.TrackFileId == localTrackFile.Id)); + trackFile.Tracks = tracks.Where(e => e.TrackFileId == localTrackFile.Id).ToList(); } return trackFiles; diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs index e124f390b..ff5113253 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs @@ -42,22 +42,22 @@ namespace NzbDrone.Core.Extras.Files public List GetFilesByArtist(int artistId) { - return Query.Where(c => c.ArtistId == artistId); + return Query(c => c.ArtistId == artistId); } public List GetFilesByAlbum(int artistId, int albumId) { - return Query.Where(c => c.ArtistId == artistId && c.AlbumId == albumId); + return Query(c => c.ArtistId == artistId && c.AlbumId == albumId); } public List GetFilesByTrackFile(int trackFileId) { - return Query.Where(c => c.TrackFileId == trackFileId); + return Query(c => c.TrackFileId == trackFileId); } public TExtraFile FindByPath(string path) { - return Query.Where(c => c.RelativePath == path).SingleOrDefault(); + return Query(c => c.RelativePath == path).SingleOrDefault(); } } } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 4126a8071..ba615b1a2 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; @@ -30,63 +30,72 @@ namespace NzbDrone.Core.History public History MostRecentForAlbum(int albumId) { - return Query.Where(h => h.AlbumId == albumId) - .OrderByDescending(h => h.Date) - .FirstOrDefault(); + return Query(h => h.AlbumId == albumId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(); } public History MostRecentForDownloadId(string downloadId) { - return Query.Where(h => h.DownloadId == downloadId) - .OrderByDescending(h => h.Date) - .FirstOrDefault(); + return Query(h => h.DownloadId == downloadId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(); } public List FindByDownloadId(string downloadId) { - return Query.Join(JoinType.Left, h => h.Artist, (h, a) => h.ArtistId == a.Id) - .Join(JoinType.Left, h => h.Album, (h, r) => h.AlbumId == r.Id) - .Where(h => h.DownloadId == downloadId); + return _database.QueryJoined( + Builder() + .Join((h, a) => h.ArtistId == a.Id) + .Join((h, a) => h.AlbumId == a.Id) + .Where(h => h.DownloadId == downloadId), + (history, artist, album) => + { + history.Artist = artist; + history.Album = album; + return history; + }).ToList(); } public List GetByArtist(int artistId, HistoryEventType? eventType) { - var query = Query.Where(h => h.ArtistId == artistId); + var builder = Builder().Where(h => h.ArtistId == artistId); if (eventType.HasValue) { - query.AndWhere(h => h.EventType == eventType); + builder.Where(h => h.EventType == eventType); } - query.OrderByDescending(h => h.Date); - - return query; + return Query(builder).OrderByDescending(h => h.Date).ToList(); } public List GetByAlbum(int albumId, HistoryEventType? eventType) { - var query = Query.Join(JoinType.Inner, h => h.Album, (h, e) => h.AlbumId == e.Id) - .Where(h => h.AlbumId == albumId); + var builder = Builder() + .Join((h, a) => h.AlbumId == a.Id) + .Where(h => h.AlbumId == albumId); if (eventType.HasValue) { - query.AndWhere(h => h.EventType == eventType); + builder.Where(h => h.EventType == eventType); } - query.OrderByDescending(h => h.Date); - - return query; + return _database.QueryJoined( + builder, + (history, album) => + { + history.Album = album; + return history; + }).OrderByDescending(h => h.Date).ToList(); } public List FindDownloadHistory(int idArtistId, QualityModel quality) { - return Query.Where(h => - h.ArtistId == idArtistId && - h.Quality == quality && - (h.EventType == HistoryEventType.Grabbed || - h.EventType == HistoryEventType.DownloadFailed || - h.EventType == HistoryEventType.TrackFileImported)) - .ToList(); + var allowed = new[] { HistoryEventType.Grabbed, HistoryEventType.DownloadFailed, HistoryEventType.TrackFileImported }; + + return Query(h => h.ArtistId == idArtistId && + h.Quality == quality && + allowed.Contains(h.EventType)); } public void DeleteForArtist(int artistId) @@ -94,27 +103,29 @@ namespace NzbDrone.Core.History Delete(c => c.ArtistId == artistId); } - protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) - { - var baseQuery = query.Join(JoinType.Inner, h => h.Artist, (h, a) => h.ArtistId == a.Id) - .Join(JoinType.Inner, h => h.Album, (h, r) => h.AlbumId == r.Id) - .Join(JoinType.Left, h => h.Track, (h, t) => h.TrackId == t.Id); - - return base.GetPagedQuery(baseQuery, pagingSpec); - } + protected override SqlBuilder PagedBuilder() => new SqlBuilder() + .Join((h, a) => h.ArtistId == a.Id) + .Join((h, a) => h.AlbumId == a.Id) + .LeftJoin((h, t) => h.TrackId == t.Id); + protected override IEnumerable PagedQuery(SqlBuilder builder) => + _database.QueryJoined(builder, (history, artist, album, track) => + { + history.Artist = artist; + history.Album = album; + history.Track = track; + return history; + }); public List Since(DateTime date, HistoryEventType? eventType) { - var query = Query.Where(h => h.Date >= date); + var builder = Builder().Where(x => x.Date >= date); if (eventType.HasValue) { - query.AndWhere(h => h.EventType == eventType); + builder.Where(h => h.EventType == eventType); } - query.OrderBy(h => h.Date); - - return query; + return Query(builder).OrderBy(h => h.Date).ToList(); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs index 099180b3b..1293d4f07 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,9 +14,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE RelativePath @@ -25,6 +26,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers OR RelativePath LIKE '/%' )"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs index a86b209e0..a1d9c56b7 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,12 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM NamingConfig + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM NamingConfig WHERE ID NOT IN ( SELECT ID FROM NamingConfig LIMIT 1)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs index 62f58b962..4460ac83c 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,12 +14,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM Users - WHERE ID NOT IN ( - SELECT ID FROM Users - LIMIT 1)"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM Users + WHERE ID NOT IN ( + SELECT ID FROM Users + LIMIT 1)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs index eb3073a1f..1e331c98b 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs @@ -1,4 +1,5 @@ -using System; +using System; +using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.Pending; @@ -15,18 +16,17 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - var twoWeeksAgo = DateTime.UtcNow.AddDays(-14); - - mapper.Delete(p => p.Added < twoWeeksAgo && - (p.Reason == PendingReleaseReason.DownloadClientUnavailable || - p.Reason == PendingReleaseReason.Fallback)); - - // mapper.AddParameter("twoWeeksAgo", $"{DateTime.UtcNow.AddDays(-14).ToString("s")}Z"); - - // mapper.ExecuteNonQuery(@"DELETE FROM PendingReleases - // WHERE Added < @twoWeeksAgo - // AND (Reason = 'DownloadClientUnavailable' OR Reason = 'Fallback')"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM PendingReleases + WHERE Added < @TwoWeeksAgo + AND REASON IN @Reasons", + new + { + TwoWeeksAgo = DateTime.UtcNow.AddDays(-14), + Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback } + }); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs index f7df1b567..3b06fdba4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -21,54 +22,58 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private void DeleteDuplicateArtistMetadata() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 1 GROUP BY ArtistId, Consumer HAVING COUNT(ArtistId) > 1 )"); + } } private void DeleteDuplicateAlbumMetadata() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles - WHERE Id IN ( + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles + WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 6 GROUP BY AlbumId, Consumer HAVING COUNT(AlbumId) > 1 )"); + } } private void DeleteDuplicateTrackMetadata() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles - WHERE Id IN ( + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles + WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 2 GROUP BY TrackFileId, Consumer HAVING COUNT(TrackFileId) > 1 )"); + } } private void DeleteDuplicateTrackImages() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles - WHERE Id IN ( + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles + WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type = 5 GROUP BY TrackFileId, Consumer HAVING COUNT(TrackFileId) > 1 )"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs index ce4893ff4..f80b93f07 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM Albums + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM Albums WHERE Id IN ( SELECT Albums.Id FROM Albums LEFT OUTER JOIN Artists ON Albums.ArtistMetadataId = Artists.ArtistMetadataId WHERE Artists.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs index f9992746c..52f674312 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,15 +14,16 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM ArtistMetadata + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ArtistMetadata WHERE Id IN ( SELECT ArtistMetadata.Id FROM ArtistMetadata LEFT OUTER JOIN Albums ON Albums.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN Tracks ON Tracks.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN Artists ON Artists.ArtistMetadataId = ArtistMetadata.Id WHERE Albums.Id IS NULL AND Tracks.Id IS NULL AND Artists.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs index 2def585c2..fa0a5f020 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM Blacklist + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM Blacklist WHERE Id IN ( SELECT Blacklist.Id FROM Blacklist LEFT OUTER JOIN Artists ON Blacklist.ArtistId = Artists.Id WHERE Artists.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs index 258a51ab9..91598ace5 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM DownloadClientStatus + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM DownloadClientStatus WHERE Id IN ( SELECT DownloadClientStatus.Id FROM DownloadClientStatus LEFT OUTER JOIN DownloadClients ON DownloadClientStatus.ProviderId = DownloadClients.Id WHERE DownloadClients.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs index ded0eae75..43b298605 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -19,26 +20,28 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private void CleanupOrphanedByArtist() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM History + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM History WHERE Id IN ( SELECT History.Id FROM History LEFT OUTER JOIN Artists ON History.ArtistId = Artists.Id WHERE Artists.Id IS NULL)"); + } } private void CleanupOrphanedByAlbum() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM History + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM History WHERE Id IN ( SELECT History.Id FROM History LEFT OUTER JOIN Albums ON History.AlbumId = Albums.Id WHERE Albums.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs index 9a267ee81..f40ec672f 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM ImportListStatus + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM ImportListStatus WHERE Id IN ( SELECT ImportListStatus.Id FROM ImportListStatus LEFT OUTER JOIN ImportLists ON ImportListStatus.ProviderId = ImportLists.Id WHERE ImportLists.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs index 4df6f502b..039db9b52 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM IndexerStatus + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM IndexerStatus WHERE Id IN ( SELECT IndexerStatus.Id FROM IndexerStatus LEFT OUTER JOIN Indexers ON IndexerStatus.ProviderId = Indexers.Id WHERE Indexers.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs index f83359ec1..2afd5feea 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -22,62 +23,67 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private void DeleteOrphanedByArtist() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles LEFT OUTER JOIN Artists ON MetadataFiles.ArtistId = Artists.Id WHERE Artists.Id IS NULL)"); + } } private void DeleteOrphanedByAlbum() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles LEFT OUTER JOIN Albums ON MetadataFiles.AlbumId = Albums.Id WHERE MetadataFiles.AlbumId > 0 AND Albums.Id IS NULL)"); + } } private void DeleteOrphanedByTrackFile() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT MetadataFiles.Id FROM MetadataFiles LEFT OUTER JOIN TrackFiles ON MetadataFiles.TrackFileId = TrackFiles.Id WHERE MetadataFiles.TrackFileId > 0 AND TrackFiles.Id IS NULL)"); + } } private void DeleteWhereAlbumIdIsZero() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type IN (4, 6) AND AlbumId = 0)"); + } } private void DeleteWhereTrackFileIsZero() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM MetadataFiles WHERE Id IN ( SELECT Id FROM MetadataFiles WHERE Type IN (2, 5) AND TrackFileId = 0)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs index bdc45d099..d1ce0fc3b 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using Dapper; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers { @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM PendingReleases + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM PendingReleases WHERE Id IN ( SELECT PendingReleases.Id FROM PendingReleases LEFT OUTER JOIN Artists ON PendingReleases.ArtistId = Artists.Id WHERE Artists.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs index 5c7c3d1fe..e2613f220 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM AlbumReleases + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM AlbumReleases WHERE Id IN ( SELECT AlbumReleases.Id FROM AlbumReleases LEFT OUTER JOIN Albums ON AlbumReleases.AlbumId = Albums.Id WHERE Albums.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs index 013df88b5..57657de83 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,10 +14,10 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - // Unlink where track no longer exists - mapper.ExecuteNonQuery(@"UPDATE TrackFiles + using (var mapper = _database.OpenConnection()) + { + // Unlink where track no longer exists + mapper.Execute(@"UPDATE TrackFiles SET AlbumId = 0 WHERE Id IN ( SELECT TrackFiles.Id FROM TrackFiles @@ -24,14 +25,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers ON TrackFiles.Id = Tracks.TrackFileId WHERE Tracks.Id IS NULL)"); - // Unlink Tracks where the Trackfiles entry no longer exists - mapper.ExecuteNonQuery(@"UPDATE Tracks + // Unlink Tracks where the Trackfiles entry no longer exists + mapper.Execute(@"UPDATE Tracks SET TrackFileId = 0 WHERE Id IN ( SELECT Tracks.Id FROM Tracks LEFT OUTER JOIN TrackFiles ON Tracks.TrackFileId = TrackFiles.Id WHERE TrackFiles.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs index 94b8e22df..ff2017c5c 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs @@ -1,3 +1,4 @@ +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -13,14 +14,15 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - mapper.ExecuteNonQuery(@"DELETE FROM Tracks + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"DELETE FROM Tracks WHERE Id IN ( SELECT Tracks.Id FROM Tracks LEFT OUTER JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id WHERE AlbumReleases.Id IS NULL)"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index ae7f1fdc6..9b993a26d 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; +using System.Data; using System.Linq; -using Marr.Data; -using NzbDrone.Common.Serializer; +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -17,24 +17,25 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - var mapper = _database.GetDataMapper(); - - var usedTags = new[] { "Artists", "Notifications", "DelayProfiles", "ReleaseProfiles" } + using (var mapper = _database.OpenConnection()) + { + var usedTags = new[] { "Artists", "Notifications", "DelayProfiles", "ReleaseProfiles" } .SelectMany(v => GetUsedTags(v, mapper)) - .Distinct() - .ToArray(); + .Distinct() + .ToArray(); - var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray()); + var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray()); - mapper.ExecuteNonQuery($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})"); + mapper.Execute($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})"); + } } - private int[] GetUsedTags(string table, IDataMapper mapper) + private int[] GetUsedTags(string table, IDbConnection mapper) { - return mapper.ExecuteReader($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'", reader => reader.GetString(0)) - .SelectMany(Json.Deserialize>) - .Distinct() - .ToArray(); + return mapper.Query>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'") + .SelectMany(x => x) + .Distinct() + .ToArray(); } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs index 1d5cdef35..82a7af7fa 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs @@ -1,4 +1,5 @@ using System; +using Dapper; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Datastore; @@ -23,12 +24,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers _logger.Debug("Not running scheduled task last execution cleanup during debug"); } - var mapper = _database.GetDataMapper(); - mapper.AddParameter("time", DateTime.UtcNow); - - mapper.ExecuteNonQuery(@"UPDATE ScheduledTasks - SET LastExecution = @time - WHERE LastExecution > @time"); + using (var mapper = _database.OpenConnection()) + { + mapper.Execute(@"UPDATE ScheduledTasks + SET LastExecution = @time + WHERE LastExecution > @time", + new { time = DateTime.UtcNow }); + } } } } diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs index 1ec78cfaa..8b055260c 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs @@ -20,12 +20,14 @@ namespace NzbDrone.Core.ImportLists.Exclusions public ImportListExclusion FindByForeignId(string foreignId) { - return Query.Where(m => m.ForeignId == foreignId).SingleOrDefault(); + return Query(m => m.ForeignId == foreignId).SingleOrDefault(); } public List FindByForeignId(List ids) { - return Query.Where($"[ForeignId] IN ('{string.Join("', '", ids)}')").ToList(); + // Using Enumerable.Contains forces the builder to create an 'IN' + // and not a string 'LIKE' expression + return Query(x => Enumerable.Contains(ids, x.ForeignId)); } } } diff --git a/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs index 8cad83c67..e2345641f 100644 --- a/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs +++ b/src/NzbDrone.Core/Indexers/Gazelle/GazelleSettings.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Gazelle public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs index e697e0dc9..4a589e5ba 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsSettings.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Indexers.IPTorrents public int MinimumSeeders { get; set; } [FieldDefinition(2)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(3, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs index 10b885429..c8df67ea1 100644 --- a/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/ITorrentIndexerSettings.cs @@ -4,6 +4,6 @@ namespace NzbDrone.Core.Indexers { int MinimumSeeders { get; set; } - SeedCriteriaSettings SeedCriteria { get; } + SeedCriteriaSettings SeedCriteria { get; set; } } } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs index 448f0fdab..a5f2b77bb 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaSettings.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Indexers.Nyaa public int MinimumSeeders { get; set; } [FieldDefinition(3)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(4, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs index 34fd23241..9cd1ddf9e 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgSettings.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.Rarbg public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Redacted/RedactedSettings.cs b/src/NzbDrone.Core/Indexers/Redacted/RedactedSettings.cs index 24a1f27ec..8ce825c2b 100644 --- a/src/NzbDrone.Core/Indexers/Redacted/RedactedSettings.cs +++ b/src/NzbDrone.Core/Indexers/Redacted/RedactedSettings.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Indexers.Redacted public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs index e0fd4f1ca..812320f5d 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerSettings.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Indexers.TorrentRss public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs index a3528a020..670d355e2 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.Indexers.Torrentleech public int MinimumSeeders { get; set; } [FieldDefinition(3)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(4, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs index fe2ec3a95..227bedd06 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabSettings.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Torznab public int MinimumSeeders { get; set; } [FieldDefinition(7)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); public override NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs index ef681924e..668c07306 100644 --- a/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs +++ b/src/NzbDrone.Core/Indexers/Waffles/WafflesSettings.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Indexers.Waffles public int MinimumSeeders { get; set; } [FieldDefinition(4)] - public SeedCriteriaSettings SeedCriteria { get; } = new SeedCriteriaSettings(); + public SeedCriteriaSettings SeedCriteria { get; set; } = new SeedCriteriaSettings(); [FieldDefinition(5, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)] public int? EarlyReleaseLimit { get; set; } diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index 47e27a4e4..35421a90d 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -4,9 +4,11 @@ using NLog; using NLog.Config; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Sentry; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Instrumentation @@ -47,6 +49,10 @@ namespace NzbDrone.Core.Instrumentation SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off); SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off); SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off); + SetLogRotation(); + + //Log Sql + SqlBuilderExtensions.LogSql = _configFileProvider.LogSql; //Sentry ReconfigureSentry(); @@ -77,6 +83,14 @@ namespace NzbDrone.Core.Instrumentation } } + private void SetLogRotation() + { + foreach (var target in LogManager.Configuration.AllTargets.OfType()) + { + target.MaxArchiveFiles = _configFileProvider.LogRotate; + } + } + private void ReconfigureSentry() { var sentryTarget = LogManager.Configuration.AllTargets.OfType().FirstOrDefault(); diff --git a/src/NzbDrone.Core/Jobs/ScheduledTaskRepository.cs b/src/NzbDrone.Core/Jobs/ScheduledTaskRepository.cs index 4b46b2efc..f409b03a7 100644 --- a/src/NzbDrone.Core/Jobs/ScheduledTaskRepository.cs +++ b/src/NzbDrone.Core/Jobs/ScheduledTaskRepository.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Jobs public ScheduledTask GetDefinition(Type type) { - return Query.Where(c => c.TypeName == type.FullName).Single(); + return Query(c => c.TypeName == type.FullName).Single(); } public void SetLastExecutionTime(int id, DateTime executionTime, DateTime startTime) diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 6af6769e4..58796f013 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -3,6 +3,8 @@ net462;netcoreapp3.1 + + @@ -21,7 +23,6 @@ - diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 982e1918d..5148be7c4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Common; -using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; @@ -32,83 +31,129 @@ namespace NzbDrone.Core.MediaFiles // always join with all the other good stuff // needed more often than not so better to load it all now - protected override QueryBuilder Query => - DataMapper.Query() - .Join(JoinType.Left, t => t.Tracks, (t, x) => t.Id == x.TrackFileId) - .Join(JoinType.Left, t => t.Album, (t, a) => t.AlbumId == a.Id) - .Join(JoinType.Left, t => t.Artist, (t, a) => t.Album.Value.ArtistMetadataId == a.ArtistMetadataId) - .Join(JoinType.Left, a => a.Metadata, (a, m) => a.ArtistMetadataId == m.Id); + protected override SqlBuilder Builder() => new SqlBuilder() + .LeftJoin((t, x) => t.Id == x.TrackFileId) + .LeftJoin((t, a) => t.AlbumId == a.Id) + .LeftJoin((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .LeftJoin((a, m) => a.ArtistMetadataId == m.Id); + + protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); + + public static IEnumerable Query(IDatabase database, SqlBuilder builder) + { + var fileDictionary = new Dictionary(); + + _ = database.QueryJoined(builder, (file, track, album, artist, metadata) => Map(fileDictionary, file, track, album, artist, metadata)); + + return fileDictionary.Values; + } + + private static TrackFile Map(Dictionary dict, TrackFile file, Track track, Album album, Artist artist, ArtistMetadata metadata) + { + if (!dict.TryGetValue(file.Id, out var entry)) + { + if (artist != null) + { + artist.Metadata = metadata; + } + + entry = file; + entry.Tracks = new List(); + entry.Album = album; + entry.Artist = artist; + dict.Add(entry.Id, entry); + } + + if (track != null) + { + entry.Tracks.Value.Add(track); + } + + return entry; + } public List GetFilesByArtist(int artistId) { - return Query - .Join(JoinType.Inner, t => t.AlbumRelease, (t, r) => t.AlbumReleaseId == r.Id) - .Where(r => r.Monitored == true) - .AndWhere(t => t.Artist.Value.Id == artistId) - .ToList(); + return Query(Builder().LeftJoin((t, r) => t.AlbumReleaseId == r.Id) + .Where(r => r.Monitored == true) + .Where(a => a.Id == artistId)); } public List GetFilesByAlbum(int albumId) { - return Query - .Join(JoinType.Inner, t => t.AlbumRelease, (t, r) => t.AlbumReleaseId == r.Id) - .Where(r => r.Monitored == true) - .AndWhere(f => f.AlbumId == albumId) - .ToList(); + return Query(Builder().LeftJoin((t, r) => t.AlbumReleaseId == r.Id) + .Where(r => r.Monitored == true) + .Where(f => f.AlbumId == albumId)); } public List GetUnmappedFiles() { - var query = "SELECT TrackFiles.* " + - "FROM TrackFiles " + - "LEFT JOIN Tracks ON Tracks.TrackFileId = TrackFiles.Id " + - "WHERE Tracks.Id IS NULL "; - - return DataMapper.Query().QueryText(query).ToList(); + //x.Id == null is converted to SQL, so warning incorrect +#pragma warning disable CS0472 + return _database.Query(new SqlBuilder().Select(typeof(TrackFile)) + .LeftJoin((f, t) => f.Id == t.TrackFileId) + .Where(t => t.Id == null)).ToList(); +#pragma warning restore CS0472 } public void DeleteFilesByAlbum(int albumId) { - var ids = DataMapper.Query().Where(x => x.AlbumId == albumId); - DeleteMany(ids); + Delete(x => x.AlbumId == albumId); } public void UnlinkFilesByAlbum(int albumId) { - var files = DataMapper.Query().Where(x => x.AlbumId == albumId).ToList(); + var files = Query(x => x.AlbumId == albumId); files.ForEach(x => x.AlbumId = 0); SetFields(files, f => f.AlbumId); } public List GetFilesByRelease(int releaseId) { - return Query - .Where(x => x.AlbumReleaseId == releaseId) - .ToList(); + return Query(Builder().Where(x => x.AlbumReleaseId == releaseId)); } public List GetFilesWithBasePath(string path) { // ensure path ends with a single trailing path separator to avoid matching partial paths var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return DataMapper.Query() - .Where(x => x.Path.StartsWith(safePath)) - .ToList(); + return _database.Query(new SqlBuilder().Where(x => x.Path.StartsWith(safePath))).ToList(); } public TrackFile GetFileWithPath(string path) { - return Query.Where(x => x.Path == path).SingleOrDefault(); + return Query(x => x.Path == path).SingleOrDefault(); } public List GetFileWithPath(List paths) { // use more limited join for speed - var all = DataMapper.Query() - .Join(JoinType.Left, t => t.Tracks, (t, x) => t.Id == x.TrackFileId) - .ToList(); + var builder = new SqlBuilder() + .LeftJoin((f, t) => f.Id == t.TrackFileId); + + var dict = new Dictionary(); + _ = _database.QueryJoined(builder, (file, track) => MapTrack(dict, file, track)).ToList(); + var all = dict.Values.ToList(); + var joined = all.Join(paths, x => x.Path, x => x, (file, path) => file, PathEqualityComparer.Instance).ToList(); return joined; } + + private TrackFile MapTrack(Dictionary dict, TrackFile file, Track track) + { + if (!dict.TryGetValue(file.Id, out var entry)) + { + entry = file; + entry.Tracks = new List(); + dict.Add(entry.Id, entry); + } + + if (track != null) + { + entry.Tracks.Value.Add(track); + } + + return entry; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index ff4188497..f7beb7c2d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -122,6 +122,7 @@ namespace NzbDrone.Core.MediaFiles (f, af) => new { DiskFile = f, DbFile = af }, PathEqualityComparer.Instance) .ToList(); + _logger.Trace($"Matched paths for {combined.Count} files"); List unwanted = null; if (filter == FilterFilesType.Known) diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index d606a3273..182dd5a3e 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Marr.Data; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs b/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs index b873d28aa..7bb1416cb 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Data.SQLite; +using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -10,22 +10,16 @@ namespace NzbDrone.Core.Messaging.Commands { void Trim(); void OrphanStarted(); - List FindCommands(string name); - List FindQueuedOrStarted(string name); List Queued(); - List Started(); void Start(CommandModel command); void End(CommandModel command); } public class CommandRepository : BasicRepository, ICommandRepository { - private readonly IMainDatabase _database; - public CommandRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { - _database = database; } public void Trim() @@ -37,37 +31,23 @@ namespace NzbDrone.Core.Messaging.Commands public void OrphanStarted() { - var mapper = _database.GetDataMapper(); + var sql = @"UPDATE Commands SET Status = @Orphaned, EndedAt = @Ended WHERE Status = @Started"; + var args = new + { + Orphaned = (int)CommandStatus.Orphaned, + Started = (int)CommandStatus.Started, + Ended = DateTime.UtcNow + }; - mapper.Parameters.Add(new SQLiteParameter("@orphaned", (int)CommandStatus.Orphaned)); - mapper.Parameters.Add(new SQLiteParameter("@started", (int)CommandStatus.Started)); - mapper.Parameters.Add(new SQLiteParameter("@ended", DateTime.UtcNow)); - - mapper.ExecuteNonQuery(@"UPDATE Commands - SET Status = @orphaned, EndedAt = @ended - WHERE Status = @started"); - } - - public List FindCommands(string name) - { - return Query.Where(c => c.Name == name).ToList(); - } - - public List FindQueuedOrStarted(string name) - { - return Query.Where(c => c.Name == name) - .AndWhere("[Status] IN (0,1)") - .ToList(); + using (var conn = _database.OpenConnection()) + { + conn.Execute(sql, args); + } } public List Queued() { - return Query.Where(c => c.Status == CommandStatus.Queued); - } - - public List Started() - { - return Query.Where(c => c.Status == CommandStatus.Started); + return Query(c => c.Status == CommandStatus.Queued); } public void Start(CommandModel command) diff --git a/src/NzbDrone.Core/Music/Model/Album.cs b/src/NzbDrone.Core/Music/Model/Album.cs index a09314c5e..961336ddb 100644 --- a/src/NzbDrone.Core/Music/Model/Album.cs +++ b/src/NzbDrone.Core/Music/Model/Album.cs @@ -2,8 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using Equ; -using Marr.Data; +using Newtonsoft.Json; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { @@ -58,6 +59,7 @@ namespace NzbDrone.Core.Music //compatibility properties with old version of Album [MemberwiseEqualityIgnore] + [JsonIgnore] public int ArtistId { get { return Artist?.Value?.Id ?? 0; } set { Artist.Value.Id = value; } diff --git a/src/NzbDrone.Core/Music/Model/Artist.cs b/src/NzbDrone.Core/Music/Model/Artist.cs index f2c1c90ae..8689d9476 100644 --- a/src/NzbDrone.Core/Music/Model/Artist.cs +++ b/src/NzbDrone.Core/Music/Model/Artist.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using Equ; -using Marr.Data; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Qualities; diff --git a/src/NzbDrone.Core/Music/Model/Release.cs b/src/NzbDrone.Core/Music/Model/Release.cs index 3639b62ed..1eea6f810 100644 --- a/src/NzbDrone.Core/Music/Model/Release.cs +++ b/src/NzbDrone.Core/Music/Model/Release.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using Equ; -using Marr.Data; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { diff --git a/src/NzbDrone.Core/Music/Model/Track.cs b/src/NzbDrone.Core/Music/Model/Track.cs index c83f654ea..9c1aed1b1 100644 --- a/src/NzbDrone.Core/Music/Model/Track.cs +++ b/src/NzbDrone.Core/Music/Model/Track.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Equ; -using Marr.Data; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Music diff --git a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs b/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs index 900255e31..e4d4b6449 100644 --- a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; @@ -32,265 +32,78 @@ namespace NzbDrone.Core.Music public class AlbumRepository : BasicRepository, IAlbumRepository { - private readonly IMainDatabase _database; - public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { - _database = database; } public List GetAlbums(int artistId) { - return Query.Join(JoinType.Inner, album => album.Artist, (l, r) => l.ArtistMetadataId == r.ArtistMetadataId) - .Where(a => a.Id == artistId).ToList(); + return Query(Builder().Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId).Where(a => a.Id == artistId)); } public List GetLastAlbums(IEnumerable artistMetadataIds) { - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "WHERE Albums.ArtistMetadataId IN ({0}) " + - "AND Albums.ReleaseDate < datetime('now') " + - "GROUP BY Albums.ArtistMetadataId " + - "HAVING Albums.ReleaseDate = MAX(Albums.ReleaseDate)", - string.Join(", ", artistMetadataIds)); - - return Query.QueryText(query); + var now = DateTime.UtcNow; + return Query(Builder().Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate < now) + .GroupBy(x => x.ArtistMetadataId) + .Having("Albums.ReleaseDate = MAX(Albums.ReleaseDate)")); } public List GetNextAlbums(IEnumerable artistMetadataIds) { - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "WHERE Albums.ArtistMetadataId IN ({0}) " + - "AND Albums.ReleaseDate > datetime('now') " + - "GROUP BY Albums.ArtistMetadataId " + - "HAVING Albums.ReleaseDate = MIN(Albums.ReleaseDate)", - string.Join(", ", artistMetadataIds)); - - return Query.QueryText(query); + var now = DateTime.UtcNow; + return Query(Builder().Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate > now) + .GroupBy(x => x.ArtistMetadataId) + .Having("Albums.ReleaseDate = MIN(Albums.ReleaseDate)")); } public List GetAlbumsByArtistMetadataId(int artistMetadataId) { - return Query.Where(s => s.ArtistMetadataId == artistMetadataId); + return Query(s => s.ArtistMetadataId == artistMetadataId); } public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) { - return Query - .Where(a => a.ArtistMetadataId == artistMetadataId) - .OrWhere($"[ForeignAlbumId] IN ('{string.Join("', '", foreignIds)}')") - .ToList(); + return Query(a => a.ArtistMetadataId == artistMetadataId || foreignIds.Contains(a.ForeignAlbumId)); } public Album FindById(string foreignAlbumId) { - return Query.Where(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault(); + return Query(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault(); } + //x.Id == null is converted to SQL, so warning incorrect +#pragma warning disable CS0472 + private SqlBuilder AlbumsWithoutFilesBuilder(DateTime currentTime) => Builder() + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Join((a, r) => a.Id == r.AlbumId) + .Join((r, t) => r.Id == t.AlbumReleaseId) + .LeftJoin((t, f) => t.TrackFileId == f.Id) + .Where(f => f.Id == null) + .Where(r => r.Monitored == true) + .Where(a => a.ReleaseDate <= currentTime) + .GroupBy(a => a.Id); +#pragma warning restore CS0472 + public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) { var currentTime = DateTime.UtcNow; - //pagingSpec.TotalRecords = GetMissingAlbumsQuery(pagingSpec, currentTime).GetRowCount(); Cant Use GetRowCount with a Manual Query - pagingSpec.TotalRecords = GetMissingAlbumsQueryCount(pagingSpec, currentTime); - pagingSpec.Records = GetMissingAlbumsQuery(pagingSpec, currentTime).ToList(); + pagingSpec.Records = GetPagedRecords(AlbumsWithoutFilesBuilder(currentTime), pagingSpec, PagedQuery); + pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWithoutFilesBuilder(currentTime).SelectCountDistinct(x => x.Id), pagingSpec); return pagingSpec; } - public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff) - { - pagingSpec.TotalRecords = GetCutOffAlbumsQueryCount(pagingSpec, qualitiesBelowCutoff); - pagingSpec.Records = GetCutOffAlbumsQuery(pagingSpec, qualitiesBelowCutoff).ToList(); - - return pagingSpec; - } - - public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) - { - var query = Query.Join(JoinType.Inner, rg => rg.Artist, (rg, a) => rg.ArtistMetadataId == a.ArtistMetadataId) - .Where(rg => rg.ReleaseDate >= startDate) - .AndWhere(rg => rg.ReleaseDate <= endDate); - - if (!includeUnmonitored) - { - query.AndWhere(e => e.Monitored) - .AndWhere(e => e.Artist.Value.Monitored); - } - - return query.ToList(); - } - - public List ArtistAlbumsBetweenDates(Artist artist, DateTime startDate, DateTime endDate, bool includeUnmonitored) - { - var query = Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistMetadataId == s.ArtistMetadataId) - .Where(e => e.ReleaseDate >= startDate) - .AndWhere(e => e.ReleaseDate <= endDate) - .AndWhere(e => e.ArtistMetadataId == artist.ArtistMetadataId); - - if (!includeUnmonitored) - { - query.AndWhere(e => e.Monitored) - .AndWhere(e => e.Artist.Value.Monitored); - } - - return query.ToList(); - } - - private QueryBuilder GetMissingAlbumsQuery(PagingSpec pagingSpec, DateTime currentTime) - { - string sortKey; - string monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; - - if (pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True")) - { - monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; - } - - if (pagingSpec.SortKey == "releaseDate") - { - sortKey = "Albums." + pagingSpec.SortKey; - } - else if (pagingSpec.SortKey == "artist.sortName") - { - sortKey = "Artists." + pagingSpec.SortKey.Split('.').Last(); - } - else if (pagingSpec.SortKey == "albumTitle") - { - sortKey = "Albums.title"; - } - else - { - sortKey = "Albums.releaseDate"; - } - - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN Artists ON Albums.ArtistMetadataId = Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "LEFT OUTER JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE TrackFiles.Id IS NULL " + - "AND AlbumReleases.Monitored = 1 " + - "AND ({0}) AND {1} " + - "GROUP BY Albums.Id " + - " ORDER BY {2} {3} LIMIT {4} OFFSET {5}", - monitored, - BuildReleaseDateCutoffWhereClause(currentTime), - sortKey, - pagingSpec.ToSortDirection(), - pagingSpec.PageSize, - pagingSpec.PagingOffset()); - - return Query.QueryText(query); - } - - private int GetMissingAlbumsQueryCount(PagingSpec pagingSpec, DateTime currentTime) - { - var monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; - - if (pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True")) - { - monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; - } - - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN Artists ON Albums.ArtistMetadataId = Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "LEFT OUTER JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE TrackFiles.Id IS NULL " + - "AND AlbumReleases.Monitored = 1 " + - "AND ({0}) AND {1} " + - "GROUP BY Albums.Id ", - monitored, - BuildReleaseDateCutoffWhereClause(currentTime)); - - return Query.QueryText(query).Count(); - } - - private string BuildReleaseDateCutoffWhereClause(DateTime currentTime) - { - return string.Format("datetime(strftime('%s', Albums.[ReleaseDate]), 'unixepoch') <= '{0}'", - currentTime.ToString("yyyy-MM-dd HH:mm:ss")); - } - - private QueryBuilder GetCutOffAlbumsQuery(PagingSpec pagingSpec, List qualitiesBelowCutoff) - { - string sortKey; - string monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; - - if (pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True")) - { - monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; - } - - if (pagingSpec.SortKey == "releaseDate") - { - sortKey = "Albums." + pagingSpec.SortKey; - } - else if (pagingSpec.SortKey == "artist.sortName") - { - sortKey = "Artists." + pagingSpec.SortKey.Split('.').Last(); - } - else if (pagingSpec.SortKey == "albumTitle") - { - sortKey = "Albums.title"; - } - else - { - sortKey = "Albums.releaseDate"; - } - - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN Artists on Albums.ArtistMetadataId == Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE {0} " + - "AND AlbumReleases.Monitored = 1 " + - "GROUP BY Albums.Id " + - "HAVING {1} " + - "ORDER BY {2} {3} LIMIT {4} OFFSET {5}", - monitored, - BuildQualityCutoffWhereClause(qualitiesBelowCutoff), - sortKey, - pagingSpec.ToSortDirection(), - pagingSpec.PageSize, - pagingSpec.PagingOffset()); - - return Query.QueryText(query); - } - - private int GetCutOffAlbumsQueryCount(PagingSpec pagingSpec, List qualitiesBelowCutoff) - { - var monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; - - if (pagingSpec.FilterExpressions.FirstOrDefault().ToString().Contains("True")) - { - monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; - } - - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN Artists on Albums.ArtistMetadataId == Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE {0} " + - "AND AlbumReleases.Monitored = 1 " + - "GROUP BY Albums.Id " + - "HAVING {1}", - monitored, - BuildQualityCutoffWhereClause(qualitiesBelowCutoff)); - - return Query.QueryText(query).Count(); - } + private SqlBuilder AlbumsWhereCutoffUnmetBuilder(List qualitiesBelowCutoff) => Builder() + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Join((a, r) => a.Id == r.AlbumId) + .Join((r, t) => r.Id == t.AlbumReleaseId) + .LeftJoin((t, f) => t.TrackFileId == f.Id) + .Where(r => r.Monitored == true) + .GroupBy(a => a.Id) + .Having(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)); private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) { @@ -307,6 +120,46 @@ namespace NzbDrone.Core.Music return string.Format("({0})", string.Join(" OR ", clauses)); } + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff) + { + pagingSpec.Records = GetPagedRecords(AlbumsWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery); + + var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Album))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)"; + pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Album)), pagingSpec, countTemplate); + + return pagingSpec; + } + + public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) + { + var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && rg.ReleaseDate <= endDate); + + if (!includeUnmonitored) + { + builder = builder.Where(e => e.Monitored == true) + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Where(e => e.Monitored == true); + } + + return Query(builder); + } + + public List ArtistAlbumsBetweenDates(Artist artist, DateTime startDate, DateTime endDate, bool includeUnmonitored) + { + var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && + rg.ReleaseDate <= endDate && + rg.ArtistMetadataId == artist.ArtistMetadataId); + + if (!includeUnmonitored) + { + builder = builder.Where(e => e.Monitored == true) + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Where(e => e.Monitored == true); + } + + return Query(builder); + } + public void SetMonitoredFlat(Album album, bool monitored) { album.Monitored = monitored; @@ -315,15 +168,8 @@ namespace NzbDrone.Core.Music public void SetMonitored(IEnumerable ids, bool monitored) { - var mapper = _database.GetDataMapper(); - - mapper.AddParameter("monitored", monitored); - - var sql = "UPDATE Albums " + - "SET Monitored = @monitored " + - $"WHERE Id IN ({string.Join(", ", ids)})"; - - mapper.ExecuteNonQuery(sql); + var albums = ids.Select(x => new Album { Id = x, Monitored = monitored }).ToList(); + SetFields(albums, p => p.Monitored); } public Album FindByTitle(int artistMetadataId, string title) @@ -335,45 +181,32 @@ namespace NzbDrone.Core.Music cleanTitle = title; } - return Query.Where(s => s.CleanTitle == cleanTitle || s.Title == title) - .AndWhere(s => s.ArtistMetadataId == artistMetadataId) - .ExclusiveOrDefault(); + return Query(s => (s.CleanTitle == cleanTitle || s.Title == title) && s.ArtistMetadataId == artistMetadataId) + .ExclusiveOrDefault(); } public Album FindAlbumByRelease(string albumReleaseId) { - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId = Albums.Id " + - "WHERE AlbumReleases.ForeignReleaseId = '{0}'", - albumReleaseId); - return Query.QueryText(query).FirstOrDefault(); + return Query(Builder().Join((a, r) => a.Id == r.AlbumId) + .Where(x => x.ForeignReleaseId == albumReleaseId)).FirstOrDefault(); } public Album FindAlbumByTrack(int trackId) { - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId = Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId = AlbumReleases.Id " + - "WHERE Tracks.Id = {0}", - trackId); - return Query.QueryText(query).FirstOrDefault(); + return Query(Builder().Join((a, r) => a.Id == r.AlbumId) + .Join((r, t) => r.Id == t.AlbumReleaseId) + .Where(x => x.Id == trackId)).FirstOrDefault(); } public List GetArtistAlbumsWithFiles(Artist artist) { - string query = string.Format("SELECT Albums.* " + - "FROM Albums " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE Albums.ArtistMetadataId == {0} " + - "AND AlbumReleases.Monitored = 1 " + - "GROUP BY Albums.Id ", - artist.ArtistMetadataId); - - return Query.QueryText(query).ToList(); + var id = artist.ArtistMetadataId; + return Query(Builder().Join((a, r) => a.Id == r.AlbumId) + .Join((r, t) => r.Id == t.AlbumReleaseId) + .Join((t, f) => t.TrackFileId == f.Id) + .Where(x => x.ArtistMetadataId == id) + .Where(r => r.Monitored == true) + .GroupBy(x => x.Id)); } } } diff --git a/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs b/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs index bf83e5145..df8a39a06 100644 --- a/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/ArtistMetadataRepository.cs @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Music public List FindById(List foreignIds) { - return Query.Where($"[ForeignArtistId] IN ('{string.Join("','", foreignIds)}')").ToList(); + return Query(x => Enumerable.Contains(foreignIds, x.ForeignArtistId)); } public bool UpsertMany(List data) diff --git a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs b/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs index 70df40d08..0c7c121da 100644 --- a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs @@ -1,5 +1,6 @@ +using System.Collections.Generic; using System.Linq; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -12,32 +13,43 @@ namespace NzbDrone.Core.Music Artist FindByName(string cleanName); Artist FindById(string foreignArtistId); Artist GetArtistByMetadataId(int artistMetadataId); + List GetArtistByMetadataId(IEnumerable artistMetadataId); } public class ArtistRepository : BasicRepository, IArtistRepository { - public ArtistRepository(IMainDatabase database, IEventAggregator eventAggregator) + public ArtistRepository(IMainDatabase database, + IEventAggregator eventAggregator) : base(database, eventAggregator) { } - // Always explicitly join with ArtistMetadata to populate Metadata without repeated LazyLoading - protected override QueryBuilder Query => DataMapper.Query().Join(JoinType.Inner, a => a.Metadata, (l, r) => l.ArtistMetadataId == r.Id); + protected override SqlBuilder Builder() => new SqlBuilder() + .Join((a, m) => a.ArtistMetadataId == m.Id); + + protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); + + public static IEnumerable Query(IDatabase database, SqlBuilder builder) + { + return database.QueryJoined(builder, (artist, metadata) => + { + artist.Metadata = metadata; + return artist; + }); + } public bool ArtistPathExists(string path) { - return Query.Where(c => c.Path == path).Any(); + return Query(c => c.Path == path).Any(); } public Artist FindById(string foreignArtistId) { - var artist = Query.Where(m => m.ForeignArtistId == foreignArtistId).SingleOrDefault(); + var artist = Query(Builder().Where(m => m.ForeignArtistId == foreignArtistId)).SingleOrDefault(); if (artist == null) { - var id = "\"" + foreignArtistId + "\""; - artist = Query.Where(x => x.OldForeignArtistIds.Contains(id)) - .SingleOrDefault(); + artist = Query(Builder().Where(x => x.OldForeignArtistIds.Contains(foreignArtistId))).SingleOrDefault(); } return artist; @@ -47,12 +59,17 @@ namespace NzbDrone.Core.Music { cleanName = cleanName.ToLowerInvariant(); - return Query.Where(s => s.CleanName == cleanName).ExclusiveOrDefault(); + return Query(s => s.CleanName == cleanName).ExclusiveOrDefault(); } public Artist GetArtistByMetadataId(int artistMetadataId) { - return Query.Where(s => s.ArtistMetadataId == artistMetadataId).SingleOrDefault(); + return Query(s => s.ArtistMetadataId == artistMetadataId).SingleOrDefault(); + } + + public List GetArtistByMetadataId(IEnumerable artistMetadataIds) + { + return Query(s => artistMetadataIds.Contains(s.ArtistMetadataId)); } } } diff --git a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs b/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs index 6ab0a3f0e..f59d678bf 100644 --- a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Marr.Data.QGen; +using Dapper; using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -25,15 +25,12 @@ namespace NzbDrone.Core.Music public AlbumRelease FindByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false) { - var release = Query - .Where(x => x.ForeignReleaseId == foreignReleaseId) - .SingleOrDefault(); + var release = Query(x => x.ForeignReleaseId == foreignReleaseId).SingleOrDefault(); if (release == null && checkRedirect) { var id = "\"" + foreignReleaseId + "\""; - release = Query.Where(x => x.OldForeignReleaseIds.Contains(id)) - .SingleOrDefault(); + release = Query(x => x.OldForeignReleaseIds.Contains(id)).SingleOrDefault(); } return release; @@ -41,31 +38,36 @@ namespace NzbDrone.Core.Music public List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds) { - return Query - .Where(r => r.AlbumId == albumId) - .OrWhere($"[ForeignReleaseId] IN ('{string.Join("', '", foreignReleaseIds)}')") - .ToList(); + return Query(r => r.AlbumId == albumId || foreignReleaseIds.Contains(r.ForeignReleaseId)); } public List FindByAlbum(int id) { // populate the albums and artist metadata also // this hopefully speeds up the track matching a lot - return Query - .Join(JoinType.Left, r => r.Album, (r, a) => r.AlbumId == a.Id) - .Join(JoinType.Left, a => a.ArtistMetadata, (a, m) => a.ArtistMetadataId == m.Id) - .Where(r => r.AlbumId == id) - .ToList(); + var builder = new SqlBuilder() + .LeftJoin((r, a) => r.AlbumId == a.Id) + .LeftJoin((a, m) => a.ArtistMetadataId == m.Id) + .Where(r => r.AlbumId == id); + + return _database.QueryJoined(builder, (release, album, metadata) => + { + if (album != null) + { + album.ArtistMetadata = metadata; + release.Album = album; + } + + return release; + }).ToList(); } public List FindByRecordingId(List recordingIds) { - var query = "SELECT DISTINCT AlbumReleases.*" + - "FROM AlbumReleases " + - "JOIN Tracks ON Tracks.AlbumReleaseId = AlbumReleases.Id " + - $"WHERE Tracks.ForeignRecordingId IN ('{string.Join("', '", recordingIds)}')"; - - return Query.QueryText(query).ToList(); + return Query(Builder() + .Join((r, t) => r.Id == t.AlbumReleaseId) + .Where(t => Enumerable.Contains(recordingIds, t.ForeignRecordingId)) + .GroupBy(x => x.Id)); } public List SetMonitored(AlbumRelease release) diff --git a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs b/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs index 7fc678bbf..ce5906378 100644 --- a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Music @@ -28,91 +30,76 @@ namespace NzbDrone.Core.Music public List GetTracks(int artistId) { - string query = string.Format("SELECT Tracks.* " + - "FROM Artists " + - "JOIN Albums ON Albums.ArtistMetadataId == Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "WHERE Artists.Id = {0} " + - "AND AlbumReleases.Monitored = 1", - artistId); - - return Query.QueryText(query).ToList(); + return Query(Builder() + .Join((t, r) => t.AlbumReleaseId == r.Id) + .Join((r, a) => r.AlbumId == a.Id) + .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .Where(r => r.Monitored == true) + .Where(x => x.Id == artistId)); } public List GetTracksByAlbum(int albumId) { - string query = string.Format("SELECT Tracks.* " + - "FROM Albums " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "WHERE Albums.Id = {0} " + - "AND AlbumReleases.Monitored = 1", - albumId); - - return Query.QueryText(query).ToList(); + return Query(Builder() + .Join((t, r) => t.AlbumReleaseId == r.Id) + .Join((r, a) => r.AlbumId == a.Id) + .Where(r => r.Monitored == true) + .Where(x => x.Id == albumId)); } public List GetTracksByRelease(int albumReleaseId) { - return Query.Where(t => t.AlbumReleaseId == albumReleaseId).ToList(); + return Query(t => t.AlbumReleaseId == albumReleaseId).ToList(); } public List GetTracksByReleases(List albumReleaseIds) { // this will populate the artist metadata also - return Query - .Join(Marr.Data.QGen.JoinType.Inner, t => t.ArtistMetadata, (l, r) => l.ArtistMetadataId == r.Id) - .Where($"[AlbumReleaseId] IN ({string.Join(", ", albumReleaseIds)})") - .ToList(); + return _database.QueryJoined(Builder() + .Join((l, r) => l.ArtistMetadataId == r.Id) + .Where(x => albumReleaseIds.Contains(x.AlbumReleaseId)), (track, metadata) => + { + track.ArtistMetadata = metadata; + return track; + }).ToList(); } public List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds) { - return Query - .Where(t => t.AlbumReleaseId == albumReleaseId) - .OrWhere($"[ForeignTrackId] IN ('{string.Join("', '", foreignTrackIds)}')") - .ToList(); + return Query(a => a.AlbumReleaseId == albumReleaseId || foreignTrackIds.Contains(a.ForeignTrackId)); } public List GetTracksByFileId(int fileId) { - return Query.Where(e => e.TrackFileId == fileId).ToList(); + return Query(e => e.TrackFileId == fileId); } public List GetTracksByFileId(IEnumerable ids) { - return Query.Where($"[TrackFileId] IN ({string.Join(", ", ids)})").ToList(); + return Query(x => ids.Contains(x.TrackFileId)); } public List TracksWithFiles(int artistId) { - string query = string.Format("SELECT Tracks.* " + - "FROM Artists " + - "JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE Artists.Id == {0} " + - "AND AlbumReleases.Monitored = 1 ", - artistId); - - return Query.QueryText(query).ToList(); + return Query(Builder() + .Join((t, r) => t.AlbumReleaseId == r.Id) + .Join((r, a) => r.AlbumId == a.Id) + .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) + .Join((t, f) => t.TrackFileId == f.Id) + .Where(r => r.Monitored == true) + .Where(x => x.Id == artistId)); } public List TracksWithoutFiles(int albumId) { - string query = string.Format("SELECT Tracks.* " + - "FROM Albums " + - "JOIN AlbumReleases ON AlbumReleases.AlbumId == Albums.Id " + - "JOIN Tracks ON Tracks.AlbumReleaseId == AlbumReleases.Id " + - "LEFT OUTER JOIN TrackFiles ON TrackFiles.Id == Tracks.TrackFileId " + - "WHERE Albums.Id == {0} " + - "AND AlbumReleases.Monitored = 1 " + - "AND TrackFiles.Id IS NULL", - albumId); - - return Query.QueryText(query).ToList(); + //x.Id == null is converted to SQL, so warning incorrect +#pragma warning disable CS0472 + return Query(Builder() + .Join((t, r) => t.AlbumReleaseId == r.Id) + .LeftJoin((t, f) => t.TrackFileId == f.Id) + .Where(r => r.Monitored == true && r.AlbumId == albumId) + .Where(x => x.Id == null)); +#pragma warning restore CS0472 } public void SetFileId(List tracks) @@ -122,11 +109,9 @@ namespace NzbDrone.Core.Music public void DetachTrackFile(int trackFileId) { - DataMapper.Update() - .Where(x => x.TrackFileId == trackFileId) - .ColumnsIncluding(x => x.TrackFileId) - .Entity(new Track { TrackFileId = 0 }) - .Execute(); + var tracks = GetTracksByFileId(trackFileId); + tracks.ForEach(x => x.TrackFileId = 0); + SetFileId(tracks); } } } diff --git a/src/NzbDrone.Core/Music/Services/AlbumService.cs b/src/NzbDrone.Core/Music/Services/AlbumService.cs index 8aa5591cb..f6bdad2bc 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumService.cs +++ b/src/NzbDrone.Core/Music/Services/AlbumService.cs @@ -202,7 +202,7 @@ namespace NzbDrone.Core.Music public void SetAddOptions(IEnumerable albums) { - _albumRepository.SetFields(albums, s => s.AddOptions); + _albumRepository.SetFields(albums.ToList(), s => s.AddOptions); } public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) diff --git a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileRepository.cs b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileRepository.cs index df725610d..2a8bf7604 100644 --- a/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Metadata/MetadataProfileRepository.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Profiles.Metadata public bool Exists(int id) { - return DataMapper.Query().Where(p => p.Id == id).GetRowCount() == 1; + return Query(p => p.Id == id).Count == 1; } } } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs index 9f8bf9a0a..9a49aa891 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileRepository.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Profiles.Qualities public bool Exists(int id) { - return DataMapper.Query().Where(p => p.Id == id).GetRowCount() == 1; + return Query(p => p.Id == id).Count == 1; } } } diff --git a/src/NzbDrone.Core/Qualities/Revision.cs b/src/NzbDrone.Core/Qualities/Revision.cs index 42c413c37..3eeba89e9 100644 --- a/src/NzbDrone.Core/Qualities/Revision.cs +++ b/src/NzbDrone.Core/Qualities/Revision.cs @@ -5,8 +5,9 @@ namespace NzbDrone.Core.Qualities { public class Revision : IEquatable, IComparable { - private Revision() + public Revision() { + Version = 1; } public Revision(int version = 1, int real = 0, bool isRepack = false) diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index 2921ca7c8..4851ad223 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Tags public Tag GetByLabel(string label) { - var model = Query.Where(c => c.Label == label).SingleOrDefault(); + var model = Query(c => c.Label == label).SingleOrDefault(); if (model == null) { @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Tags public Tag FindByLabel(string label) { - return Query.Where(c => c.Label == label).SingleOrDefault(); + return Query(c => c.Label == label).SingleOrDefault(); } } } diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs b/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs index 305f80efb..d312b83c6 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderRepository.cs @@ -1,10 +1,15 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using System.Text.Json; +using Dapper; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.ThingiProvider { public class ProviderRepository : BasicRepository, IProviderRepository - where TProviderDefinition : ModelBase, + where TProviderDefinition : ProviderDefinition, new() { protected ProviderRepository(IMainDatabase database, IEventAggregator eventAggregator) @@ -12,9 +17,40 @@ namespace NzbDrone.Core.ThingiProvider { } - // public void DeleteImplementations(string implementation) - // { - // DataMapper.Delete(c => c.Implementation == implementation); - // } + protected override List Query(SqlBuilder builder) + { + var type = typeof(TProviderDefinition); + var sql = builder.Select(type).AddSelectTemplate(type); + + var results = new List(); + + using (var conn = _database.OpenConnection()) + using (var reader = conn.ExecuteReader(sql.RawSql, sql.Parameters)) + { + var parser = reader.GetRowParser(typeof(TProviderDefinition)); + var settingsIndex = reader.GetOrdinal(nameof(ProviderDefinition.Settings)); + var serializerSettings = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + + while (reader.Read()) + { + var body = reader.IsDBNull(settingsIndex) ? null : reader.GetString(settingsIndex); + var item = parser(reader); + var impType = typeof(IProviderConfig).Assembly.FindTypeByName(item.ConfigContract); + + if (body.IsNullOrWhiteSpace()) + { + item.Settings = NullConfig.Instance; + } + else + { + item.Settings = (IProviderConfig)JsonSerializer.Deserialize(body, impType, serializerSettings); + } + + results.Add(item); + } + } + + return results; + } } } diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs index 071f0ea9f..de947ad21 100644 --- a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.ThingiProvider.Status public TModel FindByProviderId(int providerId) { - return Query.Where(c => c.ProviderId == providerId).SingleOrDefault(); + return Query(c => c.ProviderId == providerId).SingleOrDefault(); } } } diff --git a/src/coverlet.runsettings b/src/coverlet.runsettings index 72987228b..9bac862e2 100644 --- a/src/coverlet.runsettings +++ b/src/coverlet.runsettings @@ -5,7 +5,7 @@ opencover - [Lidarr.*.Test]*,[Lidarr.Test.*]*,[Lidarr.Api.V1]*,[Marr.Data]*,[MonoTorrent]* + [Lidarr.*.Test]*,[Lidarr.Test.*]*,[Lidarr.Api.V1]*,[MonoTorrent]*