diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f6c3cd2bd..6fd2d382a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -534,6 +534,62 @@ stages: testResultsFiles: '**/TestResult.xml' testRunTitle: '$(testName) Unit Tests' failTaskOnFailedTests: true + + - job: Unit_LinuxCore_Postgres + displayName: Unit Native LinuxCore with Postgres Database + dependsOn: Prepare + condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) + variables: + pattern: 'Lidarr.*.linux-core-x64.tar.gz' + artifactName: linux-x64-tests + Lidarr__Postgres__Host: 'localhost' + Lidarr__Postgres__Port: '5432' + Lidarr__Postgres__User: 'lidarr' + Lidarr__Postgres__Password: 'lidarr' + + pool: + vmImage: ${{ variables.linuxImage }} + + timeoutInMinutes: 10 + + steps: + - task: UseDotNet@2 + displayName: 'Install .net core' + inputs: + version: $(dotnetVersion) + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download Test Artifact + inputs: + buildType: 'current' + artifactName: $(artifactName) + targetPath: $(testsFolder) + - bash: | + chmod a+x _tests/fpcalc + displayName: Make fpcalc Executable + condition: and(succeeded(), ne(variables['osName'], 'Windows')) + - bash: find ${TESTSFOLDER} -name "Lidarr.Test.Dummy" -exec chmod a+x {} \; + displayName: Make Test Dummy Executable + condition: and(succeeded(), ne(variables['osName'], 'Windows')) + - bash: | + docker run -d --name=postgres14 \ + -e POSTGRES_PASSWORD=lidarr \ + -e POSTGRES_USER=lidarr \ + -p 5432:5432/tcp \ + postgres:14 + displayName: Start postgres + - bash: | + chmod a+x ${TESTSFOLDER}/test.sh + ls -lR ${TESTSFOLDER} + ${TESTSFOLDER}/test.sh Linux Unit Test + displayName: Run Tests + - task: PublishTestResults@2 + displayName: Publish Test Results + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '**/TestResult.xml' + testRunTitle: 'LinuxCore Postgres Unit Tests' + failTaskOnFailedTests: true - stage: Integration displayName: Integration @@ -617,6 +673,67 @@ stages: failTaskOnFailedTests: true displayName: Publish Test Results + - job: Integration_LinuxCore_Postgres + displayName: Integration Native LinuxCore with Postgres Database + dependsOn: Prepare + condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) + variables: + pattern: 'Lidarr.*.linux-core-x64.tar.gz' + Lidarr__Postgres__Host: 'localhost' + Lidarr__Postgres__Port: '5432' + Lidarr__Postgres__User: 'lidarr' + Lidarr__Postgres__Password: 'lidarr' + + pool: + vmImage: ${{ variables.linuxImage }} + + steps: + - task: UseDotNet@2 + displayName: 'Install .net core' + inputs: + version: $(dotnetVersion) + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download Test Artifact + inputs: + buildType: 'current' + artifactName: 'linux-x64-tests' + targetPath: $(testsFolder) + - task: DownloadPipelineArtifact@2 + displayName: Download Build Artifact + inputs: + buildType: 'current' + artifactName: Packages + itemPattern: '**/$(pattern)' + targetPath: $(Build.ArtifactStagingDirectory) + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' + destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' + displayName: Extract Package + - bash: | + mkdir -p ./bin/ + cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Lidarr/. ./bin/ + displayName: Move Package Contents + - bash: | + docker run -d --name=postgres14 \ + -e POSTGRES_PASSWORD=lidarr \ + -e POSTGRES_USER=lidarr \ + -p 5432:5432/tcp \ + postgres:14 + displayName: Start postgres + - bash: | + chmod a+x ${TESTSFOLDER}/test.sh + ${TESTSFOLDER}/test.sh Linux Integration Test + displayName: Run Integration Tests + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '**/TestResult.xml' + testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests' + failTaskOnFailedTests: true + displayName: Publish Test Results + - job: Integration_FreeBSD displayName: Integration Native FreeBSD dependsOn: Prepare diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 1acd44b11..4635fef68 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -23,6 +23,8 @@ class About extends Component { isDocker, runtimeVersion, migrationVersion, + databaseVersion, + databaseType, appData, startupPath, mode, @@ -68,6 +70,11 @@ class About extends Component { data={migrationVersion} /> + + (new Mock().Object); + container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock>().Object); var serviceProvider = container.GetServiceProvider(); diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index df0f49e1e..aa9692fb0 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"/fetch/[a-z0-9]{32}/(?[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex(@"\b(\w*)?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce"), diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 9336248bc..27fd09299 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes try { _logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value); - startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString()); + + var key = environmentVariable.Key.ToString(); + var value = environmentVariable.Value?.ToString(); + + if (startInfo.EnvironmentVariables.ContainsKey(key)) + { + startInfo.EnvironmentVariables[key] = value; + } + else + { + startInfo.EnvironmentVariables.Add(key, value); + } } catch (Exception e) { diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index 807057837..f6c182660 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore public void SingleOrDefault_should_return_null_on_empty_db() { Mocker.Resolve() - .OpenConnection().Query("SELECT * FROM Artists") + .OpenConnection().Query("SELECT * FROM \"Artists\"") .SingleOrDefault(c => c.CleanName == "SomeTitle") .Should() .BeNull(); diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index ab17a1550..6c073b18f 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -2,7 +2,9 @@ using System; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; +using FluentAssertions.Equivalency; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.History; using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; @@ -13,6 +15,17 @@ namespace NzbDrone.Core.Test.Datastore [TestFixture] public class DatabaseRelationshipFixture : DbTest { + [SetUp] + public void Setup() + { + AssertionOptions.AssertEquivalencyUsing(options => + { + options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs(); + options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs(); + return options; + }); + } + [Test] public void one_to_one() { @@ -33,13 +46,7 @@ namespace NzbDrone.Core.Test.Datastore var loadedAlbum = Db.Single().Album.Value; loadedAlbum.Should().NotBeNull(); - loadedAlbum.Should().BeEquivalentTo(album, - options => options - .IncludingAllRuntimeProperties() - .Excluding(c => c.Artist) - .Excluding(c => c.ArtistId) - .Excluding(c => c.ArtistMetadata) - .Excluding(c => c.AlbumReleases)); + loadedAlbum.Should().BeEquivalentTo(album, AlbumComparerOptions); } [Test] @@ -86,5 +93,9 @@ namespace NzbDrone.Core.Test.Datastore returnedHistory[0].Quality.Quality.Should().Be(Quality.MP3_320); } + + private EquivalencyAssertionOptions AlbumComparerOptions(EquivalencyAssertionOptions opts) => opts.ComparingByMembers() + .Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>)) + .Excluding(x => x.ArtistId); } } diff --git a/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs index 5241eefcd..427f62f2b 100644 --- a/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/LazyLoadingFixture.cs @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_artist_for_trackfile() { var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); + var tracks = db.Query(new SqlBuilder(db.DatabaseType)).ToList(); Assert.IsNotEmpty(tracks); foreach (var track in tracks) @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_trackfile_if_not_joined() { var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); + var tracks = db.Query(new SqlBuilder(db.DatabaseType)).ToList(); foreach (var track in tracks) { @@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.Datastore { var db = Mocker.Resolve(); var files = MediaFileRepository.Query(db, - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((f, t) => f.Id == t.TrackFileId) .Join((t, a) => t.AlbumId == a.Id) .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) @@ -157,7 +157,7 @@ namespace NzbDrone.Core.Test.Datastore { var db = Mocker.Resolve(); var files = db.QueryJoined( - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((t, a) => t.AlbumId == a.Id) .Join((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) .Join((a, m) => a.ArtistMetadataId == m.Id), @@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_tracks_if_not_joined() { var db = Mocker.Resolve(); - var release = db.Query(new SqlBuilder().Where(x => x.Id == 1)).SingleOrDefault(); + var release = db.Query(new SqlBuilder(db.DatabaseType).Where(x => x.Id == 1)).SingleOrDefault(); Assert.IsFalse(release.Tracks.IsLoaded); Assert.IsNotNull(release.Tracks.Value); @@ -198,7 +198,7 @@ namespace NzbDrone.Core.Test.Datastore public void should_lazy_load_track_if_not_joined() { var db = Mocker.Resolve(); - var tracks = db.Query(new SqlBuilder()).ToList(); + var tracks = db.Query(new SqlBuilder(db.DatabaseType)).ToList(); foreach (var track in tracks) { diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/004_add_various_qualities_in_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/004_add_various_qualities_in_profileFixture.cs index cba53eb71..c13f542e8 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/004_add_various_qualities_in_profileFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/004_add_various_qualities_in_profileFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + var profiles = db.Query("SELECT \"Items\" FROM \"Profiles\" LIMIT 1"); var items = profiles.First().Items; items.Should().HaveCount(7); @@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var profiles = db.Query("SELECT Items FROM Profiles LIMIT 1"); + var profiles = db.Query("SELECT \"Items\" FROM \"Profiles\" LIMIT 1"); var items = profiles.First().Items; items.Should().HaveCount(7); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/023_add_release_groups_etcFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/023_add_release_groups_etcFixture.cs index 233968f4d..0278fbb79 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/023_add_release_groups_etcFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/023_add_release_groups_etcFixture.cs @@ -24,8 +24,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration Status = 1, Images = "", Path = $"/mnt/data/path/{name}", - Monitored = 1, - AlbumFolder = 1, + Monitored = true, + AlbumFolder = true, LanguageProfileId = 1, MetadataProfileId = 1 }); @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration Title = title, CleanTitle = title, Images = "", - Monitored = 1, + Monitored = true, AlbumType = "Studio", Duration = 100, Media = "", @@ -61,9 +61,9 @@ namespace NzbDrone.Core.Test.Datastore.Migration ForeignTrackId = id.ToString(), ArtistId = artistid, AlbumId = albumid, - Explicit = 0, - Compilation = 0, - Monitored = 0, + Explicit = false, + Compilation = false, + Monitored = false, Duration = 100, MediumNumber = 1, AbsoluteTrackNumber = i, @@ -74,8 +74,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration private IEnumerable VerifyAlbumReleases(IDirectDataMapper db) { - var releases = db.Query("SELECT * FROM AlbumReleases"); - var albums = db.Query("SELECT * FROM Albums"); + var releases = db.Query("SELECT * FROM \"AlbumReleases\""); + var albums = db.Query("SELECT * FROM \"Albums\""); // we only put in one release per album releases.Count().Should().Be(albums.Count()); @@ -91,12 +91,12 @@ namespace NzbDrone.Core.Test.Datastore.Migration private void VerifyTracks(IDirectDataMapper db, int albumId, int albumReleaseId, int expectedCount) { - var tracks = db.Query("SELECT Tracks.* FROM Tracks " + - "JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id " + - "JOIN Albums ON AlbumReleases.AlbumId = Albums.Id " + - "WHERE Albums.Id = " + albumId).ToList(); + var tracks = db.Query("SELECT \"Tracks\".* FROM \"Tracks\" " + + "JOIN \"AlbumReleases\" ON \"Tracks\".\"AlbumReleaseId\" = \"AlbumReleases\".\"Id\" " + + "JOIN \"Albums\" ON \"AlbumReleases\".\"AlbumId\" = \"Albums\".\"Id\" " + + "WHERE \"Albums\".\"Id\" = " + albumId).ToList(); - var album = db.Query("SELECT * FROM Albums WHERE Albums.Id = " + albumId).ToList().Single(); + var album = db.Query("SELECT * FROM \"Albums\" WHERE \"Albums\".\"Id\" = " + albumId).ToList().Single(); tracks.Count.Should().Be(expectedCount); tracks.First().AlbumReleaseId.Should().Be(albumReleaseId); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs index 201b6a99f..67a82d38f 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/030_add_mediafilerepository_mtimeFixture.cs @@ -25,8 +25,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration Id = id, CleanName = name, Path = _artistPath, - Monitored = 1, - AlbumFolder = 1, + Monitored = true, + AlbumFolder = true, LanguageProfileId = 1, MetadataProfileId = 1, ArtistMetadataId = id @@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.Datastore.Migration Title = title, CleanTitle = title, Images = "", - Monitored = 1, + Monitored = true, AlbumType = "Studio", - AnyReleaseOk = 1 + AnyReleaseOk = true }); } @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { Id = id, ForeignTrackId = id.ToString(), - Explicit = 0, + Explicit = false, TrackFileId = id, Duration = 100, MediumNumber = 1, @@ -102,8 +102,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration private void VerifyTracksFiles(IDirectDataMapper db, int albumId, List expectedPaths) { - var tracks = db.Query("SELECT TrackFiles.* FROM TrackFiles " + - "WHERE TrackFiles.AlbumId = " + albumId); + var tracks = db.Query("SELECT \"TrackFiles\".* FROM \"TrackFiles\" " + + "WHERE \"TrackFiles\".\"AlbumId\" = " + albumId); TestLogger.Debug($"Got {tracks.Count} tracks"); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/031_add_artistmetadataid_constraintFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/031_add_artistmetadataid_constraintFixture.cs index 4af321c33..b83e38e74 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/031_add_artistmetadataid_constraintFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/031_add_artistmetadataid_constraintFixture.cs @@ -34,8 +34,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration ArtistMetadataId = artistMetadataId, CleanName = name, Path = _artistPath, - Monitored = 1, - AlbumFolder = 1, + Monitored = true, + AlbumFolder = true, LanguageProfileId = 1, MetadataProfileId = 1, }); @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration private void VerifyArtists(IDirectDataMapper db, List ids) { - var artists = db.Query("SELECT Artists.* from Artists"); + var artists = db.Query("SELECT \"Artists\".* from \"Artists\""); artists.Select(x => x["Id"]).Should().BeEquivalentTo(ids); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/036_add_download_client_priorityFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/036_add_download_client_priorityFixture.cs index 965c37973..038adf911 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/036_add_download_client_priorityFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/036_add_download_client_priorityFixture.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 1, + Enable = true, Name = "Deluge", Implementation = "Deluge", Settings = new DelugeSettings36 @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(1); items.First().Priority.Should().Be(1); @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 1, + Enable = true, Name = "Deluge", Implementation = "Deluge", Settings = new DelugeSettings36 @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration ConfigContract = "DelugeSettings" }).Row(new { - Enable = 1, + Enable = true, Name = "Deluge2", Implementation = "Deluge", Settings = new DelugeSettings36 @@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration ConfigContract = "DelugeSettings" }).Row(new { - Enable = 1, + Enable = true, Name = "sab", Implementation = "Sabnzbd", Settings = new SabnzbdSettings36 @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(3); items[0].Priority.Should().Be(1); @@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 0, + Enable = false, Name = "Deluge", Implementation = "Deluge", Settings = new DelugeSettings36 @@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration ConfigContract = "DelugeSettings" }).Row(new { - Enable = 0, + Enable = false, Name = "Deluge2", Implementation = "Deluge", Settings = new DelugeSettings36 @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration ConfigContract = "DelugeSettings" }).Row(new { - Enable = 0, + Enable = false, Name = "sab", Implementation = "Sabnzbd", Settings = new SabnzbdSettings36 @@ -131,7 +131,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(3); items[0].Priority.Should().Be(1); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/049_email_multiple_addressesFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/049_email_multiple_addressesFixture.cs index 4cad9f2ef..2fbc7fb5d 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/049_email_multiple_addressesFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/049_email_multiple_addressesFixture.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM Notifications"); + var items = db.Query("SELECT * FROM \"Notifications\""); items.Should().HaveCount(1); items.First().Implementation.Should().Be("Email"); diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/051_cdh_per_downloadclientFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/051_cdh_per_downloadclientFixture.cs index e1e374e2f..9591947b3 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/051_cdh_per_downloadclientFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/051_cdh_per_downloadclientFixture.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 1, + Enable = true, Name = "Deluge", Implementation = "Deluge", Priority = 1, @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(1); items.First().RemoveCompletedDownloads.Should().BeFalse(); @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 1, + Enable = true, Name = "Deluge", Implementation = "Deluge", Priority = 1, @@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(1); items.First().RemoveCompletedDownloads.Should().BeTrue(); @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration { c.Insert.IntoTable("DownloadClients").Row(new { - Enable = 1, + Enable = true, Name = "RTorrent", Implementation = "RTorrent", Priority = 1, @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration }); }); - var items = db.Query("SELECT * FROM DownloadClients"); + var items = db.Query("SELECT * FROM \"DownloadClients\""); items.Should().HaveCount(1); items.First().RemoveCompletedDownloads.Should().BeFalse(); diff --git a/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs b/src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs similarity index 86% rename from src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs rename to src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs index ae0a62725..700a6f4c0 100644 --- a/src/NzbDrone.Core.Test/Datastore/WhereBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs @@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore { [TestFixture] - public class WhereBuilderFixture : CoreTest + public class WhereBuilderPostgresFixture : CoreTest { - private WhereBuilder _subject; + private WhereBuilderPostgres _subject; [OneTimeSetUp] public void MapTables() @@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore Mocker.Resolve(); } - private WhereBuilder Where(Expression> filter) + private WhereBuilderPostgres Where(Expression> filter) { - return new WhereBuilder(filter, true, 0); + return new WhereBuilderPostgres(filter, true, 0); } - private WhereBuilder WhereMetadata(Expression> filter) + private WhereBuilderPostgres WhereMetadata(Expression> filter) { - return new WhereBuilder(filter, true, 0); + return new WhereBuilderPostgres(filter, true, 0); } [Test] @@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore public void where_throws_without_concrete_condition_if_requiresConcreteCondition() { Expression> filter = (x, y) => x.Id == y.Id; - _subject = new WhereBuilder(filter, true, 0); + _subject = new WhereBuilderPostgres(filter, true, 0); Assert.Throws(() => _subject.ToString()); } @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore public void where_allows_abstract_condition_if_not_requiresConcreteCondition() { Expression> filter = (x, y) => x.Id == y.Id; - _subject = new WhereBuilder(filter, false, 0); + _subject = new WhereBuilderPostgres(filter, false, 0); _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = \"Artists\".\"Id\")"); } @@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.Contains(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1 || '%')"); + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => test.Contains(x.CleanName)); - _subject.ToString().Should().Be($"(@Clause1_P1 LIKE '%' || \"Artists\".\"CleanName\" || '%')"); + _subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Artists\".\"CleanName\" || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.StartsWith(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE @Clause1_P1 || '%')"); + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" ILIKE @Clause1_P1 || '%')"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.Datastore var test = "small"; _subject = Where(x => x.CleanName.EndsWith(test)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" LIKE '%' || @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" ILIKE '%' || @Clause1_P1)"); _subject.Parameters.Get("Clause1_P1").Should().Be(test); } @@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.Datastore var list = new List { 1, 2, 3 }; _subject = Where(x => list.Contains(x.Id)); - _subject.ToString().Should().Be($"(\"Artists\".\"Id\" IN (1, 2, 3))"); + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = ANY (('{{1, 2, 3}}')))"); } [Test] @@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Datastore 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)))"); + _subject.ToString().Should().Be($"((\"Artists\".\"CleanName\" = @Clause1_P1) AND (\"Artists\".\"Id\" = ANY (('{{1, 2, 3}}'))))"); } [Test] @@ -179,7 +179,7 @@ namespace NzbDrone.Core.Test.Datastore _subject = Where(x => list.Contains(x.CleanName)); - _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"Artists\".\"CleanName\" = ANY (@Clause1_P1))"); } [Test] @@ -187,7 +187,7 @@ namespace NzbDrone.Core.Test.Datastore { _subject = WhereMetadata(x => x.OldForeignArtistIds.Contains("foreignId")); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"OldForeignArtistIds\" LIKE '%' || @Clause1_P1 || '%')"); + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"OldForeignArtistIds\" ILIKE '%' || @Clause1_P1 || '%')"); } [Test] @@ -204,7 +204,7 @@ namespace NzbDrone.Core.Test.Datastore var allowed = new List { ArtistStatusType.Continuing, ArtistStatusType.Ended }; _subject = WhereMetadata(x => allowed.Contains(x.Status)); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" = ANY (@Clause1_P1))"); } [Test] @@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.Datastore var allowed = new ArtistStatusType[] { ArtistStatusType.Continuing, ArtistStatusType.Ended }; _subject = WhereMetadata(x => allowed.Contains(x.Status)); - _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" IN @Clause1_P1)"); + _subject.ToString().Should().Be($"(\"ArtistMetadata\".\"Status\" = ANY (@Clause1_P1))"); } } } diff --git a/src/NzbDrone.Core.Test/Datastore/WhereBuilderSqliteFixture.cs b/src/NzbDrone.Core.Test/Datastore/WhereBuilderSqliteFixture.cs new file mode 100644 index 000000000..cee2029d6 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/WhereBuilderSqliteFixture.cs @@ -0,0 +1,203 @@ +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 WhereBuilderSqliteFixture : CoreTest + { + private WhereBuilderSqlite _subject; + + [OneTimeSetUp] + public void MapTables() + { + // Generate table mapping + Mocker.Resolve(); + } + + private WhereBuilderSqlite Where(Expression> filter) + { + return new WhereBuilderSqlite(filter, true, 0); + } + + private WhereBuilderSqlite WhereMetadata(Expression> filter) + { + return new WhereBuilderSqlite(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 author = new Artist { Id = 10 }; + _subject = Where(x => x.Id == author.Id); + + _subject.Parameters.ParameterNames.Should().HaveCount(1); + _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = @Clause1_P1)"); + _subject.Parameters.Get("Clause1_P1").Should().Be(author.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 WhereBuilderSqlite(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 WhereBuilderSqlite(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 author = new Artist { CleanName = null }; + _subject = Where(x => x.CleanName == author.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))"); + + _subject.Parameters.ParameterNames.Should().BeEmpty(); + } + + [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 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/Framework/DbTest.cs b/src/NzbDrone.Core.Test/Framework/DbTest.cs index e38336375..fba791b15 100644 --- a/src/NzbDrone.Core.Test/Framework/DbTest.cs +++ b/src/NzbDrone.Core.Test/Framework/DbTest.cs @@ -1,14 +1,18 @@ -using System; +using System; using System.Collections.Generic; using System.Data.SQLite; using System.IO; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Npgsql; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Test.Common.Datastore; namespace NzbDrone.Core.Test.Framework { @@ -49,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework public abstract class DbTest : CoreTest { private ITestDatabase _db; + private DatabaseType _databaseType; protected virtual MigrationType MigrationType => MigrationType.Main; @@ -101,17 +106,39 @@ namespace NzbDrone.Core.Test.Framework private IDatabase CreateDatabase(MigrationContext migrationContext) { + if (_databaseType == DatabaseType.PostgreSQL) + { + CreatePostgresDb(); + } + var factory = Mocker.Resolve(); // If a special migration test or log migration then create new - if (migrationContext.BeforeMigration != null) + if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL) { return factory.Create(migrationContext); } + return CreateSqliteDatabase(factory, migrationContext); + } + + private void CreatePostgresDb() + { + var options = Mocker.Resolve>().Value; + PostgresDatabase.Create(options, MigrationType); + } + + private void DropPostgresDb() + { + var options = Mocker.Resolve>().Value; + PostgresDatabase.Drop(options, MigrationType); + } + + private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext) + { // Otherwise try to use a cached migrated db - var cachedDb = GetCachedDatabase(migrationContext.MigrationType); - var testDb = GetTestDb(migrationContext.MigrationType); + var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType); + var testDb = GetTestSqliteDb(migrationContext.MigrationType); if (File.Exists(cachedDb)) { TestLogger.Info($"Using cached initial database {cachedDb}"); @@ -131,12 +158,7 @@ namespace NzbDrone.Core.Test.Framework } } - private string GetCachedDatabase(MigrationType type) - { - return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db"); - } - - private string GetTestDb(MigrationType type) + private string GetTestSqliteDb(MigrationType type) { return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase(); } @@ -151,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework WithTempAsAppPath(); SetupLogging(); + // populate the possible postgres options + var postgresOptions = PostgresDatabase.GetTestOptions(); + _databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite; + + // Set up remaining container services + Mocker.SetConstant(Options.Create(postgresOptions)); + Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -170,12 +199,19 @@ namespace NzbDrone.Core.Test.Framework // Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly) GC.Collect(); GC.WaitForPendingFinalizers(); + SQLiteConnection.ClearAllPools(); + NpgsqlConnection.ClearAllPools(); if (TestFolderInfo != null) { DeleteTempFolder(TestFolderInfo.AppDataFolder); } + + if (_databaseType == DatabaseType.PostgreSQL) + { + DropPostgresDb(); + } } } } diff --git a/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs b/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs index 587043e95..ae6b102a7 100644 --- a/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs +++ b/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs @@ -1,5 +1,7 @@ using System.IO; using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Test.Common.Datastore; namespace NzbDrone.Core.Test { @@ -10,13 +12,13 @@ namespace NzbDrone.Core.Test [OneTimeTearDown] public void ClearCachedDatabase() { - var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db"); + var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main); if (File.Exists(mainCache)) { File.Delete(mainCache); } - var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db"); + var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log); if (File.Exists(logCache)) { File.Delete(logCache); diff --git a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs index 3fbfdf028..5391cfb1f 100644 --- a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs +++ b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework where T : ModelBase, new(); IDirectDataMapper GetDirectDataMapper(); IDbConnection OpenConnection(); + DatabaseType DatabaseType { get; } } public class TestDatabase : ITestDatabase @@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework private readonly IDatabase _dbConnection; private readonly IEventAggregator _eventAggregator; + public DatabaseType DatabaseType => _dbConnection.DatabaseType; + public TestDatabase(IDatabase dbConnection) { _eventAggregator = new Mock().Object; diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedReleasesFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedReleasesFixture.cs new file mode 100644 index 000000000..4b57909a2 --- /dev/null +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupOrphanedReleasesFixture.cs @@ -0,0 +1,43 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Housekeeping.Housekeepers; +using NzbDrone.Core.Music; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Housekeeping.Housekeepers +{ + [TestFixture] + public class CleanupOrphanedReleasesFixture : DbTest + { + [Test] + public void should_delete_orphaned_releases() + { + var albumRelease = Builder.CreateNew() + .BuildNew(); + + Db.Insert(albumRelease); + Subject.Clean(); + AllStoredModels.Should().BeEmpty(); + } + + [Test] + public void should_not_delete_unorphaned_albums() + { + var album = Builder.CreateNew() + .BuildNew(); + + Db.Insert(album); + + var albumReleases = Builder.CreateListOfSize(2) + .TheFirst(1) + .With(e => e.AlbumId = album.Id) + .BuildListOfNew(); + + Db.InsertMany(albumReleases); + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + AllStoredModels.Should().Contain(e => e.AlbumId == album.Id); + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs index c6bc5cbb9..5d57b2755 100644 --- a/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/AlbumRepositoryTests/AlbumRepositoryFixture.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; +using FluentAssertions.Equivalency; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; @@ -23,6 +25,13 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests [SetUp] public void Setup() { + AssertionOptions.AssertEquivalencyUsing(options => + { + options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs(); + options.Using(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs(); + return options; + }); + _artist = new Artist { Name = "Alien Ant Farm", @@ -184,7 +193,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests GivenMultipleAlbums(); var result = _albumRepo.GetNextAlbums(new[] { _artist.ArtistMetadataId }); - result.Should().BeEquivalentTo(_albums.Take(1)); + result.Should().BeEquivalentTo(_albums.Take(1), AlbumComparerOptions); } [Test] @@ -193,7 +202,11 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests GivenMultipleAlbums(); var result = _albumRepo.GetLastAlbums(new[] { _artist.ArtistMetadataId }); - result.Should().BeEquivalentTo(_albums.Skip(2).Take(1)); + result.Should().BeEquivalentTo(_albums.Skip(2).Take(1), AlbumComparerOptions); } + + private EquivalencyAssertionOptions AlbumComparerOptions(EquivalencyAssertionOptions opts) => opts.ComparingByMembers() + .Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>)) + .Excluding(x => x.ArtistId); } } diff --git a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs index 48b5530cf..50c28fe30 100644 --- a/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/ArtistRepositoryTests/ArtistRepositoryFixture.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Data.SQLite; using FizzWare.NBuilder; using FluentAssertions; +using Npgsql; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Music; using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Qualities; @@ -159,7 +161,14 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests _artistRepo.Insert(artist1); Action insertDupe = () => _artistRepo.Insert(artist2); - insertDupe.Should().Throw(); + if (Db.DatabaseType == DatabaseType.PostgreSQL) + { + insertDupe.Should().Throw(); + } + else + { + insertDupe.Should().Throw(); + } } } } diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs index f1df30bb2..cd9a83bfc 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MusicTests .BuildList(); Mocker.GetMock() - .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) + .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) .Returns(_tracks); } @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.MusicTests .Returns(clash); Mocker.GetMock() - .Setup(x => x.GetTracksForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetTracksForRefresh(It.IsAny(), It.IsAny>())) .Returns(_tracks); var newInfo = existing.JsonClone(); @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.MusicTests newInfo.Tracks = new List { newTrack }; Mocker.GetMock() - .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) + .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) .Returns(new List { oldTrack }); Subject.RefreshEntityInfo(_release, new List { newInfo }, false, false, null); diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs index 56b98c2b7..0ad534b64 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.MusicTests .Returns(_artist); Mocker.GetMock() - .Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny>())) + .Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny>())) .Returns(new List { release }); Mocker.GetMock() @@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.MusicTests .Returns(new List()); Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) .Returns(_releases); var newAlbumInfo = existing.JsonClone(); @@ -209,7 +209,7 @@ namespace NzbDrone.Core.Test.MusicTests .Build() as List; Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) .Returns(existingReleases); Mocker.GetMock() @@ -263,7 +263,7 @@ namespace NzbDrone.Core.Test.MusicTests .Build() as List; Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) .Returns(existingReleases); Mocker.GetMock() @@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.MusicTests .Build() as List; Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) .Returns(existingReleases); Mocker.GetMock() @@ -376,7 +376,7 @@ namespace NzbDrone.Core.Test.MusicTests .Build() as List; Mocker.GetMock() - .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetReleasesForRefresh(It.IsAny(), It.IsAny>())) .Returns(existingReleases); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs index f6a865f52..aacbde747 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshArtistServiceFixture.cs @@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.MusicTests private void GivenAlbumsForRefresh(List albums) { Mocker.GetMock(MockBehavior.Strict) - .Setup(s => s.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) + .Setup(s => s.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) .Returns(albums); } @@ -229,7 +229,7 @@ namespace NzbDrone.Core.Test.MusicTests Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) + .Setup(x => x.GetAlbumsForRefresh(It.IsAny(), It.IsAny>())) .Returns(new List()); // Update called twice for a move/merge @@ -289,7 +289,7 @@ namespace NzbDrone.Core.Test.MusicTests Mocker.GetMock(MockBehavior.Strict) .InSequence(seq) - .Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny>())) + .Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny>())) .Returns(_albums); // Update called twice for a move/merge diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs index 29ec139ba..17fee9dc2 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.ArtistStats public class ArtistStatisticsRepository : IArtistStatisticsRepository { - private const string _selectTemplate = "SELECT /**select**/ FROM Tracks /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + private const string _selectTemplate = "SELECT /**select**/ FROM \"Tracks\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; private readonly IMainDatabase _database; @@ -28,14 +28,27 @@ namespace NzbDrone.Core.ArtistStats public List ArtistStatistics() { var time = DateTime.UtcNow; + + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + return Query(Builder().WherePostgres(x => x.ReleaseDate < time)); + } + return Query(Builder().Where(x => x.ReleaseDate < time)); } public List ArtistStatistics(int artistId) { var time = DateTime.UtcNow; + + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + return Query(Builder().WherePostgres(x => x.ReleaseDate < time) + .WherePostgres(x => x.Id == artistId)); + } + return Query(Builder().Where(x => x.ReleaseDate < time) - .Where(x => x.Id == artistId)); + .Where(x => x.Id == artistId)); } private List Query(SqlBuilder builder) @@ -48,20 +61,23 @@ namespace NzbDrone.Core.ArtistStats } } - 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") - .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); + private SqlBuilder Builder() + { + return new SqlBuilder(_database.DatabaseType) + .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"" = true 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""") + .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/Blocklisting/BlocklistRepository.cs b/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs index f80e997d9..d2368d400 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistRepository.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Core.Blocklisting Delete(x => artistIds.Contains(x.ArtistId)); } - protected override SqlBuilder PagedBuilder() => new SqlBuilder().Join((b, m) => b.ArtistId == m.Id); + protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType).Join((b, m) => b.ArtistId == m.Id); protected override IEnumerable PagedQuery(SqlBuilder builder) => _database.QueryJoined(builder, (bl, artist) => { bl.Artist = artist; diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 99b11aba0..a69101a9d 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -4,12 +4,14 @@ using System.IO; using System.Linq; using System.Xml; using System.Xml.Linq; +using Microsoft.Extensions.Options; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -49,6 +51,12 @@ namespace NzbDrone.Core.Configuration int SyslogPort { get; } string SyslogLevel { get; } string Theme { get; } + string PostgresHost { get; } + int PostgresPort { get; } + string PostgresUser { get; } + string PostgresPassword { get; } + string PostgresMainDb { get; } + string PostgresLogDb { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -58,6 +66,7 @@ namespace NzbDrone.Core.Configuration private readonly IEventAggregator _eventAggregator; private readonly IDiskProvider _diskProvider; private readonly ICached _cache; + private readonly PostgresOptions _postgresOptions; private readonly string _configFile; @@ -66,12 +75,14 @@ namespace NzbDrone.Core.Configuration public ConfigFileProvider(IAppFolderInfo appFolderInfo, ICacheManager cacheManager, IEventAggregator eventAggregator, - IDiskProvider diskProvider) + IDiskProvider diskProvider, + IOptions postgresOptions) { _cache = cacheManager.GetCache(GetType()); _eventAggregator = eventAggregator; _diskProvider = diskProvider; _configFile = appFolderInfo.GetConfigPath(); + _postgresOptions = postgresOptions.Value; } public Dictionary GetConfigDictionary() @@ -186,6 +197,12 @@ namespace NzbDrone.Core.Configuration public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string Theme => GetValue("Theme", "light", persist: false); + public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false); + public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false); + public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false); + public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "lidarr-main", persist: false); + public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "lidarr-log", persist: false); + public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, 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); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 02e16f2ab..8f997ad8f 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -67,7 +67,7 @@ namespace NzbDrone.Core.Datastore _updateSql = GetUpdateSql(_properties); } - protected virtual SqlBuilder Builder() => new SqlBuilder(); + protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType); protected virtual List Query(SqlBuilder builder) => _database.Query(builder).ToList(); @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Datastore { using (var conn = _database.OpenConnection()) { - return conn.ExecuteScalar($"SELECT COUNT(*) FROM {_table}"); + return conn.ExecuteScalar($"SELECT COUNT(*) FROM \"{_table}\""); } } @@ -175,6 +175,11 @@ namespace NzbDrone.Core.Datastore } } + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\""; + } + return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id"; } @@ -182,7 +187,8 @@ namespace NzbDrone.Core.Datastore { SqlBuilderExtensions.LogQuery(_insertSql, model); var multi = connection.QueryMultiple(_insertSql, model, transaction); - var id = (int)multi.Read().First().id; + var multiRead = multi.Read(); + var id = (int)(multiRead.First().id ?? multiRead.First().Id); _keyProperty.SetValue(model, id); return model; @@ -293,7 +299,7 @@ namespace NzbDrone.Core.Datastore { using (var conn = _database.OpenConnection()) { - conn.Execute($"DELETE FROM [{_table}]"); + conn.Execute($"DELETE FROM \"{_table}\""); } if (vacuum) @@ -352,7 +358,7 @@ namespace NzbDrone.Core.Datastore private string GetUpdateSql(List propertiesToUpdate) { var sb = new StringBuilder(); - sb.AppendFormat("UPDATE {0} SET ", _table); + sb.AppendFormat("UPDATE \"{0}\" SET ", _table); for (var i = 0; i < propertiesToUpdate.Count; i++) { @@ -420,9 +426,10 @@ namespace NzbDrone.Core.Datastore pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}"; } + var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey); 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}"); + var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize; + builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}"); return queryFunc(builder).ToList(); } diff --git a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs index cc087b450..bf53f907c 100644 --- a/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs +++ b/src/NzbDrone.Core/Datastore/ConnectionStringFactory.cs @@ -1,7 +1,9 @@ using System; using System.Data.SQLite; +using Npgsql; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Datastore { @@ -14,10 +16,17 @@ namespace NzbDrone.Core.Datastore public class ConnectionStringFactory : IConnectionStringFactory { - public ConnectionStringFactory(IAppFolderInfo appFolderInfo) + private readonly IConfigFileProvider _configFileProvider; + + public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider) { - MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase()); - LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase()); + _configFileProvider = configFileProvider; + + MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) : + GetConnectionString(appFolderInfo.GetDatabase()); + + LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) : + GetConnectionString(appFolderInfo.GetLogDatabase()); } public string MainDbConnectionString { get; private set; } @@ -48,5 +57,19 @@ namespace NzbDrone.Core.Datastore return connectionBuilder.ConnectionString; } + + private string GetPostgresConnectionString(string dbName) + { + var connectionBuilder = new NpgsqlConnectionStringBuilder(); + + connectionBuilder.Database = dbName; + connectionBuilder.Host = _configFileProvider.PostgresHost; + connectionBuilder.Username = _configFileProvider.PostgresUser; + connectionBuilder.Password = _configFileProvider.PostgresPassword; + connectionBuilder.Port = _configFileProvider.PostgresPort; + connectionBuilder.Enlist = false; + + return connectionBuilder.ConnectionString; + } } } diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index a9b6e807f..433fef0b6 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Text.RegularExpressions; using Dapper; using NLog; using NzbDrone.Common.Instrumentation; @@ -11,6 +12,7 @@ namespace NzbDrone.Core.Datastore IDbConnection OpenConnection(); Version Version { get; } int Migration { get; } + DatabaseType DatabaseType { get; } void Vacuum(); } @@ -32,13 +34,44 @@ namespace NzbDrone.Core.Datastore return _datamapperFactory(); } + public DatabaseType DatabaseType + { + get + { + using (var db = _datamapperFactory()) + { + if (db.ConnectionString.Contains(".db")) + { + return DatabaseType.SQLite; + } + else + { + return DatabaseType.PostgreSQL; + } + } + } + } + public Version Version { get { using (var db = _datamapperFactory()) { - var version = db.QueryFirstOrDefault("SELECT sqlite_version()"); + string version; + + try + { + version = db.QueryFirstOrDefault("SHOW server_version"); + + //Postgres can return extra info about operating system on version call, ignore this + version = Regex.Replace(version, @"\(.*?\)", ""); + } + catch + { + version = db.QueryFirstOrDefault("SELECT sqlite_version()"); + } + return new Version(version); } } @@ -50,7 +83,7 @@ namespace NzbDrone.Core.Datastore { using (var db = _datamapperFactory()) { - return db.QueryFirstOrDefault("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1"); + return db.QueryFirstOrDefault("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1"); } } } @@ -73,4 +106,10 @@ namespace NzbDrone.Core.Datastore } } } + + public enum DatabaseType + { + SQLite, + PostgreSQL + } } diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index 9fa0181d9..1b21589a2 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -1,6 +1,8 @@ using System; +using System.Data.Common; using System.Data.SQLite; using NLog; +using Npgsql; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Exceptions; @@ -85,10 +87,19 @@ namespace NzbDrone.Core.Datastore var db = new Database(migrationContext.MigrationType.ToString(), () => { - var conn = SQLiteFactory.Instance.CreateConnection(); - conn.ConnectionString = connectionString; - conn.Open(); + DbConnection conn; + if (connectionString.Contains(".db")) + { + conn = SQLiteFactory.Instance.CreateConnection(); + conn.ConnectionString = connectionString; + } + else + { + conn = new NpgsqlConnection(connectionString); + } + + conn.Open(); return conn; }); diff --git a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs index 97ce5d731..3bff36b7a 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/BuilderExtensions.cs @@ -20,12 +20,12 @@ namespace NzbDrone.Core.Datastore public static SqlBuilder Select(this SqlBuilder builder, params Type[] types) { - return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", ")); + 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(", ")); + return builder.Select("DISTINCT " + types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", ")); } public static SqlBuilder SelectCount(this SqlBuilder builder) @@ -42,41 +42,48 @@ namespace NzbDrone.Core.Datastore public static SqlBuilder Where(this SqlBuilder builder, Expression> filter) { - var wb = new WhereBuilder(filter, true, builder.Sequence); + var wb = GetWhereBuilder(builder.DatabaseType, filter, true, builder.Sequence); + + return builder.Where(wb.ToString(), wb.Parameters); + } + + public static SqlBuilder WherePostgres(this SqlBuilder builder, Expression> filter) + { + var wb = new WhereBuilderPostgres(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); + var wb = GetWhereBuilder(builder.DatabaseType, 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 wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); - return builder.Join($"{rightTable} ON {wb.ToString()}"); + 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 wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence); var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight)); - return builder.LeftJoin($"{rightTable} ON {wb.ToString()}"); + 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}"); + return builder.GroupBy($"\"{table}\".\"{propName}\""); } public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type) @@ -138,6 +145,18 @@ namespace NzbDrone.Core.Datastore return sb.ToString(); } + private static WhereBuilder GetWhereBuilder(DatabaseType databaseType, Expression filter, bool requireConcrete, int seq) + { + if (databaseType == DatabaseType.PostgreSQL) + { + return new WhereBuilderPostgres(filter, requireConcrete, seq); + } + else + { + return new WhereBuilderSqlite(filter, requireConcrete, seq); + } + } + private static Dictionary ToDictionary(this DynamicParameters dynamicParams) { var argsDictionary = new Dictionary(); @@ -150,11 +169,14 @@ namespace NzbDrone.Core.Datastore } var templates = dynamicParams.GetType().GetField("templates", BindingFlags.NonPublic | BindingFlags.Instance); - if (templates != null && templates.GetValue(dynamicParams) is List list) + if (templates != null) { - foreach (var objProps in list.Select(obj => obj.GetPropertyValuePairs().ToList())) + if (templates.GetValue(dynamicParams) is List list) { - objProps.ForEach(p => argsDictionary.Add(p.Key, p.Value)); + foreach (var objProps in list.Select(obj => obj.GetPropertyValuePairs().ToList())) + { + objProps.ForEach(p => argsDictionary.Add(p.Key, p.Value)); + } } } diff --git a/src/NzbDrone.Core/Datastore/LogDatabase.cs b/src/NzbDrone.Core/Datastore/LogDatabase.cs index f992c8bbe..a770c2661 100644 --- a/src/NzbDrone.Core/Datastore/LogDatabase.cs +++ b/src/NzbDrone.Core/Datastore/LogDatabase.cs @@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore public class LogDatabase : ILogDatabase { private readonly IDatabase _database; + private readonly DatabaseType _databaseType; public LogDatabase(IDatabase database) { _database = database; + _databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType; } public IDbConnection OpenConnection() @@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore public int Migration => _database.Migration; + public DatabaseType DatabaseType => _databaseType; + public void Vacuum() { _database.Vacuum(); diff --git a/src/NzbDrone.Core/Datastore/MainDatabase.cs b/src/NzbDrone.Core/Datastore/MainDatabase.cs index 4a9d3298c..521293299 100644 --- a/src/NzbDrone.Core/Datastore/MainDatabase.cs +++ b/src/NzbDrone.Core/Datastore/MainDatabase.cs @@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore public class MainDatabase : IMainDatabase { private readonly IDatabase _database; + private readonly DatabaseType _databaseType; public MainDatabase(IDatabase database) { _database = database; + _databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType; } public IDbConnection OpenConnection() @@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore public int Migration => _database.Migration; + public DatabaseType DatabaseType => _databaseType; + public void Vacuum() { _database.Vacuum(); diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index 7d6d9a190..a38e84073 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -288,8 +288,8 @@ namespace NzbDrone.Core.Datastore.Migration Insert.IntoTable("DelayProfiles").Row(new { - EnableUsenet = 1, - EnableTorrent = 1, + EnableUsenet = true, + EnableTorrent = true, PreferredProtocol = 1, UsenetDelay = 0, TorrentDelay = 0, diff --git a/src/NzbDrone.Core/Datastore/Migration/003_add_medium_support.cs b/src/NzbDrone.Core/Datastore/Migration/003_add_medium_support.cs index f0b68d390..9e0ddf501 100644 --- a/src/NzbDrone.Core/Datastore/Migration/003_add_medium_support.cs +++ b/src/NzbDrone.Core/Datastore/Migration/003_add_medium_support.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("Tracks").AddColumn("MediumNumber").AsInt32().WithDefaultValue(0); Alter.Table("Tracks").AddColumn("AbsoluteTrackNumber").AsInt32().WithDefaultValue(0); - Execute.Sql("UPDATE Tracks SET AbsoluteTrackNumber = TrackNumber"); + Execute.Sql("UPDATE \"Tracks\" SET \"AbsoluteTrackNumber\" = \"TrackNumber\""); Delete.Column("TrackNumber").FromTable("Tracks"); Alter.Table("Tracks").AddColumn("TrackNumber").AsString().Nullable(); diff --git a/src/NzbDrone.Core/Datastore/Migration/004_add_various_qualities_in_profile.cs b/src/NzbDrone.Core/Datastore/Migration/004_add_various_qualities_in_profile.cs index cabed21e5..ae58e81f5 100644 --- a/src/NzbDrone.Core/Datastore/Migration/004_add_various_qualities_in_profile.cs +++ b/src/NzbDrone.Core/Datastore/Migration/004_add_various_qualities_in_profile.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("UPDATE QualityDefinitions SET Title = 'MP3-160' WHERE Quality = 5"); // Change MP3-512 to MP3-160 + Execute.Sql("UPDATE \"QualityDefinitions\" SET \"Title\" = 'MP3-160' WHERE \"Quality\" = 5"); // Change MP3-512 to MP3-160 Execute.WithConnection(ConvertProfile); } @@ -172,8 +172,17 @@ namespace NzbDrone.Core.Datastore.Migration using (var updateProfileCmd = _connection.CreateCommand()) { updateProfileCmd.Transaction = _transaction; - updateProfileCmd.CommandText = - "UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ? WHERE Id = ?"; + if (_connection.GetType().FullName == "Npgsql.NpgsqlConnection") + { + updateProfileCmd.CommandText = + "UPDATE \"Profiles\" SET \"Name\" = $1, \"Cutoff\" = $2, \"Items\" = $3 WHERE \"Id\" = $4"; + } + else + { + updateProfileCmd.CommandText = + "UPDATE \"Profiles\" SET \"Name\" = ?, \"Cutoff\" = ?, \"Items\" = ? WHERE \"Id\" = ?"; + } + updateProfileCmd.AddParameter(profile.Name); updateProfileCmd.AddParameter(profile.Cutoff); updateProfileCmd.AddParameter(profile.Items.ToJson()); @@ -323,7 +332,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var getProfilesCmd = _connection.CreateCommand()) { getProfilesCmd.Transaction = _transaction; - getProfilesCmd.CommandText = @"SELECT Id, Name, Cutoff, Items FROM Profiles"; + getProfilesCmd.CommandText = @"SELECT ""Id"", ""Name"", ""Cutoff"", ""Items"" FROM ""Profiles"""; using (var profileReader = getProfilesCmd.ExecuteReader()) { diff --git a/src/NzbDrone.Core/Datastore/Migration/006_separate_automatic_and_interactive_search.cs b/src/NzbDrone.Core/Datastore/Migration/006_separate_automatic_and_interactive_search.cs index 3f03f8207..7b5a49849 100644 --- a/src/NzbDrone.Core/Datastore/Migration/006_separate_automatic_and_interactive_search.cs +++ b/src/NzbDrone.Core/Datastore/Migration/006_separate_automatic_and_interactive_search.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Datastore.Migration Rename.Column("EnableSearch").OnTable("Indexers").To("EnableAutomaticSearch"); Alter.Table("Indexers").AddColumn("EnableInteractiveSearch").AsBoolean().Nullable(); - Execute.Sql("UPDATE Indexers SET EnableInteractiveSearch = EnableAutomaticSearch"); + Execute.Sql("UPDATE \"Indexers\" SET \"EnableInteractiveSearch\" = \"EnableAutomaticSearch\""); Alter.Table("Indexers").AlterColumn("EnableInteractiveSearch").AsBoolean().NotNullable(); } diff --git a/src/NzbDrone.Core/Datastore/Migration/008_change_quality_size_mb_to_kb.cs b/src/NzbDrone.Core/Datastore/Migration/008_change_quality_size_mb_to_kb.cs index 07cb91c3d..43a62161a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/008_change_quality_size_mb_to_kb.cs +++ b/src/NzbDrone.Core/Datastore/Migration/008_change_quality_size_mb_to_kb.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("UPDATE QualityDefinitions SET MaxSize = CASE " + + IfDatabase("sqlite").Execute.Sql("UPDATE QualityDefinitions SET MaxSize = CASE " + "WHEN (CAST(MaxSize AS FLOAT) / 60) * 8 * 1024 < 1500 THEN " + "ROUND((CAST(MaxSize AS FLOAT) / 60) * 8 * 1024, 0) " + "ELSE NULL " + diff --git a/src/NzbDrone.Core/Datastore/Migration/012_add_release_status.cs b/src/NzbDrone.Core/Datastore/Migration/012_add_release_status.cs index cd1de385b..4a3423e95 100644 --- a/src/NzbDrone.Core/Datastore/Migration/012_add_release_status.cs +++ b/src/NzbDrone.Core/Datastore/Migration/012_add_release_status.cs @@ -115,7 +115,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var getProfilesCmd = _connection.CreateCommand()) { getProfilesCmd.Transaction = _transaction; - getProfilesCmd.CommandText = @"SELECT Id, Name FROM MetadataProfiles"; + getProfilesCmd.CommandText = @"SELECT ""Id"", ""Name"" FROM ""MetadataProfiles"""; using (var profileReader = getProfilesCmd.ExecuteReader()) { diff --git a/src/NzbDrone.Core/Datastore/Migration/013_album_download_notification.cs b/src/NzbDrone.Core/Datastore/Migration/013_album_download_notification.cs index 39bba97be..ec13cd84f 100644 --- a/src/NzbDrone.Core/Datastore/Migration/013_album_download_notification.cs +++ b/src/NzbDrone.Core/Datastore/Migration/013_album_download_notification.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("Notifications").AddColumn("OnAlbumDownload").AsBoolean().WithDefaultValue(0); + Alter.Table("Notifications").AddColumn("OnAlbumDownload").AsBoolean().WithDefaultValue(false); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/014_fix_language_metadata_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/014_fix_language_metadata_profiles.cs index 20f62fa65..7072fa881 100644 --- a/src/NzbDrone.Core/Datastore/Migration/014_fix_language_metadata_profiles.cs +++ b/src/NzbDrone.Core/Datastore/Migration/014_fix_language_metadata_profiles.cs @@ -8,17 +8,17 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("UPDATE artists SET metadataProfileId = " + - "CASE WHEN ((SELECT COUNT(*) FROM metadataprofiles) > 0) " + - "THEN (SELECT id FROM metadataprofiles ORDER BY id ASC LIMIT 1) " + + Execute.Sql("UPDATE \"Artists\" SET \"MetadataProfileId\" = " + + "CASE WHEN ((SELECT COUNT(*) FROM \"MetadataProfiles\") > 0) " + + "THEN (SELECT \"Id\" FROM \"MetadataProfiles\" ORDER BY \"Id\" ASC LIMIT 1) " + "ELSE 0 END " + - "WHERE artists.metadataProfileId == 0"); + "WHERE \"Artists\".\"MetadataProfileId\" = 0"); - Execute.Sql("UPDATE artists SET languageProfileId = " + - "CASE WHEN ((SELECT COUNT(*) FROM languageprofiles) > 0) " + - "THEN (SELECT id FROM languageprofiles ORDER BY id ASC LIMIT 1) " + + Execute.Sql("UPDATE \"Artists\" SET \"LanguageProfileId\" = " + + "CASE WHEN ((SELECT COUNT(*) FROM \"LanguageProfiles\") > 0) " + + "THEN (SELECT \"Id\" FROM \"LanguageProfiles\" ORDER BY \"Id\" ASC LIMIT 1) " + "ELSE 0 END " + - "WHERE artists.languageProfileId == 0"); + "WHERE \"Artists\".\"LanguageProfileId\" = 0"); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/015_remove_fanzub.cs b/src/NzbDrone.Core/Datastore/Migration/015_remove_fanzub.cs index 75f4dc2c9..edb798f94 100644 --- a/src/NzbDrone.Core/Datastore/Migration/015_remove_fanzub.cs +++ b/src/NzbDrone.Core/Datastore/Migration/015_remove_fanzub.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Fanzub';"); + Execute.Sql("DELETE FROM \"Indexers\" WHERE \"Implementation\" = 'Fanzub';"); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs index 1bb698528..97266331b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs +++ b/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var updateProfileCmd = _connection.CreateCommand()) { updateProfileCmd.Transaction = _transaction; - updateProfileCmd.CommandText = "UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ? WHERE Id = ?"; + updateProfileCmd.CommandText = "UPDATE \"Profiles\" SET \"Name\" = ?, \"Cutoff\" = ?, \"Items\" = ? WHERE \"Id\" = ?"; updateProfileCmd.AddParameter(profile.Name); updateProfileCmd.AddParameter(profile.Cutoff); updateProfileCmd.AddParameter(profile.Items.ToJson()); @@ -115,7 +115,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var getProfilesCmd = _connection.CreateCommand()) { getProfilesCmd.Transaction = _transaction; - getProfilesCmd.CommandText = @"SELECT Id, Name, Cutoff, Items FROM Profiles"; + getProfilesCmd.CommandText = @"SELECT ""Id"", ""Name"", ""Cutoff"", ""Items"" FROM ""Profiles"""; using (var profileReader = getProfilesCmd.ExecuteReader()) { diff --git a/src/NzbDrone.Core/Datastore/Migration/023_add_release_groups_etc.cs b/src/NzbDrone.Core/Datastore/Migration/023_add_release_groups_etc.cs index 5e5a58c0a..0b095f234 100644 --- a/src/NzbDrone.Core/Datastore/Migration/023_add_release_groups_etc.cs +++ b/src/NzbDrone.Core/Datastore/Migration/023_add_release_groups_etc.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Generic; using System.Data; using System.Linq; +using Dapper; using FluentMigrator; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Migration.Framework; -using NzbDrone.Core.Music; namespace NzbDrone.Core.Datastore.Migration { @@ -30,18 +30,18 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Members").AsString().Nullable(); // we want to preserve the artist ID. Shove all the metadata into the metadata table. - Execute.Sql(@"INSERT INTO ArtistMetadata (ForeignArtistId, Name, Overview, Disambiguation, Type, Status, Images, Links, Genres, Ratings, Members) - SELECT ForeignArtistId, Name, Overview, Disambiguation, ArtistType, Status, Images, Links, Genres, Ratings, Members - FROM Artists"); + Execute.Sql(@"INSERT INTO ""ArtistMetadata"" (""ForeignArtistId"", ""Name"", ""Overview"", ""Disambiguation"", ""Type"", ""Status"", ""Images"", ""Links"", ""Genres"", ""Ratings"", ""Members"") + SELECT ""ForeignArtistId"", ""Name"", ""Overview"", ""Disambiguation"", ""ArtistType"", ""Status"", ""Images"", ""Links"", ""Genres"", ""Ratings"", ""Members"" + FROM ""Artists"""); // Add an ArtistMetadataId column to Artists Alter.Table("Artists").AddColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0); // Update artistmetadataId - Execute.Sql(@"UPDATE Artists - SET ArtistMetadataId = (SELECT ArtistMetadata.Id - FROM ArtistMetadata - WHERE ArtistMetadata.ForeignArtistId = Artists.ForeignArtistId)"); + Execute.Sql(@"UPDATE ""Artists"" + SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id"" + FROM ""ArtistMetadata"" + WHERE ""ArtistMetadata"".""ForeignArtistId"" = ""Artists"".""ForeignArtistId"")"); // ALBUM RELEASES TABLE - Do this before we mess with the Albums table Create.TableForModel("AlbumReleases") @@ -68,11 +68,11 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("Albums").AddColumn("Links").AsString().Nullable(); // Set metadata ID - Execute.Sql(@"UPDATE Albums - SET ArtistMetadataId = (SELECT ArtistMetadata.Id - FROM ArtistMetadata - JOIN Artists ON ArtistMetadata.Id = Artists.ArtistMetadataId - WHERE Albums.ArtistId = Artists.Id)"); + Execute.Sql(@"UPDATE ""Albums"" + SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id"" + FROM ""ArtistMetadata"" + JOIN ""Artists"" ON ""ArtistMetadata"".""Id"" = ""Artists"".""ArtistMetadataId"" + WHERE ""Albums"".""ArtistId"" = ""Artists"".""Id"")"); // TRACKS TABLE Alter.Table("Tracks").AddColumn("ForeignRecordingId").AsString().WithDefaultValue("0"); @@ -80,18 +80,18 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("Tracks").AddColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0); // Set track release to the only release we've bothered populating - Execute.Sql(@"UPDATE Tracks - SET AlbumReleaseId = (SELECT AlbumReleases.Id - FROM AlbumReleases - JOIN Albums ON AlbumReleases.AlbumId = Albums.Id - WHERE Albums.Id = Tracks.AlbumId)"); + Execute.Sql(@"UPDATE ""Tracks"" + SET ""AlbumReleaseId"" = (SELECT ""AlbumReleases"".""Id"" + FROM ""AlbumReleases"" + JOIN ""Albums"" ON ""AlbumReleases"".""AlbumId"" = ""Albums"".""Id"" + WHERE ""Albums"".""Id"" = ""Tracks"".""AlbumId"")"); // Set metadata ID - Execute.Sql(@"UPDATE Tracks - SET ArtistMetadataId = (SELECT ArtistMetadata.Id - FROM ArtistMetadata - JOIN Albums ON ArtistMetadata.Id = Albums.ArtistMetadataId - WHERE Tracks.AlbumId = Albums.Id)"); + Execute.Sql(@"UPDATE ""Tracks"" + SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id"" + FROM ""ArtistMetadata"" + JOIN ""Albums"" ON ""ArtistMetadata"".""Id"" = ""Albums"".""ArtistMetadataId"" + WHERE ""Tracks"".""AlbumId"" = ""Albums"".""Id"")"); // CLEAR OUT OLD COLUMNS @@ -188,15 +188,15 @@ namespace NzbDrone.Core.Datastore.Migration public List Label { get; set; } } - private List ReadReleasesFromAlbums(IDbConnection conn, IDbTransaction tran) + private List ReadReleasesFromAlbums(IDbConnection conn, IDbTransaction tran) { // need to get all the old albums - var releases = new List(); + var releases = new List(); using (var getReleasesCmd = conn.CreateCommand()) { getReleasesCmd.Transaction = tran; - getReleasesCmd.CommandText = @"SELECT Id, CurrentRelease FROM Albums"; + getReleasesCmd.CommandText = @"SELECT ""Id"", ""CurrentRelease"" FROM ""Albums"""; using (var releaseReader = getReleasesCmd.ExecuteReader()) { @@ -205,16 +205,16 @@ namespace NzbDrone.Core.Datastore.Migration int albumId = releaseReader.GetInt32(0); var albumRelease = Json.Deserialize(releaseReader.GetString(1)); - AlbumRelease toInsert = null; + AlbumRelease023 toInsert = null; if (albumRelease != null) { - var media = new List(); + var media = new List(); for (var i = 1; i <= Math.Max(albumRelease.MediaCount, 1); i++) { - media.Add(new Medium { Number = i, Name = "", Format = albumRelease.Format ?? "Unknown" }); + media.Add(new Medium023 { Number = i, Name = "", Format = albumRelease.Format ?? "Unknown" }); } - toInsert = new AlbumRelease + toInsert = new AlbumRelease023 { AlbumId = albumId, ForeignReleaseId = albumRelease.Id.IsNotNullOrWhiteSpace() ? albumRelease.Id : albumId.ToString(), @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Datastore.Migration } else { - toInsert = new AlbumRelease + toInsert = new AlbumRelease023 { AlbumId = albumId, ForeignReleaseId = albumId.ToString(), @@ -239,7 +239,7 @@ namespace NzbDrone.Core.Datastore.Migration Status = "", Label = new List(), Country = new List(), - Media = new List { new Medium { Name = "Unknown", Number = 1, Format = "Unknown" } }, + Media = new List { new Medium023 { Name = "Unknown", Number = 1, Format = "Unknown" } }, Monitored = true }; } @@ -252,31 +252,54 @@ namespace NzbDrone.Core.Datastore.Migration return releases; } - private void WriteReleasesToReleases(List releases, IDbConnection conn, IDbTransaction tran) + private void WriteReleasesToReleases(List releases, IDbConnection conn, IDbTransaction tran) { + var dbReleases = new List(); + foreach (var release in releases) { - using (var writeReleaseCmd = conn.CreateCommand()) + dbReleases.Add(new { - writeReleaseCmd.Transaction = tran; - writeReleaseCmd.CommandText = - "INSERT INTO AlbumReleases (AlbumId, ForeignReleaseId, Title, Status, Duration, Label, Disambiguation, Country, Media, TrackCount, Monitored) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - writeReleaseCmd.AddParameter(release.AlbumId); - writeReleaseCmd.AddParameter(release.ForeignReleaseId); - writeReleaseCmd.AddParameter(release.Title); - writeReleaseCmd.AddParameter(release.Status); - writeReleaseCmd.AddParameter(release.Duration); - writeReleaseCmd.AddParameter(release.Label.ToJson()); - writeReleaseCmd.AddParameter(release.Disambiguation); - writeReleaseCmd.AddParameter(release.Country.ToJson()); - writeReleaseCmd.AddParameter(release.Media.ToJson()); - writeReleaseCmd.AddParameter(release.TrackCount); - writeReleaseCmd.AddParameter(release.Monitored); - - writeReleaseCmd.ExecuteNonQuery(); - } + AlbumId = release.AlbumId, + ForeignReleaseId = release.ForeignReleaseId, + Title = release.Title, + Status = release.Status, + Duration = release.Duration, + Label = release.Label.ToJson(), + Disambiguation = release.Disambiguation, + Country = release.Country.ToJson(), + Media = release.Media.ToJson(), + TrackCount = release.TrackCount, + Monitored = release.Monitored + }); } + + var updateSql = "INSERT INTO \"AlbumReleases\" (\"AlbumId\", \"ForeignReleaseId\", \"Title\", \"Status\", \"Duration\", \"Label\", \"Disambiguation\", \"Country\", \"Media\", \"TrackCount\", \"Monitored\") " + + "VALUES (@AlbumId, @ForeignReleaseId, @Title, @Status, @Duration, @Label, @Disambiguation, @Country, @Media, @TrackCount, @Monitored)"; + + conn.Execute(updateSql, dbReleases, transaction: tran); + } + + public class AlbumRelease023 + { + public int AlbumId { get; set; } + public string ForeignReleaseId { get; set; } + public string Title { get; set; } + public string Status { get; set; } + public int Duration { get; set; } + public List Label { get; set; } + public string Disambiguation { get; set; } + public List Country { get; set; } + public List Media { get; set; } + public int TrackCount { get; set; } + public bool Monitored { get; set; } + } + + public class Medium023 + { + public int Number { get; set; } + public string Name { get; set; } + public string Format { get; set; } } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/026_rename_quality_profiles_add_upgrade_allowed.cs b/src/NzbDrone.Core/Datastore/Migration/026_rename_quality_profiles_add_upgrade_allowed.cs index 7263f9217..4bf67b5d7 100644 --- a/src/NzbDrone.Core/Datastore/Migration/026_rename_quality_profiles_add_upgrade_allowed.cs +++ b/src/NzbDrone.Core/Datastore/Migration/026_rename_quality_profiles_add_upgrade_allowed.cs @@ -10,8 +10,8 @@ namespace NzbDrone.Core.Datastore.Migration { Rename.Table("Profiles").To("QualityProfiles"); - Alter.Table("QualityProfiles").AddColumn("UpgradeAllowed").AsInt32().Nullable(); - Alter.Table("LanguageProfiles").AddColumn("UpgradeAllowed").AsInt32().Nullable(); + Alter.Table("QualityProfiles").AddColumn("UpgradeAllowed").AsBoolean().Nullable(); + Alter.Table("LanguageProfiles").AddColumn("UpgradeAllowed").AsBoolean().Nullable(); // Set upgrade allowed for existing profiles (default will be false for new profiles) Update.Table("QualityProfiles").Set(new { UpgradeAllowed = true }).AllRows(); diff --git a/src/NzbDrone.Core/Datastore/Migration/028_clean_artistmetadata_table.cs b/src/NzbDrone.Core/Datastore/Migration/028_clean_artistmetadata_table.cs index d5e491517..b1d33eda3 100644 --- a/src/NzbDrone.Core/Datastore/Migration/028_clean_artistmetadata_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/028_clean_artistmetadata_table.cs @@ -9,47 +9,47 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { // Remove any artists linked to missing metadata - Execute.Sql(@"DELETE FROM Artists - WHERE Id in ( - SELECT Artists.Id from Artists - LEFT OUTER JOIN ArtistMetadata ON Artists.ArtistMetadataId = ArtistMetadata.Id - WHERE ArtistMetadata.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""Artists"" + WHERE ""Id"" in ( + SELECT ""Artists"".""Id"" from ""Artists"" + LEFT OUTER JOIN ""ArtistMetadata"" ON ""Artists"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id"" + WHERE ""ArtistMetadata"".""Id"" IS NULL)"); // Remove any albums linked to missing metadata - Execute.Sql(@"DELETE FROM Albums - WHERE Id in ( - SELECT Albums.Id from Albums - LEFT OUTER JOIN ArtistMetadata ON Albums.ArtistMetadataId = ArtistMetadata.Id - WHERE ArtistMetadata.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""Albums"" + WHERE ""Id"" in ( + SELECT ""Albums"".""Id"" from ""Albums"" + LEFT OUTER JOIN ""ArtistMetadata"" ON ""Albums"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id"" + WHERE ""ArtistMetadata"".""Id"" IS NULL)"); // Remove any album releases linked to albums that were deleted - Execute.Sql(@"DELETE FROM AlbumReleases - WHERE Id in ( - SELECT AlbumReleases.Id from AlbumReleases - LEFT OUTER JOIN Albums ON Albums.Id = AlbumReleases.AlbumId - WHERE Albums.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""AlbumReleases"" + WHERE ""Id"" in ( + SELECT ""AlbumReleases"".""Id"" from ""AlbumReleases"" + LEFT OUTER JOIN ""Albums"" ON ""Albums"".""Id"" = ""AlbumReleases"".""AlbumId"" + WHERE ""Albums"".""Id"" IS NULL)"); // Remove any tracks linked to album releases that were deleted - Execute.Sql(@"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)"); + Execute.Sql(@"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)"); // Remove any tracks linked to the original missing metadata - Execute.Sql(@"DELETE FROM Tracks - WHERE Id in ( - SELECT Tracks.Id from Tracks - LEFT OUTER JOIN ArtistMetadata ON Tracks.ArtistMetadataId = ArtistMetadata.Id - WHERE ArtistMetadata.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""Tracks"" + WHERE ""Id"" in ( + SELECT ""Tracks"".""Id"" from ""Tracks"" + LEFT OUTER JOIN ""ArtistMetadata"" ON ""Tracks"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id"" + WHERE ""ArtistMetadata"".""Id"" IS NULL)"); // Remove any trackfiles linked to the deleted tracks - Execute.Sql(@"DELETE FROM TrackFiles - WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - LEFT OUTER JOIN Tracks - ON TrackFiles.Id = Tracks.TrackFileId - WHERE Tracks.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""TrackFiles"" + WHERE ""Id"" IN ( + SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles"" + LEFT OUTER JOIN ""Tracks"" + ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId"" + WHERE ""Tracks"".""Id"" IS NULL)"); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/029_health_issue_notification.cs b/src/NzbDrone.Core/Datastore/Migration/029_health_issue_notification.cs index 4407daef6..4efbf3d4b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/029_health_issue_notification.cs +++ b/src/NzbDrone.Core/Datastore/Migration/029_health_issue_notification.cs @@ -9,11 +9,11 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("Notifications").AddColumn("OnHealthIssue").AsBoolean().WithDefaultValue(0); - Alter.Table("Notifications").AddColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(0); - Alter.Table("Notifications").AddColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(0); - Alter.Table("Notifications").AddColumn("OnImportFailure").AsBoolean().WithDefaultValue(0); - Alter.Table("Notifications").AddColumn("OnTrackRetag").AsBoolean().WithDefaultValue(0); + Alter.Table("Notifications").AddColumn("OnHealthIssue").AsBoolean().WithDefaultValue(false); + Alter.Table("Notifications").AddColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(false); + Alter.Table("Notifications").AddColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(false); + Alter.Table("Notifications").AddColumn("OnImportFailure").AsBoolean().WithDefaultValue(false); + Alter.Table("Notifications").AddColumn("OnTrackRetag").AsBoolean().WithDefaultValue(false); Delete.Column("OnDownload").FromTable("Notifications"); diff --git a/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs b/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs index 971f1cc41..32d76bf76 100644 --- a/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs +++ b/src/NzbDrone.Core/Datastore/Migration/030_add_mediafilerepository_mtime.cs @@ -13,45 +13,45 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("TrackFiles").AddColumn("Path").AsString().Nullable(); // Remove anything where RelativePath is null - Execute.Sql(@"DELETE FROM TrackFiles WHERE RelativePath IS NULL"); + Execute.Sql(@"DELETE FROM ""TrackFiles"" WHERE ""RelativePath"" IS NULL"); // Remove anything not linked to a track (these shouldn't be present in version < 30) - Execute.Sql(@"DELETE FROM TrackFiles - WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId - WHERE Tracks.Id IS NULL)"); + Execute.Sql(@"DELETE FROM ""TrackFiles"" + WHERE ""Id"" IN ( + SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles"" + LEFT JOIN ""Tracks"" ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId"" + WHERE ""Tracks"".""Id"" IS NULL)"); // Remove anything where we can't get an artist path (i.e. we don't know where it is) - Execute.Sql(@"DELETE FROM TrackFiles - WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - LEFT JOIN Albums ON TrackFiles.AlbumId = Albums.Id - LEFT JOIN Artists on Artists.ArtistMetadataId = Albums.ArtistMetadataId - WHERE Artists.Path IS NULL)"); + Execute.Sql(@"DELETE FROM ""TrackFiles"" + WHERE ""Id"" IN ( + SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles"" + LEFT JOIN ""Albums"" ON ""TrackFiles"".""AlbumId"" = ""Albums"".""Id"" + LEFT JOIN ""Artists"" ON ""Artists"".""ArtistMetadataId"" = ""Albums"".""ArtistMetadataId"" + WHERE ""Artists"".""Path"" IS NULL)"); // Remove anything linked to unmonitored or unidentified releases. This should ensure uniqueness of track files. - Execute.Sql(@"DELETE FROM TrackFiles - WHERE Id IN ( - SELECT TrackFiles.Id FROM TrackFiles - LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId - LEFT JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id - WHERE AlbumReleases.Monitored = 0 - OR AlbumReleases.Monitored IS NULL)"); + Execute.Sql(@"DELETE FROM ""TrackFiles"" + WHERE ""Id"" IN ( + SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles"" + LEFT JOIN ""Tracks"" ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId"" + LEFT JOIN ""AlbumReleases"" ON ""Tracks"".""AlbumReleaseId"" = ""AlbumReleases"".""Id"" + WHERE ""AlbumReleases"".""Monitored"" = false + OR ""AlbumReleases"".""Monitored"" IS NULL)"); // Populate the full paths - Execute.Sql(@"UPDATE TrackFiles - SET Path = (SELECT Artists.Path || '" + System.IO.Path.DirectorySeparatorChar + @"' || TrackFiles.RelativePath - FROM Artists - JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId - WHERE TrackFiles.AlbumId = Albums.Id)"); + Execute.Sql(@"UPDATE ""TrackFiles"" + SET ""Path"" = (SELECT ""Artists"".""Path"" || '" + System.IO.Path.DirectorySeparatorChar + @"' || ""TrackFiles"".""RelativePath"" + FROM ""Artists"" + JOIN ""Albums"" ON ""Albums"".""ArtistMetadataId"" = ""Artists"".""ArtistMetadataId"" + WHERE ""TrackFiles"".""AlbumId"" = ""Albums"".""Id"")"); // Belt and braces to ensure uniqueness - Execute.Sql(@"DELETE FROM TrackFiles - WHERE rowid NOT IN ( - SELECT min(rowid) - FROM TrackFiles - GROUP BY Path + Execute.Sql(@"DELETE FROM ""TrackFiles"" + WHERE ""Id"" NOT IN ( + SELECT MIN(""Id"") + FROM ""TrackFiles"" + GROUP BY ""Path"" )"); // Now enforce the uniqueness constraint diff --git a/src/NzbDrone.Core/Datastore/Migration/031_add_artistmetadataid_constraint.cs b/src/NzbDrone.Core/Datastore/Migration/031_add_artistmetadataid_constraint.cs index 0f7fda558..a6056a23b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/031_add_artistmetadataid_constraint.cs +++ b/src/NzbDrone.Core/Datastore/Migration/031_add_artistmetadataid_constraint.cs @@ -9,11 +9,11 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { // Remove any duplicate artists - Execute.Sql(@"DELETE FROM Artists - WHERE Id NOT IN ( - SELECT MIN(Artists.id) from Artists - JOIN ArtistMetadata ON Artists.ArtistMetadataId = ArtistMetadata.Id - GROUP BY ArtistMetadata.Id)"); + Execute.Sql(@"DELETE FROM ""Artists"" + WHERE ""Id"" NOT IN ( + SELECT MIN(""Artists"".""Id"") from ""Artists"" + JOIN ""ArtistMetadata"" ON ""Artists"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id"" + GROUP BY ""ArtistMetadata"".""Id"")"); // The index exists but will be recreated as part of unique constraint Delete.Index().OnTable("Artists").OnColumn("ArtistMetadataId"); diff --git a/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs b/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs index d47a4b4d1..1f6f793c9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs +++ b/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { Execute.WithConnection(SetConfigValue); - Execute.Sql("DELETE FROM Config WHERE Key = 'autodownloadpropers'"); + Execute.Sql("DELETE FROM \"Config\" WHERE \"Key\" = 'autodownloadpropers'"); } private void SetConfigValue(IDbConnection conn, IDbTransaction tran) @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var cmd = conn.CreateCommand()) { cmd.Transaction = tran; - cmd.CommandText = "SELECT Value FROM Config WHERE Key = 'autodownloadpropers'"; + cmd.CommandText = "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'autodownloadpropers'"; using (var reader = cmd.ExecuteReader()) { @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Datastore.Migration using (var updateCmd = conn.CreateCommand()) { updateCmd.Transaction = tran; - updateCmd.CommandText = "INSERT INTO Config (key, value) VALUES ('downloadpropersandrepacks', ?)"; + updateCmd.CommandText = "INSERT INTO \"Config\" (\"key\", \"value\") VALUES ('downloadpropersandrepacks', ?)"; updateCmd.AddParameter(newValue); updateCmd.ExecuteNonQuery(); diff --git a/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs b/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs index 493b60013..6dd2c8873 100644 --- a/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs +++ b/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Datastore.Migration protected override void MainDbUpgrade() { Alter.Table("NamingConfig").AddColumn("MultiDiscTrackFormat").AsString().Nullable(); - Execute.Sql("UPDATE NamingConfig SET MultiDiscTrackFormat = '{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}'"); + Execute.Sql("UPDATE \"NamingConfig\" SET \"MultiDiscTrackFormat\" = '{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}'"); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/036_add_download_client_priority.cs b/src/NzbDrone.Core/Datastore/Migration/036_add_download_client_priority.cs index 3c3e1192e..53149727a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_add_download_client_priority.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_add_download_client_priority.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Data; +using System.Linq; +using Dapper; using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration { - [Migration(36)] + [Migration(036)] public class add_download_client_priority : NzbDroneMigrationBase { // Need snapshot in time without having to instantiate. @@ -22,34 +24,43 @@ namespace NzbDrone.Core.Datastore.Migration private void InitPriorityForBackwardCompatibility(IDbConnection conn, IDbTransaction tran) { - using (var cmd = conn.CreateCommand()) + var downloadClients = conn.Query($"SELECT \"Id\", \"Implementation\" FROM \"DownloadClients\" WHERE \"Enable\""); + + if (!downloadClients.Any()) { - cmd.Transaction = tran; - cmd.CommandText = "SELECT Id, Implementation FROM DownloadClients WHERE Enable = 1"; + return; + } + + var nextUsenet = 1; + var nextTorrent = 1; - using (var reader = cmd.ExecuteReader()) + foreach (var downloadClient in downloadClients) + { + var isUsenet = _usenetImplementations.Contains(downloadClient.Implementation); + using (var updateCmd = conn.CreateCommand()) { - int nextUsenet = 1; - int nextTorrent = 1; - while (reader.Read()) + updateCmd.Transaction = tran; + if (conn.GetType().FullName == "Npgsql.NpgsqlConnection") { - var id = reader.GetInt32(0); - var implName = reader.GetString(1); - - var isUsenet = _usenetImplementations.Contains(implName); + updateCmd.CommandText = "UPDATE \"DownloadClients\" SET \"Priority\" = $1 WHERE \"Id\" = $2"; + } + else + { + updateCmd.CommandText = "UPDATE \"DownloadClients\" SET \"Priority\" = ? WHERE \"Id\" = ?"; + } - using (var updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Priority = ? WHERE Id = ?"; - updateCmd.AddParameter(isUsenet ? nextUsenet++ : nextTorrent++); - updateCmd.AddParameter(id); + updateCmd.AddParameter(isUsenet ? nextUsenet++ : nextTorrent++); + updateCmd.AddParameter(downloadClient.Id); - updateCmd.ExecuteNonQuery(); - } - } + updateCmd.ExecuteNonQuery(); } } } } + + public class DownloadClients036 + { + public int Id { get; set; } + public string Implementation { get; set; } + } } diff --git a/src/NzbDrone.Core/Datastore/Migration/039_add_root_folder_add_defaults.cs b/src/NzbDrone.Core/Datastore/Migration/039_add_root_folder_add_defaults.cs index 9815e1ab2..87570fd48 100644 --- a/src/NzbDrone.Core/Datastore/Migration/039_add_root_folder_add_defaults.cs +++ b/src/NzbDrone.Core/Datastore/Migration/039_add_root_folder_add_defaults.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Datastore.Migration Alter.Table("RootFolders").AddColumn("DefaultMonitorOption").AsInt32().WithDefaultValue(0); Alter.Table("RootFolders").AddColumn("DefaultTags").AsString().Nullable(); - Execute.WithConnection(SetDefaultOptions); + IfDatabase("sqlite").Execute.WithConnection(SetDefaultOptions); } private void SetDefaultOptions(IDbConnection conn, IDbTransaction tran) diff --git a/src/NzbDrone.Core/Datastore/Migration/042_remove_album_folders.cs b/src/NzbDrone.Core/Datastore/Migration/042_remove_album_folders.cs index a0a54f90a..eccaf4209 100644 --- a/src/NzbDrone.Core/Datastore/Migration/042_remove_album_folders.cs +++ b/src/NzbDrone.Core/Datastore/Migration/042_remove_album_folders.cs @@ -10,8 +10,8 @@ namespace NzbDrone.Core.Datastore.Migration { Delete.Column("AlbumFolder").FromTable("Artists"); - Execute.Sql("UPDATE NamingConfig SET StandardTrackFormat = AlbumFolderFormat || '/' || StandardTrackFormat"); - Execute.Sql("UPDATE NamingConfig SET MultiDiscTrackFormat = AlbumFolderFormat || '/' || MultiDiscTrackFormat"); + Execute.Sql("UPDATE \"NamingConfig\" SET \"StandardTrackFormat\" = \"AlbumFolderFormat\" || '/' || \"StandardTrackFormat\""); + Execute.Sql("UPDATE \"NamingConfig\" SET \"MultiDiscTrackFormat\" = \"AlbumFolderFormat\" || '/' || \"MultiDiscTrackFormat\""); Delete.Column("AlbumFolderFormat").FromTable("NamingConfig"); } diff --git a/src/NzbDrone.Core/Datastore/Migration/045_remove_chown_and_folderchmod_config.cs b/src/NzbDrone.Core/Datastore/Migration/045_remove_chown_and_folderchmod_config.cs index 7e4df00ce..f98400841 100644 --- a/src/NzbDrone.Core/Datastore/Migration/045_remove_chown_and_folderchmod_config.cs +++ b/src/NzbDrone.Core/Datastore/Migration/045_remove_chown_and_folderchmod_config.cs @@ -11,8 +11,8 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')"); - Execute.WithConnection(ConvertFileChmodToFolderChmod); + IfDatabase("sqlite").Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')"); + IfDatabase("sqlite").Execute.WithConnection(ConvertFileChmodToFolderChmod); } private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran) diff --git a/src/NzbDrone.Core/Datastore/Migration/047_update_notifiarr.cs b/src/NzbDrone.Core/Datastore/Migration/047_update_notifiarr.cs index 15d145df1..826ebc4db 100644 --- a/src/NzbDrone.Core/Datastore/Migration/047_update_notifiarr.cs +++ b/src/NzbDrone.Core/Datastore/Migration/047_update_notifiarr.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("UPDATE Notifications SET Implementation = Replace(Implementation, 'DiscordNotifier', 'Notifiarr'),ConfigContract = Replace(ConfigContract, 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE Implementation = 'DiscordNotifier';"); + Execute.Sql("UPDATE \"Notifications\" SET \"Implementation\" = Replace(\"Implementation\", 'DiscordNotifier', 'Notifiarr'),\"ConfigContract\" = Replace(\"ConfigContract\", 'DiscordNotifierSettings', 'NotifiarrSettings') WHERE \"Implementation\" = 'DiscordNotifier';"); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/049_email_multiple_addresses.cs b/src/NzbDrone.Core/Datastore/Migration/049_email_multiple_addresses.cs index 0ac6ad989..a1c11fcc6 100644 --- a/src/NzbDrone.Core/Datastore/Migration/049_email_multiple_addresses.cs +++ b/src/NzbDrone.Core/Datastore/Migration/049_email_multiple_addresses.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Datastore.Migration private void ChangeEmailAddressType(IDbConnection conn, IDbTransaction tran) { - var rows = conn.Query($"SELECT Id, Settings FROM Notifications WHERE Implementation = 'Email'"); + var rows = conn.Query($"SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'"); var corrected = new List(); @@ -62,7 +62,7 @@ namespace NzbDrone.Core.Datastore.Migration }); } - var updateSql = "UPDATE Notifications SET Settings = @Settings WHERE Id = @Id"; + var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; conn.Execute(updateSql, corrected, transaction: tran); } diff --git a/src/NzbDrone.Core/Datastore/Migration/051_cdh_per_downloadclient.cs b/src/NzbDrone.Core/Datastore/Migration/051_cdh_per_downloadclient.cs index ed1a44e44..a49770ff2 100644 --- a/src/NzbDrone.Core/Datastore/Migration/051_cdh_per_downloadclient.cs +++ b/src/NzbDrone.Core/Datastore/Migration/051_cdh_per_downloadclient.cs @@ -1,8 +1,5 @@ using System.Data; -using System.Linq; using FluentMigrator; -using Newtonsoft.Json.Linq; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration @@ -24,7 +21,7 @@ namespace NzbDrone.Core.Datastore.Migration var removeCompletedDownloads = false; var removeFailedDownloads = true; - using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removecompleteddownloads'")) + using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'removecompleteddownloads'")) { if ((removeCompletedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "true") { @@ -32,7 +29,7 @@ namespace NzbDrone.Core.Datastore.Migration } } - using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removefaileddownloads'")) + using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT \"Value\" FROM \"Config\" WHERE \"Key\" = 'removefaileddownloads'")) { if ((removeFailedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "false") { @@ -40,14 +37,25 @@ namespace NzbDrone.Core.Datastore.Migration } } - using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN (\"RTorrent\", \"Flood\") THEN 0 ELSE ? END), RemoveFailedDownloads = ?")) + string commandText; + + if (conn.GetType().FullName == "Npgsql.NpgsqlConnection") + { + commandText = $"UPDATE \"DownloadClients\" SET \"RemoveCompletedDownloads\" = (CASE WHEN \"Implementation\" IN ('RTorrent', 'Flood') THEN 'false' ELSE $1 END), \"RemoveFailedDownloads\" = $2"; + } + else + { + commandText = $"UPDATE \"DownloadClients\" SET \"RemoveCompletedDownloads\" = (CASE WHEN \"Implementation\" IN ('RTorrent', 'Flood') THEN 'false' ELSE ? END), \"RemoveFailedDownloads\" = ?"; + } + + using (var updateClientCmd = conn.CreateCommand(tran, commandText)) { - updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0); - updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0); + updateClientCmd.AddParameter(removeCompletedDownloads); + updateClientCmd.AddParameter(removeFailedDownloads); updateClientCmd.ExecuteNonQuery(); } - using (var removeConfigCmd = conn.CreateCommand(tran, $"DELETE FROM Config WHERE Key IN ('removecompleteddownloads', 'removefaileddownloads')")) + using (var removeConfigCmd = conn.CreateCommand(tran, $"DELETE FROM \"Config\" WHERE \"Key\" IN ('removecompleteddownloads', 'removefaileddownloads')")) { removeConfigCmd.ExecuteNonQuery(); } diff --git a/src/NzbDrone.Core/Datastore/Migration/052_download_history.cs b/src/NzbDrone.Core/Datastore/Migration/052_download_history.cs index b536534f1..8c6319103 100644 --- a/src/NzbDrone.Core/Datastore/Migration/052_download_history.cs +++ b/src/NzbDrone.Core/Datastore/Migration/052_download_history.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Datastore.Migration Create.Index().OnTable("DownloadHistory").OnColumn("ArtistId"); Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId"); - Execute.WithConnection(InitialImportedDownloadHistory); + IfDatabase("sqlite").Execute.WithConnection(InitialImportedDownloadHistory); } private static readonly Dictionary EventTypeMap = new Dictionary() diff --git a/src/NzbDrone.Core/Datastore/Migration/057_import_list_search.cs b/src/NzbDrone.Core/Datastore/Migration/057_import_list_search.cs index 8f1d6a5c7..be9ffce19 100644 --- a/src/NzbDrone.Core/Datastore/Migration/057_import_list_search.cs +++ b/src/NzbDrone.Core/Datastore/Migration/057_import_list_search.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("ImportLists").AddColumn("ShouldSearch").AsInt32().WithDefaultValue(1); + Alter.Table("ImportLists").AddColumn("ShouldSearch").AsBoolean().WithDefaultValue(true); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/058_import_list_monitor_existing.cs b/src/NzbDrone.Core/Datastore/Migration/058_import_list_monitor_existing.cs index f578ab013..c928a6930 100644 --- a/src/NzbDrone.Core/Datastore/Migration/058_import_list_monitor_existing.cs +++ b/src/NzbDrone.Core/Datastore/Migration/058_import_list_monitor_existing.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsInt32().WithDefaultValue(0); + Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsBoolean().WithDefaultValue(false); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs b/src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs index e45340a3c..764108246 100644 --- a/src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs +++ b/src/NzbDrone.Core/Datastore/Migration/059_indexer_tags.cs @@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Omgwtfnzbs'"); - Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Waffles'"); + Delete.FromTable("Indexers").Row(new { Implementation = "Omgwtfnzbs" }); + Delete.FromTable("Indexers").Row(new { Implementation = "Waffles" }); Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable(); } diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index 35c5c6b59..1249dfd8b 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Reflection; using FluentMigrator.Runner; +using FluentMigrator.Runner.Generators; using FluentMigrator.Runner.Initialization; using FluentMigrator.Runner.Processors; using Microsoft.Extensions.DependencyInjection; @@ -34,11 +35,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework _logger.Info("*** Migrating {0} ***", connectionString); - var serviceProvider = new ServiceCollection() + ServiceProvider serviceProvider; + + var db = connectionString.Contains(".db") ? "sqlite" : "postgres"; + + serviceProvider = new ServiceCollection() .AddLogging(b => b.AddNLog()) .AddFluentMigratorCore() .ConfigureRunner( builder => builder + .AddPostgres() .AddNzbDroneSQLite() .WithGlobalConnectionString(connectionString) .WithMigrationsIn(Assembly.GetExecutingAssembly())) @@ -48,6 +54,14 @@ namespace NzbDrone.Core.Datastore.Migration.Framework opt.PreviewOnly = false; opt.Timeout = TimeSpan.FromSeconds(60); }) + .Configure(cfg => + { + cfg.ProcessorId = db; + }) + .Configure(cfg => + { + cfg.GeneratorId = db; + }) .BuildServiceProvider(); using (var scope = serviceProvider.CreateScope()) diff --git a/src/NzbDrone.Core/Datastore/PostgresOptions.cs b/src/NzbDrone.Core/Datastore/PostgresOptions.cs new file mode 100644 index 000000000..1daa94500 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/PostgresOptions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Configuration; + +namespace NzbDrone.Core.Datastore +{ + public class PostgresOptions + { + public string Host { get; set; } + public int Port { get; set; } + public string User { get; set; } + public string Password { get; set; } + public string MainDb { get; set; } + public string LogDb { get; set; } + + public static PostgresOptions GetOptions() + { + var config = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + var postgresOptions = new PostgresOptions(); + config.GetSection("Lidarr:Postgres").Bind(postgresOptions); + + return postgresOptions; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/SqlBuilder.cs b/src/NzbDrone.Core/Datastore/SqlBuilder.cs index e686d4852..8febd5b6a 100644 --- a/src/NzbDrone.Core/Datastore/SqlBuilder.cs +++ b/src/NzbDrone.Core/Datastore/SqlBuilder.cs @@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore public class SqlBuilder { private readonly Dictionary _data = new Dictionary(); + private readonly DatabaseType _databaseType; + + public SqlBuilder(DatabaseType databaseType) + { + _databaseType = databaseType; + } public int Sequence { get; private set; } + public DatabaseType DatabaseType => _databaseType; + public Template AddTemplate(string sql, dynamic parameters = null) => new Template(this, sql, parameters); diff --git a/src/NzbDrone.Core/Datastore/TableMapper.cs b/src/NzbDrone.Core/Datastore/TableMapper.cs index f1c3ef7e1..24ab5869e 100644 --- a/src/NzbDrone.Core/Datastore/TableMapper.cs +++ b/src/NzbDrone.Core/Datastore/TableMapper.cs @@ -49,17 +49,17 @@ namespace NzbDrone.Core.Datastore public string SelectTemplate(Type x) { - return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; } public string DeleteTemplate(Type x) { - return $"DELETE FROM {TableMap[x]} /**where**/"; + return $"DELETE FROM \"{TableMap[x]}\" /**where**/"; } public string PageCountTemplate(Type x) { - return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/"; + return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/"; } public bool IsValidSortKey(string sortKey) @@ -90,6 +90,35 @@ namespace NzbDrone.Core.Datastore return true; } + + public string GetSortKey(string sortKey) + { + string table = null; + + if (sortKey.Contains('.')) + { + var split = sortKey.Split('.'); + if (split.Length != 2) + { + return sortKey; + } + + table = split[0]; + sortKey = split[1]; + } + + if (table != null && !TableMap.Values.Contains(table, StringComparer.OrdinalIgnoreCase)) + { + return sortKey; + } + + if (!_allowedOrderBy.Contains(sortKey)) + { + return sortKey; + } + + return _allowedOrderBy.First(x => x.Equals(sortKey, StringComparison.OrdinalIgnoreCase)); + } } public class LazyLoadedProperty @@ -154,7 +183,7 @@ namespace NzbDrone.Core.Datastore (db, parent) => { var id = childIdSelector(parent); - return db.Query(new SqlBuilder().Where(x => x.Id == id)).SingleOrDefault(); + return db.Query(new SqlBuilder(db.DatabaseType).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 096ec53b1..53f41f44e 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -105,24 +105,24 @@ namespace NzbDrone.Core.Datastore .HasOne(a => a.Metadata, a => a.ArtistMetadataId) .HasOne(a => a.QualityProfile, a => a.QualityProfileId) .HasOne(s => s.MetadataProfile, s => s.MetadataProfileId) - .LazyLoad(a => a.Albums, (db, a) => db.Query(new SqlBuilder().Where(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0); + .LazyLoad(a => a.Albums, (db, a) => db.Query(new SqlBuilder(db.DatabaseType).Where(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0); Mapper.Entity("ArtistMetadata").RegisterModel(); Mapper.Entity("Albums").RegisterModel() .Ignore(x => x.ArtistId) .HasOne(r => r.ArtistMetadata, r => r.ArtistMetadataId) - .LazyLoad(a => a.AlbumReleases, (db, album) => db.Query(new SqlBuilder().Where(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0) + .LazyLoad(a => a.AlbumReleases, (db, album) => db.Query(new SqlBuilder(db.DatabaseType).Where(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0) .LazyLoad(a => a.Artist, (db, album) => ArtistRepository.Query(db, - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((a, m) => a.ArtistMetadataId == m.Id) .Where(a => a.ArtistMetadataId == album.ArtistMetadataId)).SingleOrDefault(), a => a.ArtistMetadataId > 0); Mapper.Entity("AlbumReleases").RegisterModel() .HasOne(r => r.Album, r => r.AlbumId) - .LazyLoad(x => x.Tracks, (db, release) => db.Query(new SqlBuilder().Where(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0); + .LazyLoad(x => x.Tracks, (db, release) => db.Query(new SqlBuilder(db.DatabaseType).Where(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0); Mapper.Entity("Tracks").RegisterModel() .Ignore(t => t.HasFile) @@ -131,7 +131,7 @@ namespace NzbDrone.Core.Datastore .HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId) .LazyLoad(t => t.TrackFile, (db, track) => MediaFileRepository.Query(db, - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((l, r) => l.Id == r.TrackFileId) .Join((l, r) => l.AlbumId == r.Id) .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Datastore t => t.TrackFileId > 0) .LazyLoad(x => x.Artist, (db, t) => ArtistRepository.Query(db, - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((a, m) => a.ArtistMetadataId == m.Id) .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) .Join((l, r) => l.Id == r.AlbumId) @@ -149,10 +149,10 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("TrackFiles").RegisterModel() .HasOne(f => f.Album, f => f.AlbumId) - .LazyLoad(x => x.Tracks, (db, file) => db.Query(new SqlBuilder().Where(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0) + .LazyLoad(x => x.Tracks, (db, file) => db.Query(new SqlBuilder(db.DatabaseType).Where(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0) .LazyLoad(x => x.Artist, (db, f) => ArtistRepository.Query(db, - new SqlBuilder() + new SqlBuilder(db.DatabaseType) .Join((a, m) => a.ArtistMetadataId == m.Id) .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) .Where(a => a.Id == f.AlbumId)).SingleOrDefault(), diff --git a/src/NzbDrone.Core/Datastore/WhereBuilder.cs b/src/NzbDrone.Core/Datastore/WhereBuilder.cs index 4524ab289..0917e9d32 100644 --- a/src/NzbDrone.Core/Datastore/WhereBuilder.cs +++ b/src/NzbDrone.Core/Datastore/WhereBuilder.cs @@ -1,391 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using Dapper; +using Dapper; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Datastore { - public class WhereBuilder : ExpressionVisitor + public abstract 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; - } + public DynamicParameters Parameters { get; protected set; } } } diff --git a/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs b/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs new file mode 100644 index 000000000..b63e861c4 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs @@ -0,0 +1,389 @@ +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 WhereBuilderPostgres : WhereBuilder + { + 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 WhereBuilderPostgres(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); + } + } + + 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(" = ANY ("); + + // 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(" ILIKE '%' || "); + + Visit(body.Arguments[0]); + + _sb.Append(" || '%')"); + } + + private void ParseStartsWith(MethodCallExpression body) + { + _sb.Append('('); + + Visit(body.Object); + + _sb.Append(" ILIKE "); + + Visit(body.Arguments[0]); + + _sb.Append(" || '%')"); + } + + private void ParseEndsWith(MethodCallExpression body) + { + _sb.Append('('); + + Visit(body.Object); + + _sb.Append(" ILIKE '%' || "); + + 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/Datastore/WhereBuilderSqlite.cs b/src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs new file mode 100644 index 000000000..576182f7d --- /dev/null +++ b/src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs @@ -0,0 +1,389 @@ +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 WhereBuilderSqlite : WhereBuilder + { + 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 WhereBuilderSqlite(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); + } + } + + 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/History/EntityHistoryRepository.cs b/src/NzbDrone.Core/History/EntityHistoryRepository.cs index 07eded0fa..030a40e82 100644 --- a/src/NzbDrone.Core/History/EntityHistoryRepository.cs +++ b/src/NzbDrone.Core/History/EntityHistoryRepository.cs @@ -90,11 +90,11 @@ namespace NzbDrone.Core.History public List FindDownloadHistory(int idArtistId, QualityModel quality) { - var allowed = new[] { EntityHistoryEventType.Grabbed, EntityHistoryEventType.DownloadFailed, EntityHistoryEventType.TrackFileImported }; + var allowed = new[] { (int)EntityHistoryEventType.Grabbed, (int)EntityHistoryEventType.DownloadFailed, (int)EntityHistoryEventType.TrackFileImported }; return Query(h => h.ArtistId == idArtistId && h.Quality == quality && - allowed.Contains(h.EventType)); + allowed.Contains((int)h.EventType)); } public void DeleteForArtists(List artistIds) @@ -102,7 +102,7 @@ namespace NzbDrone.Core.History Delete(c => artistIds.Contains(c.ArtistId)); } - protected override SqlBuilder PagedBuilder() => new SqlBuilder() + protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType) .Join((h, a) => h.ArtistId == a.Id) .Join((h, a) => h.AlbumId == a.Id) .LeftJoin((h, t) => h.TrackId == t.Id); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs index 1293d4f07..8d50f05de 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAbsolutePathMetadataFiles.cs @@ -1,4 +1,4 @@ -using Dapper; +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -16,16 +16,32 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM MetadataFiles - WHERE Id IN ( - SELECT Id FROM MetadataFiles - WHERE RelativePath + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" = ANY ( + SELECT ""Id"" FROM ""MetadataFiles"" + WHERE ""RelativePath"" + LIKE '_:\\%' + OR ""RelativePath"" + LIKE '\\%' + OR ""RelativePath"" + LIKE '/%' + )"); + } + else + { + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT ""Id"" FROM ""MetadataFiles"" + WHERE ""RelativePath"" LIKE '_:\%' - OR RelativePath + OR ""RelativePath"" LIKE '\%' - OR RelativePath + OR ""RelativePath"" LIKE '/%' )"); + } } } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs index a1d9c56b7..33184f1fb 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalNamingSpecs.cs @@ -1,4 +1,4 @@ -using Dapper; +using Dapper; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM NamingConfig - WHERE ID NOT IN ( - SELECT ID FROM NamingConfig + 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 4460ac83c..3845110f4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupAdditionalUsers.cs @@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM Users - WHERE ID NOT IN ( - SELECT ID FROM Users + 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 1e331c98b..7e3cb6d81 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDownloadClientUnavailablePendingReleases.cs @@ -1,4 +1,4 @@ -using System; +using System; using Dapper; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.Pending; @@ -16,16 +16,29 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - using (var mapper = _database.OpenConnection()) + var mapper = _database.OpenConnection(); + + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + mapper.Execute(@"DELETE FROM ""PendingReleases"" + WHERE ""Added"" < @TwoWeeksAgo + AND ""Reason"" = ANY (@Reasons)", + new + { + TwoWeeksAgo = DateTime.UtcNow.AddDays(-14), + Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback } + }); + } + else { - 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 } - }); + 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 3b06fdba4..b46310850 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupDuplicateMetadataFiles.cs @@ -24,12 +24,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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 + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT MIN(""Id"") FROM ""MetadataFiles"" + WHERE ""Type"" = 1 + GROUP BY ""ArtistId"", ""Consumer"" + HAVING COUNT(""ArtistId"") > 1 )"); } } @@ -38,12 +38,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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 + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT MIN(""Id"") FROM ""MetadataFiles"" + WHERE ""Type"" = 6 + GROUP BY ""AlbumId"", ""Consumer"" + HAVING COUNT(""AlbumId"") > 1 )"); } } @@ -52,12 +52,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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 + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT MIN(""Id"") FROM ""MetadataFiles"" + WHERE ""Type"" = 2 + GROUP BY ""TrackFileId"", ""Consumer"" + HAVING COUNT(""TrackFileId"") > 1 )"); } } @@ -66,12 +66,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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 + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT MIN(""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 f80b93f07..ab921af81 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedAlbums.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 52f674312..500d85df8 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedArtistMetadata.cs @@ -16,13 +16,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 bc02d1b50..7beeddd13 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedBlacklist.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"DELETE FROM Blocklist - WHERE Id IN ( - SELECT Blocklist.Id FROM Blocklist - LEFT OUTER JOIN Artists - ON Blocklist.ArtistId = Artists.Id - WHERE Artists.Id IS NULL)"); + mapper.Execute(@"DELETE FROM ""Blocklist"" + WHERE ""Id"" IN ( + SELECT ""Blocklist"".""Id"" FROM ""Blocklist"" + LEFT OUTER JOIN ""Artists"" + ON ""Blocklist"".""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 91598ace5..124f4476d 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedDownloadClientStatus.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 43b298605..8fedcd873 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs @@ -22,12 +22,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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)"); } } @@ -35,12 +35,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 f40ec672f..1ceb0f396 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedImportListStatus.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 039db9b52..059f059e4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedIndexerStatus.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 2afd5feea..989cf9de4 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedMetadataFiles.cs @@ -25,12 +25,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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)"); } } @@ -38,13 +38,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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)"); } } @@ -52,13 +52,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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)"); } } @@ -66,11 +66,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + mapper.Execute(@"DELETE FROM ""MetadataFiles"" + WHERE ""Id"" IN ( + SELECT ""Id"" FROM ""MetadataFiles"" + WHERE ""Type"" IN (4, 6) + AND ""AlbumId"" = 0)"); } } @@ -78,11 +78,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 d1ce0fc3b..1dccc8f2a 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedPendingReleases.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 e2613f220..d401794c1 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedReleases.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 57657de83..62286c667 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTrackFiles.cs @@ -17,22 +17,22 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers 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 - LEFT OUTER JOIN Tracks - ON TrackFiles.Id = Tracks.TrackFileId - WHERE Tracks.Id IS NULL)"); + mapper.Execute(@"UPDATE ""TrackFiles"" + SET ""AlbumId"" = 0 + WHERE ""Id"" IN ( + SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles"" + LEFT OUTER JOIN ""Tracks"" + ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId"" + WHERE ""Tracks"".""Id"" IS NULL)"); // 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)"); + 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 ff2017c5c..6b26e5da9 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedTracks.cs @@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { 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)"); + 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 e164325e4..9431d04ff 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; using Dapper; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -24,15 +25,29 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers .Distinct() .ToArray(); - var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray()); + if (usedTags.Any()) + { + var usedTagsList = usedTags.Select(d => d.ToString()).Join(","); - mapper.Execute($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})"); + if (_database.DatabaseType == DatabaseType.PostgreSQL) + { + mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" = ANY (\'{{{usedTagsList}}}\'::int[])"); + } + else + { + mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})"); + } + } + else + { + mapper.Execute("DELETE FROM \"Tags\""); + } } } private int[] GetUsedTags(string table, IDbConnection mapper) { - return mapper.Query>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]' AND NOT Tags IS NULL") + return mapper.Query>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") .SelectMany(x => x) .Distinct() .ToArray(); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs index 82a7af7fa..47333e596 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/FixFutureRunScheduledTasks.cs @@ -26,9 +26,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers using (var mapper = _database.OpenConnection()) { - mapper.Execute(@"UPDATE ScheduledTasks - SET LastExecution = @time - WHERE LastExecution > @time", + mapper.Execute(@"UPDATE ""ScheduledTasks"" + SET ""LastExecution"" = @time + WHERE ""LastExecution"" > @time", new { time = DateTime.UtcNow }); } } diff --git a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs index ccca19073..0e0eee611 100644 --- a/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs +++ b/src/NzbDrone.Core/Instrumentation/DatabaseTarget.cs @@ -1,9 +1,11 @@ -using System.Data; +using System; +using System.Data; using System.Data.SQLite; using NLog; using NLog.Common; using NLog.Config; using NLog.Targets; +using Npgsql; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; @@ -13,7 +15,7 @@ namespace NzbDrone.Core.Instrumentation { public class DatabaseTarget : TargetWithLayout, IHandle { - private const string INSERT_COMMAND = "INSERT INTO [Logs]([Message],[Time],[Logger],[Exception],[ExceptionType],[Level]) " + + private const string INSERT_COMMAND = "INSERT INTO \"Logs\" (\"Message\",\"Time\",\"Logger\",\"Exception\",\"ExceptionType\",\"Level\") " + "VALUES(@Message,@Time,@Logger,@Exception,@ExceptionType,@Level)"; private readonly IConnectionStringFactory _connectionStringFactory; @@ -55,7 +57,6 @@ namespace NzbDrone.Core.Instrumentation { try { - using var connection = new SQLiteConnection(_connectionStringFactory.LogDbConnectionString).OpenAndReturn(); var log = new Log(); log.Time = logEvent.TimeStamp; log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage); @@ -84,16 +85,17 @@ namespace NzbDrone.Core.Instrumentation log.Level = logEvent.Level.Name; - var sqlCommand = new SQLiteCommand(INSERT_COMMAND, connection); + var connectionString = _connectionStringFactory.LogDbConnectionString; - sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message }); - sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() }); - sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger }); - sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception }); - sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType }); - sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level }); - - sqlCommand.ExecuteNonQuery(); + //TODO: Probably need more robust way to differentiate what's being used + if (connectionString.Contains(".db")) + { + WriteSqliteLog(log, connectionString); + } + else + { + WritePostgresLog(log, connectionString); + } } catch (SQLiteException ex) { @@ -102,6 +104,48 @@ namespace NzbDrone.Core.Instrumentation } } + private void WritePostgresLog(Log log, string connectionString) + { + using (var connection = + new NpgsqlConnection(connectionString)) + { + connection.Open(); + using (var sqlCommand = connection.CreateCommand()) + { + sqlCommand.CommandText = INSERT_COMMAND; + sqlCommand.Parameters.Add(new NpgsqlParameter("Message", DbType.String) { Value = log.Message }); + sqlCommand.Parameters.Add(new NpgsqlParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() }); + sqlCommand.Parameters.Add(new NpgsqlParameter("Logger", DbType.String) { Value = log.Logger }); + sqlCommand.Parameters.Add(new NpgsqlParameter("Exception", DbType.String) { Value = log.Exception == null ? DBNull.Value : log.Exception }); + sqlCommand.Parameters.Add(new NpgsqlParameter("ExceptionType", DbType.String) { Value = log.ExceptionType == null ? DBNull.Value : log.ExceptionType }); + sqlCommand.Parameters.Add(new NpgsqlParameter("Level", DbType.String) { Value = log.Level }); + + sqlCommand.ExecuteNonQuery(); + } + } + } + + private void WriteSqliteLog(Log log, string connectionString) + { + using (var connection = + SQLiteFactory.Instance.CreateConnection()) + { + connection.ConnectionString = connectionString; + connection.Open(); + using (var sqlCommand = connection.CreateCommand()) + { + sqlCommand.CommandText = INSERT_COMMAND; + sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message }); + sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() }); + sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger }); + sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception }); + sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType }); + sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level }); + sqlCommand.ExecuteNonQuery(); + } + } + } + public void Handle(ApplicationShutdownRequested message) { if (LogManager.Configuration != null && LogManager.Configuration.LoggingRules.Contains(Rule)) diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 34b027680..5b9b9adb8 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -11,6 +11,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 4dc606652..1a11a83a1 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -152,6 +152,7 @@ "CustomFilters": "Custom Filters", "CutoffHelpText": "Once this quality is reached Lidarr will no longer download albums", "CutoffUnmet": "Cutoff Unmet", + "Database": "Database", "Date": "Date", "DateAdded": "Date Added", "Dates": "Dates", diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 5148be7c4..cac31f4cc 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -31,7 +31,7 @@ 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 SqlBuilder Builder() => new SqlBuilder() + protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType) .LeftJoin((t, x) => t.Id == x.TrackFileId) .LeftJoin((t, a) => t.AlbumId == a.Id) .LeftJoin((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) @@ -90,7 +90,7 @@ namespace NzbDrone.Core.MediaFiles { //x.Id == null is converted to SQL, so warning incorrect #pragma warning disable CS0472 - return _database.Query(new SqlBuilder().Select(typeof(TrackFile)) + return _database.Query(new SqlBuilder(_database.DatabaseType).Select(typeof(TrackFile)) .LeftJoin((f, t) => f.Id == t.TrackFileId) .Where(t => t.Id == null)).ToList(); #pragma warning restore CS0472 @@ -117,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles { // ensure path ends with a single trailing path separator to avoid matching partial paths var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - return _database.Query(new SqlBuilder().Where(x => x.Path.StartsWith(safePath))).ToList(); + return _database.Query(new SqlBuilder(_database.DatabaseType).Where(x => x.Path.StartsWith(safePath))).ToList(); } public TrackFile GetFileWithPath(string path) @@ -128,7 +128,7 @@ namespace NzbDrone.Core.MediaFiles public List GetFileWithPath(List paths) { // use more limited join for speed - var builder = new SqlBuilder() + var builder = new SqlBuilder(_database.DatabaseType) .LeftJoin((f, t) => f.Id == t.TrackFileId); var dict = new Dictionary(); diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs b/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs index 7bb1416cb..00b24ad59 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Messaging.Commands public void OrphanStarted() { - var sql = @"UPDATE Commands SET Status = @Orphaned, EndedAt = @Ended WHERE Status = @Started"; + var sql = @"UPDATE ""Commands"" SET ""Status"" = @Orphaned, ""EndedAt"" = @Ended WHERE ""Status"" = @Started"; var args = new { Orphaned = (int)CommandStatus.Orphaned, diff --git a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs b/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs index a2419c382..4dbb18f22 100644 --- a/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/AlbumRepository.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music List GetLastAlbums(IEnumerable artistMetadataIds); List GetNextAlbums(IEnumerable artistMetadataIds); List GetAlbumsByArtistMetadataId(int artistMetadataId); - List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); + List GetAlbumsForRefresh(int artistMetadataId, List foreignIds); Album FindByTitle(int artistMetadataId, string title); Album FindById(string foreignAlbumId); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); @@ -44,17 +44,35 @@ namespace NzbDrone.Core.Music public List GetLastAlbums(IEnumerable artistMetadataIds) { 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)")); + + var inner = Builder() + .Select("MIN(\"Albums\".\"Id\") as id, MAX(\"Albums\".\"ReleaseDate\") as date") + .Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate < now) + .GroupBy(x => x.ArtistMetadataId) + .AddSelectTemplate(typeof(Album)); + + var outer = Builder() + .Join($"({inner.RawSql}) ids on ids.id = \"Albums\".\"Id\" and ids.date = \"Albums\".\"ReleaseDate\"") + .AddParameters(inner.Parameters); + + return Query(outer); } public List GetNextAlbums(IEnumerable artistMetadataIds) { 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)")); + + var inner = Builder() + .Select("MIN(\"Albums\".\"Id\") as id, MIN(\"Albums\".\"ReleaseDate\") as date") + .Where(x => artistMetadataIds.Contains(x.ArtistMetadataId) && x.ReleaseDate > now) + .GroupBy(x => x.ArtistMetadataId) + .AddSelectTemplate(typeof(Album)); + + var outer = Builder() + .Join($"({inner.RawSql}) ids on ids.id = \"Albums\".\"Id\" and ids.date = \"Albums\".\"ReleaseDate\"") + .AddParameters(inner.Parameters); + + return Query(outer); } public List GetAlbumsByArtistMetadataId(int artistMetadataId) @@ -62,7 +80,7 @@ namespace NzbDrone.Core.Music return Query(s => s.ArtistMetadataId == artistMetadataId); } - public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) + public List GetAlbumsForRefresh(int artistMetadataId, List foreignIds) { return Query(a => a.ArtistMetadataId == artistMetadataId || foreignIds.Contains(a.ForeignAlbumId)); } @@ -74,15 +92,17 @@ namespace NzbDrone.Core.Music //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); + private SqlBuilder AlbumsWithoutFilesBuilder(DateTime currentTime) + { + return 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); + } #pragma warning restore CS0472 public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) @@ -95,14 +115,16 @@ namespace NzbDrone.Core.Music return pagingSpec; } - 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 SqlBuilder AlbumsWhereCutoffUnmetBuilder(List qualitiesBelowCutoff) + { + return 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) + .Where(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)); + } private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) { @@ -112,7 +134,7 @@ namespace NzbDrone.Core.Music { foreach (var belowCutoff in profile.QualityIds) { - clauses.Add(string.Format("(Artists.[QualityProfileId] = {0} AND MIN(TrackFiles.Quality) LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); + clauses.Add(string.Format("(\"Artists\".\"QualityProfileId\" = {0} AND \"TrackFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); } } @@ -123,7 +145,7 @@ namespace NzbDrone.Core.Music { 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**/)"; + var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Album))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\""; pagingSpec.TotalRecords = GetPagedRecordCount(AlbumsWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Album)), pagingSpec, countTemplate); return pagingSpec; @@ -131,13 +153,15 @@ namespace NzbDrone.Core.Music public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) { - var builder = Builder().Where(rg => rg.ReleaseDate >= startDate && rg.ReleaseDate <= endDate); + SqlBuilder builder; + + 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); + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Where(e => e.Monitored == true); } return Query(builder); @@ -145,15 +169,17 @@ namespace NzbDrone.Core.Music 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); + SqlBuilder builder; + + 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); + .Join((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) + .Where(e => e.Monitored == true); } return Query(builder); @@ -200,6 +226,7 @@ namespace NzbDrone.Core.Music public List GetArtistAlbumsWithFiles(Artist artist) { 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) diff --git a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs b/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs index dfe0699e9..3e5d73765 100644 --- a/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/ArtistRepository.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Music { } - protected override SqlBuilder Builder() => new SqlBuilder() + protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType) .Join((a, m) => a.ArtistMetadataId == m.Id); protected override List Query(SqlBuilder builder) => Query(_database, builder).ToList(); @@ -46,7 +46,9 @@ namespace NzbDrone.Core.Music public Artist FindById(string foreignArtistId) { - var artist = Query(Builder().Where(m => m.ForeignArtistId == foreignArtistId)).SingleOrDefault(); + Artist artist; + + artist = Query(Builder().Where(m => m.ForeignArtistId == foreignArtistId)).SingleOrDefault(); if (artist == null) { @@ -72,7 +74,7 @@ namespace NzbDrone.Core.Music { using (var conn = _database.OpenConnection()) { - var strSql = "SELECT Id AS [Key], Path AS [Value] FROM Artists"; + var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Artists\""; return conn.Query>(strSql).ToDictionary(x => x.Key, x => x.Value); } } diff --git a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs b/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs index 034480eba..32b8bc706 100644 --- a/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/ReleaseRepository.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using TagLib.Riff; namespace NzbDrone.Core.Music { @@ -11,7 +12,7 @@ namespace NzbDrone.Core.Music AlbumRelease FindByForeignReleaseId(string foreignReleaseId, bool checkRedirect = false); List FindByAlbum(int id); List FindByRecordingId(List recordingIds); - List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds); + List GetReleasesForRefresh(int albumId, List foreignReleaseIds); List SetMonitored(AlbumRelease release); } @@ -35,7 +36,7 @@ namespace NzbDrone.Core.Music return release; } - public List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds) + public List GetReleasesForRefresh(int albumId, List foreignReleaseIds) { return Query(r => r.AlbumId == albumId || foreignReleaseIds.Contains(r.ForeignReleaseId)); } @@ -44,10 +45,12 @@ namespace NzbDrone.Core.Music { // populate the albums and artist metadata also // this hopefully speeds up the track matching a lot - var builder = new SqlBuilder() - .LeftJoin((r, a) => r.AlbumId == a.Id) - .LeftJoin((a, m) => a.ArtistMetadataId == m.Id) - .Where(r => r.AlbumId == id); + SqlBuilder builder; + + builder = new SqlBuilder(_database.DatabaseType) + .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) => { diff --git a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs b/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs index ce5906378..75c8b7a1b 100644 --- a/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs +++ b/src/NzbDrone.Core/Music/Repositories/TrackRepository.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Music List GetTracksByAlbum(int albumId); List GetTracksByRelease(int albumReleaseId); List GetTracksByReleases(List albumReleaseIds); - List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds); + List GetTracksForRefresh(int albumReleaseId, List foreignTrackIds); List GetTracksByFileId(int fileId); List GetTracksByFileId(IEnumerable ids); List TracksWithFiles(int artistId); @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Music }).ToList(); } - public List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds) + public List GetTracksForRefresh(int albumReleaseId, List foreignTrackIds) { return Query(a => a.AlbumReleaseId == albumReleaseId || foreignTrackIds.Contains(a.ForeignTrackId)); } diff --git a/src/NzbDrone.Core/Music/Services/AlbumService.cs b/src/NzbDrone.Core/Music/Services/AlbumService.cs index 14b1af6d7..abce226ce 100644 --- a/src/NzbDrone.Core/Music/Services/AlbumService.cs +++ b/src/NzbDrone.Core/Music/Services/AlbumService.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Music List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); List GetAlbumsByArtistMetadataId(int artistMetadataId); - List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds); + List GetAlbumsForRefresh(int artistMetadataId, List foreignIds); Album AddAlbum(Album newAlbum, bool doRefresh); Album FindById(string foreignId); Album FindByTitle(int artistMetadataId, string title); @@ -185,7 +185,7 @@ namespace NzbDrone.Core.Music return _albumRepository.GetAlbumsByArtistMetadataId(artistMetadataId).ToList(); } - public List GetAlbumsForRefresh(int artistMetadataId, IEnumerable foreignIds) + public List GetAlbumsForRefresh(int artistMetadataId, List foreignIds) { return _albumRepository.GetAlbumsForRefresh(artistMetadataId, foreignIds); } diff --git a/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs b/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs index a02791501..983ca0c07 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs +++ b/src/NzbDrone.Core/Music/Services/RefreshAlbumReleaseService.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Music { return _trackService.GetTracksForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignTrackId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignTrackIds))); + .Concat(remoteChildren.SelectMany(x => x.OldForeignTrackIds)).ToList()); } protected override Tuple> GetMatchingExistingChildren(List existingChildren, Track remote) diff --git a/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs index a9a271f8f..2be37b499 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/Services/RefreshAlbumService.cs @@ -247,7 +247,7 @@ namespace NzbDrone.Core.Music { var children = _releaseService.GetReleasesForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignReleaseId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds))); + .Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds)).ToList()); // Make sure trackfiles point to the new album where we are grabbing a release from another album var files = new List(); diff --git a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs b/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs index bd6b82a87..50882e9ac 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs @@ -213,7 +213,7 @@ namespace NzbDrone.Core.Music { return _albumService.GetAlbumsForRefresh(entity.ArtistMetadataId, remoteChildren.Select(x => x.ForeignAlbumId) - .Concat(remoteChildren.SelectMany(x => x.OldForeignAlbumIds))); + .Concat(remoteChildren.SelectMany(x => x.OldForeignAlbumIds)).ToList()); } protected override Tuple> GetMatchingExistingChildren(List existingChildren, Album remote) diff --git a/src/NzbDrone.Core/Music/Services/ReleaseService.cs b/src/NzbDrone.Core/Music/Services/ReleaseService.cs index 69b33613c..4365472ac 100644 --- a/src/NzbDrone.Core/Music/Services/ReleaseService.cs +++ b/src/NzbDrone.Core/Music/Services/ReleaseService.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Music void InsertMany(List releases); void UpdateMany(List releases); void DeleteMany(List releases); - List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds); + List GetReleasesForRefresh(int albumId, List foreignReleaseIds); List GetReleasesByAlbum(int releaseGroupId); List GetReleasesByRecordingIds(List recordingIds); List SetMonitored(AlbumRelease release); @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Music } } - public List GetReleasesForRefresh(int albumId, IEnumerable foreignReleaseIds) + public List GetReleasesForRefresh(int albumId, List foreignReleaseIds) { return _releaseRepository.GetReleasesForRefresh(albumId, foreignReleaseIds); } diff --git a/src/NzbDrone.Core/Music/Services/TrackService.cs b/src/NzbDrone.Core/Music/Services/TrackService.cs index 50c4d8891..38ee2da24 100644 --- a/src/NzbDrone.Core/Music/Services/TrackService.cs +++ b/src/NzbDrone.Core/Music/Services/TrackService.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music List GetTracksByAlbum(int albumId); List GetTracksByRelease(int albumReleaseId); List GetTracksByReleases(List albumReleaseIds); - List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds); + List GetTracksForRefresh(int albumReleaseId, List foreignTrackIds); List TracksWithFiles(int artistId); List TracksWithoutFiles(int albumId); List GetTracksByFileId(int trackFileId); @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Music return _trackRepository.GetTracksByReleases(albumReleaseIds); } - public List GetTracksForRefresh(int albumReleaseId, IEnumerable foreignTrackIds) + public List GetTracksForRefresh(int albumReleaseId, List foreignTrackIds) { return _trackRepository.GetTracksForRefresh(albumReleaseId, foreignTrackIds); } diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index a2001b2da..3c4d11564 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Cloud; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Core.Analytics; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Update { @@ -21,13 +22,15 @@ namespace NzbDrone.Core.Update private readonly IHttpRequestBuilderFactory _requestBuilder; private readonly IPlatformInfo _platformInfo; private readonly IAnalyticsService _analyticsService; + private readonly IMainDatabase _mainDatabase; - public UpdatePackageProvider(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo) + public UpdatePackageProvider(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo, IMainDatabase mainDatabase) { _platformInfo = platformInfo; _analyticsService = analyticsService; _requestBuilder = requestBuilder.Services; _httpClient = httpClient; + _mainDatabase = mainDatabase; } public UpdatePackage GetLatestUpdate(string branch, Version currentVersion) @@ -39,6 +42,7 @@ namespace NzbDrone.Core.Update .AddQueryParam("arch", RuntimeInformation.OSArchitecture) .AddQueryParam("runtime", "netcore") .AddQueryParam("runtimeVer", _platformInfo.Version) + .AddQueryParam("dbType", _mainDatabase.DatabaseType) .SetSegment("branch", branch); if (_analyticsService.IsEnabled) diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index f2aeb5039..b266df217 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -5,12 +5,14 @@ using DryIoc.Microsoft.DependencyInjection; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; @@ -43,6 +45,7 @@ namespace NzbDrone.App.Test // dummy lifetime and broadcaster so tests resolve container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock().Object); + container.RegisterInstance>(new Mock>().Object); _container = container.GetServiceProvider(); } diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index fc24058ad..e8699228f 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -8,7 +8,6 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using DryIoc; using DryIoc.Microsoft.DependencyInjection; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -25,6 +24,9 @@ using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Host; +using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions; + namespace NzbDrone.Host { public static class Bootstrap @@ -138,6 +140,10 @@ namespace NzbDrone.Host .AddDatabase() .AddStartupContext(context); }) + .ConfigureServices(services => + { + services.Configure(config.GetSection("Lidarr:Postgres")); + }) .ConfigureWebHost(builder => { builder.UseConfiguration(config); @@ -208,6 +214,7 @@ namespace NzbDrone.Host return new ConfigurationBuilder() .AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false) .AddInMemoryCollection(new List> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) }) + .AddEnvironmentVariables() .Build(); } diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index a177efb97..c8e0f45fc 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,10 +1,16 @@ +using System.Collections.Generic; using System.Threading; using Lidarr.Http.ClientSchema; +using Microsoft.Extensions.Configuration; using NLog; +using Npgsql; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; +using NzbDrone.Test.Common.Datastore; namespace NzbDrone.Integration.Test { @@ -19,6 +25,8 @@ namespace NzbDrone.Integration.Test protected int Port { get; private set; } + protected PostgresOptions PostgresOptions { get; set; } = new (); + protected override string RootUrl => $"http://localhost:{Port}/"; protected override string ApiKey => _runner.ApiKey; @@ -27,7 +35,14 @@ namespace NzbDrone.Integration.Test { Port = Interlocked.Increment(ref StaticPort); - _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), Port); + PostgresOptions = PostgresDatabase.GetTestOptions(); + + if (PostgresOptions?.Host != null) + { + CreatePostgresDb(PostgresOptions); + } + + _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), PostgresOptions, Port); _runner.Kill(); _runner.Start(); @@ -59,6 +74,22 @@ namespace NzbDrone.Integration.Test protected override void StopTestTarget() { _runner.Kill(); + if (PostgresOptions?.Host != null) + { + DropPostgresDb(PostgresOptions); + } + } + + private static void CreatePostgresDb(PostgresOptions options) + { + PostgresDatabase.Create(options, MigrationType.Main); + PostgresDatabase.Create(options, MigrationType.Log); + } + + private static void DropPostgresDb(PostgresOptions options) + { + PostgresDatabase.Drop(options, MigrationType.Main); + PostgresDatabase.Drop(options, MigrationType.Log); } } } diff --git a/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs b/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs new file mode 100644 index 000000000..940334ca2 --- /dev/null +++ b/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs @@ -0,0 +1,69 @@ +using System; +using Npgsql; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Test.Common.Datastore +{ + public static class PostgresDatabase + { + public static PostgresOptions GetTestOptions() + { + var options = PostgresOptions.GetOptions(); + + var uid = TestBase.GetUID(); + options.MainDb = uid + "_main"; + options.LogDb = uid + "_log"; + + return options; + } + + public static void Create(PostgresOptions options, MigrationType migrationType) + { + var db = GetDatabaseName(options, migrationType); + var connectionString = GetConnectionString(options); + using var conn = new NpgsqlConnection(connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"CREATE DATABASE \"{db}\" WITH OWNER = {options.User} ENCODING = 'UTF8' CONNECTION LIMIT = -1;"; + cmd.ExecuteNonQuery(); + } + + public static void Drop(PostgresOptions options, MigrationType migrationType) + { + var db = GetDatabaseName(options, migrationType); + var connectionString = GetConnectionString(options); + using var conn = new NpgsqlConnection(connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"DROP DATABASE \"{db}\" WITH (FORCE);"; + cmd.ExecuteNonQuery(); + } + + private static string GetConnectionString(PostgresOptions options) + { + var builder = new NpgsqlConnectionStringBuilder() + { + Host = options.Host, + Port = options.Port, + Username = options.User, + Password = options.Password, + Enlist = false + }; + + return builder.ConnectionString; + } + + private static string GetDatabaseName(PostgresOptions options, MigrationType migrationType) + { + return migrationType switch + { + MigrationType.Main => options.MainDb, + MigrationType.Log => options.LogDb, + _ => throw new NotImplementedException("Unknown migration type") + }; + } + } +} diff --git a/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs b/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs new file mode 100644 index 000000000..151e245fc --- /dev/null +++ b/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs @@ -0,0 +1,14 @@ +using System.IO; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Test.Common.Datastore +{ + public static class SqliteDatabase + { + public static string GetCachedDb(MigrationType type) + { + return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db"); + } + } +} diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 639931cb3..fbeef901f 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Threading; @@ -9,7 +10,9 @@ using NUnit.Framework; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Processes; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using RestSharp; namespace NzbDrone.Test.Common @@ -23,13 +26,15 @@ namespace NzbDrone.Test.Common public string AppData { get; private set; } public string ApiKey { get; private set; } + public PostgresOptions PostgresOptions { get; private set; } public int Port { get; private set; } - public NzbDroneRunner(Logger logger, int port = 8686) + public NzbDroneRunner(Logger logger, PostgresOptions postgresOptions, int port = 8686) { _processProvider = new ProcessProvider(logger); _restClient = new RestClient($"http://localhost:{port}/api/v1"); + PostgresOptions = postgresOptions; Port = port; } @@ -136,12 +141,25 @@ namespace NzbDrone.Test.Common TestBase.DeleteTempFolder(AppData); } - private void Start(string outputNzbdroneConsoleExe) + private void Start(string outputLidarrConsoleExe) { - TestContext.Progress.WriteLine("Starting instance from {0} on port {1}", outputNzbdroneConsoleExe, Port); + StringDictionary envVars = new (); + if (PostgresOptions?.Host != null) + { + envVars.Add("Lidarr__Postgres__Host", PostgresOptions.Host); + envVars.Add("Lidarr__Postgres__Port", PostgresOptions.Port.ToString()); + envVars.Add("Lidarr__Postgres__User", PostgresOptions.User); + envVars.Add("Lidarr__Postgres__Password", PostgresOptions.Password); + envVars.Add("Lidarr__Postgres__MainDb", PostgresOptions.MainDb); + envVars.Add("Lidarr__Postgres__LogDb", PostgresOptions.LogDb); + + TestContext.Progress.WriteLine("Using env vars:\n{0}", envVars.ToJson()); + } + + TestContext.Progress.WriteLine("Starting instance from {0} on port {1}", outputLidarrConsoleExe, Port); var args = "-nobrowser -nosingleinstancecheck -data=\"" + AppData + "\""; - _nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, null, OnOutputDataReceived, OnOutputDataReceived); + _nzbDroneProcess = _processProvider.Start(outputLidarrConsoleExe, args, envVars, OnOutputDataReceived, OnOutputDataReceived); } private void OnOutputDataReceived(string data) diff --git a/src/postgres.runsettings b/src/postgres.runsettings new file mode 100644 index 000000000..2285b4395 --- /dev/null +++ b/src/postgres.runsettings @@ -0,0 +1,11 @@ + + + + + 192.168.100.5 + 5432 + abc + abc + + +