New: Postgres Database Support

Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com>
pull/3106/head
Robin Dadswell 2 years ago committed by Qstick
parent f7839adc38
commit 8f6e099794

@ -534,6 +534,62 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true 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 - stage: Integration
displayName: Integration displayName: Integration
@ -617,6 +673,67 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results 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 - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
dependsOn: Prepare dependsOn: Prepare

@ -23,6 +23,8 @@ class About extends Component {
isDocker, isDocker,
runtimeVersion, runtimeVersion,
migrationVersion, migrationVersion,
databaseVersion,
databaseType,
appData, appData,
startupPath, startupPath,
mode, mode,
@ -68,6 +70,11 @@ class About extends Component {
data={migrationVersion} data={migrationVersion}
/> />
<DescriptionListItem
title={translate('Database')}
data={`${titleCase(databaseType)} ${databaseVersion}`}
/>
<DescriptionListItem <DescriptionListItem
title={translate('AppDataDirectory')} title={translate('AppDataDirectory')}
data={appData} data={appData}
@ -108,6 +115,8 @@ About.propTypes = {
runtimeVersion: PropTypes.string.isRequired, runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired, migrationVersion: PropTypes.number.isRequired,
databaseType: PropTypes.string.isRequired,
databaseVersion: PropTypes.string.isRequired,
appData: PropTypes.string.isRequired, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,

@ -78,7 +78,8 @@ namespace Lidarr.Api.V1.System
Mode = _runtimeInfo.Mode, Mode = _runtimeInfo.Mode,
Branch = _configFileProvider.Branch, Branch = _configFileProvider.Branch,
Authentication = _configFileProvider.AuthenticationMethod, Authentication = _configFileProvider.AuthenticationMethod,
SqliteVersion = _database.Version, DatabaseType = _database.DatabaseType,
DatabaseVersion = _database.Version,
MigrationVersion = _database.Migration, MigrationVersion = _database.Migration,
UrlBase = _configFileProvider.UrlBase, UrlBase = _configFileProvider.UrlBase,
RuntimeVersion = _platformInfo.Version, RuntimeVersion = _platformInfo.Version,

@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080); driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
_runner.KillAll(); _runner.KillAll();
_runner.Start(); _runner.Start();

@ -72,6 +72,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")] [TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")] [TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/lidarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")] [TestCase("/lidarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=lidarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase(@"[Info] MigrationController: *** Migrating Database=lidarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
// Announce URLs (passkeys) Magnet & Tracker // Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")] [TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
@ -96,9 +98,24 @@ namespace NzbDrone.Common.Test.InstrumentationTests
var cleansedMessage = CleanseLogMessage.Cleanse(message); var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret"); cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210"); cleansedMessage.Should().NotContain("01233210");
} }
[TestCase(@"[Info] MigrationController: *** Migrating Database=lidarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
public void should_keep_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);
cleansedMessage.Should().NotContain("mySecret");
cleansedMessage.Should().NotContain("123%@%_@!#^#@");
cleansedMessage.Should().NotContain("01233210");
cleansedMessage.Should().Contain("shouldkeep1");
cleansedMessage.Should().Contain("shouldkeep2");
cleansedMessage.Should().Contain("shouldkeep3");
}
[TestCase(@"Some message (from 32.2.3.5 user agent)")] [TestCase(@"Some message (from 32.2.3.5 user agent)")]
[TestCase(@"Auth-Invalidated ip 32.2.3.5")] [TestCase(@"Auth-Invalidated ip 32.2.3.5")]
[TestCase(@"Auth-Success ip 32.2.3.5")] [TestCase(@"Auth-Success ip 32.2.3.5")]

@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test
.AddDummyDatabase() .AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second")); .AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object); container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
var serviceProvider = container.GetServiceProvider(); var serviceProvider = container.GetServiceProvider();

@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled), new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"), new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),

@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes
try try
{ {
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value); _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) catch (Exception e)
{ {

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore
public void SingleOrDefault_should_return_null_on_empty_db() public void SingleOrDefault_should_return_null_on_empty_db()
{ {
Mocker.Resolve<IDatabase>() Mocker.Resolve<IDatabase>()
.OpenConnection().Query<Artist>("SELECT * FROM Artists") .OpenConnection().Query<Artist>("SELECT * FROM \"Artists\"")
.SingleOrDefault(c => c.CleanName == "SomeTitle") .SingleOrDefault(c => c.CleanName == "SomeTitle")
.Should() .Should()
.BeNull(); .BeNull();

@ -2,7 +2,9 @@ using System;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using FluentAssertions.Equivalency;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -13,6 +15,17 @@ namespace NzbDrone.Core.Test.Datastore
[TestFixture] [TestFixture]
public class DatabaseRelationshipFixture : DbTest public class DatabaseRelationshipFixture : DbTest
{ {
[SetUp]
public void Setup()
{
AssertionOptions.AssertEquivalencyUsing(options =>
{
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
return options;
});
}
[Test] [Test]
public void one_to_one() public void one_to_one()
{ {
@ -33,13 +46,7 @@ namespace NzbDrone.Core.Test.Datastore
var loadedAlbum = Db.Single<AlbumRelease>().Album.Value; var loadedAlbum = Db.Single<AlbumRelease>().Album.Value;
loadedAlbum.Should().NotBeNull(); loadedAlbum.Should().NotBeNull();
loadedAlbum.Should().BeEquivalentTo(album, loadedAlbum.Should().BeEquivalentTo(album, AlbumComparerOptions);
options => options
.IncludingAllRuntimeProperties()
.Excluding(c => c.Artist)
.Excluding(c => c.ArtistId)
.Excluding(c => c.ArtistMetadata)
.Excluding(c => c.AlbumReleases));
} }
[Test] [Test]
@ -86,5 +93,9 @@ namespace NzbDrone.Core.Test.Datastore
returnedHistory[0].Quality.Quality.Should().Be(Quality.MP3_320); returnedHistory[0].Quality.Quality.Should().Be(Quality.MP3_320);
} }
private EquivalencyAssertionOptions<Album> AlbumComparerOptions(EquivalencyAssertionOptions<Album> opts) => opts.ComparingByMembers<Album>()
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
.Excluding(x => x.ArtistId);
} }
} }

@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_artist_for_trackfile() public void should_lazy_load_artist_for_trackfile()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<TrackFile>(new SqlBuilder()).ToList(); var tracks = db.Query<TrackFile>(new SqlBuilder(db.DatabaseType)).ToList();
Assert.IsNotEmpty(tracks); Assert.IsNotEmpty(tracks);
foreach (var track in tracks) foreach (var track in tracks)
@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_trackfile_if_not_joined() public void should_lazy_load_trackfile_if_not_joined()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<Track>(new SqlBuilder()).ToList(); var tracks = db.Query<Track>(new SqlBuilder(db.DatabaseType)).ToList();
foreach (var track in tracks) foreach (var track in tracks)
{ {
@ -135,7 +135,7 @@ namespace NzbDrone.Core.Test.Datastore
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var files = MediaFileRepository.Query(db, var files = MediaFileRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<TrackFile, Track>((f, t) => f.Id == t.TrackFileId) .Join<TrackFile, Track>((f, t) => f.Id == t.TrackFileId)
.Join<TrackFile, Album>((t, a) => t.AlbumId == a.Id) .Join<TrackFile, Album>((t, a) => t.AlbumId == a.Id)
.Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) .Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId)
@ -157,7 +157,7 @@ namespace NzbDrone.Core.Test.Datastore
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var files = db.QueryJoined<TrackFile, Album, Artist, ArtistMetadata>( var files = db.QueryJoined<TrackFile, Album, Artist, ArtistMetadata>(
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<TrackFile, Album>((t, a) => t.AlbumId == a.Id) .Join<TrackFile, Album>((t, a) => t.AlbumId == a.Id)
.Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) .Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId)
.Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id), .Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id),
@ -186,7 +186,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_tracks_if_not_joined() public void should_lazy_load_tracks_if_not_joined()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var release = db.Query<AlbumRelease>(new SqlBuilder().Where<AlbumRelease>(x => x.Id == 1)).SingleOrDefault(); var release = db.Query<AlbumRelease>(new SqlBuilder(db.DatabaseType).Where<AlbumRelease>(x => x.Id == 1)).SingleOrDefault();
Assert.IsFalse(release.Tracks.IsLoaded); Assert.IsFalse(release.Tracks.IsLoaded);
Assert.IsNotNull(release.Tracks.Value); Assert.IsNotNull(release.Tracks.Value);
@ -198,7 +198,7 @@ namespace NzbDrone.Core.Test.Datastore
public void should_lazy_load_track_if_not_joined() public void should_lazy_load_track_if_not_joined()
{ {
var db = Mocker.Resolve<IDatabase>(); var db = Mocker.Resolve<IDatabase>();
var tracks = db.Query<TrackFile>(new SqlBuilder()).ToList(); var tracks = db.Query<TrackFile>(new SqlBuilder(db.DatabaseType)).ToList();
foreach (var track in tracks) foreach (var track in tracks)
{ {

@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var profiles = db.Query<Profile4>("SELECT Items FROM Profiles LIMIT 1"); var profiles = db.Query<Profile4>("SELECT \"Items\" FROM \"Profiles\" LIMIT 1");
var items = profiles.First().Items; var items = profiles.First().Items;
items.Should().HaveCount(7); items.Should().HaveCount(7);
@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var profiles = db.Query<Profile4>("SELECT Items FROM Profiles LIMIT 1"); var profiles = db.Query<Profile4>("SELECT \"Items\" FROM \"Profiles\" LIMIT 1");
var items = profiles.First().Items; var items = profiles.First().Items;
items.Should().HaveCount(7); items.Should().HaveCount(7);

@ -24,8 +24,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration
Status = 1, Status = 1,
Images = "", Images = "",
Path = $"/mnt/data/path/{name}", Path = $"/mnt/data/path/{name}",
Monitored = 1, Monitored = true,
AlbumFolder = 1, AlbumFolder = true,
LanguageProfileId = 1, LanguageProfileId = 1,
MetadataProfileId = 1 MetadataProfileId = 1
}); });
@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
Title = title, Title = title,
CleanTitle = title, CleanTitle = title,
Images = "", Images = "",
Monitored = 1, Monitored = true,
AlbumType = "Studio", AlbumType = "Studio",
Duration = 100, Duration = 100,
Media = "", Media = "",
@ -61,9 +61,9 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ForeignTrackId = id.ToString(), ForeignTrackId = id.ToString(),
ArtistId = artistid, ArtistId = artistid,
AlbumId = albumid, AlbumId = albumid,
Explicit = 0, Explicit = false,
Compilation = 0, Compilation = false,
Monitored = 0, Monitored = false,
Duration = 100, Duration = 100,
MediumNumber = 1, MediumNumber = 1,
AbsoluteTrackNumber = i, AbsoluteTrackNumber = i,
@ -74,8 +74,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration
private IEnumerable<AlbumRelease> VerifyAlbumReleases(IDirectDataMapper db) private IEnumerable<AlbumRelease> VerifyAlbumReleases(IDirectDataMapper db)
{ {
var releases = db.Query<AlbumRelease>("SELECT * FROM AlbumReleases"); var releases = db.Query<AlbumRelease>("SELECT * FROM \"AlbumReleases\"");
var albums = db.Query<Album>("SELECT * FROM Albums"); var albums = db.Query<Album>("SELECT * FROM \"Albums\"");
// we only put in one release per album // we only put in one release per album
releases.Count().Should().Be(albums.Count()); 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) private void VerifyTracks(IDirectDataMapper db, int albumId, int albumReleaseId, int expectedCount)
{ {
var tracks = db.Query<Track>("SELECT Tracks.* FROM Tracks " + var tracks = db.Query<Track>("SELECT \"Tracks\".* FROM \"Tracks\" " +
"JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id " + "JOIN \"AlbumReleases\" ON \"Tracks\".\"AlbumReleaseId\" = \"AlbumReleases\".\"Id\" " +
"JOIN Albums ON AlbumReleases.AlbumId = Albums.Id " + "JOIN \"Albums\" ON \"AlbumReleases\".\"AlbumId\" = \"Albums\".\"Id\" " +
"WHERE Albums.Id = " + albumId).ToList(); "WHERE \"Albums\".\"Id\" = " + albumId).ToList();
var album = db.Query<Album>("SELECT * FROM Albums WHERE Albums.Id = " + albumId).ToList().Single(); var album = db.Query<Album>("SELECT * FROM \"Albums\" WHERE \"Albums\".\"Id\" = " + albumId).ToList().Single();
tracks.Count.Should().Be(expectedCount); tracks.Count.Should().Be(expectedCount);
tracks.First().AlbumReleaseId.Should().Be(albumReleaseId); tracks.First().AlbumReleaseId.Should().Be(albumReleaseId);

@ -25,8 +25,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration
Id = id, Id = id,
CleanName = name, CleanName = name,
Path = _artistPath, Path = _artistPath,
Monitored = 1, Monitored = true,
AlbumFolder = 1, AlbumFolder = true,
LanguageProfileId = 1, LanguageProfileId = 1,
MetadataProfileId = 1, MetadataProfileId = 1,
ArtistMetadataId = id ArtistMetadataId = id
@ -43,9 +43,9 @@ namespace NzbDrone.Core.Test.Datastore.Migration
Title = title, Title = title,
CleanTitle = title, CleanTitle = title,
Images = "", Images = "",
Monitored = 1, Monitored = true,
AlbumType = "Studio", AlbumType = "Studio",
AnyReleaseOk = 1 AnyReleaseOk = true
}); });
} }
@ -85,7 +85,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
Id = id, Id = id,
ForeignTrackId = id.ToString(), ForeignTrackId = id.ToString(),
Explicit = 0, Explicit = false,
TrackFileId = id, TrackFileId = id,
Duration = 100, Duration = 100,
MediumNumber = 1, MediumNumber = 1,
@ -102,8 +102,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration
private void VerifyTracksFiles(IDirectDataMapper db, int albumId, List<string> expectedPaths) private void VerifyTracksFiles(IDirectDataMapper db, int albumId, List<string> expectedPaths)
{ {
var tracks = db.Query("SELECT TrackFiles.* FROM TrackFiles " + var tracks = db.Query("SELECT \"TrackFiles\".* FROM \"TrackFiles\" " +
"WHERE TrackFiles.AlbumId = " + albumId); "WHERE \"TrackFiles\".\"AlbumId\" = " + albumId);
TestLogger.Debug($"Got {tracks.Count} tracks"); TestLogger.Debug($"Got {tracks.Count} tracks");

@ -34,8 +34,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ArtistMetadataId = artistMetadataId, ArtistMetadataId = artistMetadataId,
CleanName = name, CleanName = name,
Path = _artistPath, Path = _artistPath,
Monitored = 1, Monitored = true,
AlbumFolder = 1, AlbumFolder = true,
LanguageProfileId = 1, LanguageProfileId = 1,
MetadataProfileId = 1, MetadataProfileId = 1,
}); });
@ -43,7 +43,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
private void VerifyArtists(IDirectDataMapper db, List<int> ids) private void VerifyArtists(IDirectDataMapper db, List<int> 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); artists.Select(x => x["Id"]).Should().BeEquivalentTo(ids);

@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 1, Enable = true,
Name = "Deluge", Name = "Deluge",
Implementation = "Deluge", Implementation = "Deluge",
Settings = new DelugeSettings36 Settings = new DelugeSettings36
@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition036>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition036>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(1); items.Should().HaveCount(1);
items.First().Priority.Should().Be(1); items.First().Priority.Should().Be(1);
@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 1, Enable = true,
Name = "Deluge", Name = "Deluge",
Implementation = "Deluge", Implementation = "Deluge",
Settings = new DelugeSettings36 Settings = new DelugeSettings36
@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ConfigContract = "DelugeSettings" ConfigContract = "DelugeSettings"
}).Row(new }).Row(new
{ {
Enable = 1, Enable = true,
Name = "Deluge2", Name = "Deluge2",
Implementation = "Deluge", Implementation = "Deluge",
Settings = new DelugeSettings36 Settings = new DelugeSettings36
@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ConfigContract = "DelugeSettings" ConfigContract = "DelugeSettings"
}).Row(new }).Row(new
{ {
Enable = 1, Enable = true,
Name = "sab", Name = "sab",
Implementation = "Sabnzbd", Implementation = "Sabnzbd",
Settings = new SabnzbdSettings36 Settings = new SabnzbdSettings36
@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition036>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition036>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(3); items.Should().HaveCount(3);
items[0].Priority.Should().Be(1); items[0].Priority.Should().Be(1);
@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 0, Enable = false,
Name = "Deluge", Name = "Deluge",
Implementation = "Deluge", Implementation = "Deluge",
Settings = new DelugeSettings36 Settings = new DelugeSettings36
@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ConfigContract = "DelugeSettings" ConfigContract = "DelugeSettings"
}).Row(new }).Row(new
{ {
Enable = 0, Enable = false,
Name = "Deluge2", Name = "Deluge2",
Implementation = "Deluge", Implementation = "Deluge",
Settings = new DelugeSettings36 Settings = new DelugeSettings36
@ -119,7 +119,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
ConfigContract = "DelugeSettings" ConfigContract = "DelugeSettings"
}).Row(new }).Row(new
{ {
Enable = 0, Enable = false,
Name = "sab", Name = "sab",
Implementation = "Sabnzbd", Implementation = "Sabnzbd",
Settings = new SabnzbdSettings36 Settings = new SabnzbdSettings36
@ -131,7 +131,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition036>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition036>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(3); items.Should().HaveCount(3);
items[0].Priority.Should().Be(1); items[0].Priority.Should().Be(1);

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<ProviderDefinition166>("SELECT * FROM Notifications"); var items = db.Query<ProviderDefinition166>("SELECT * FROM \"Notifications\"");
items.Should().HaveCount(1); items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email"); items.First().Implementation.Should().Be("Email");

@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 1, Enable = true,
Name = "Deluge", Name = "Deluge",
Implementation = "Deluge", Implementation = "Deluge",
Priority = 1, Priority = 1,
@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition158>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(1); items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse(); items.First().RemoveCompletedDownloads.Should().BeFalse();
@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 1, Enable = true,
Name = "Deluge", Name = "Deluge",
Implementation = "Deluge", Implementation = "Deluge",
Priority = 1, Priority = 1,
@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition158>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(1); items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeTrue(); items.First().RemoveCompletedDownloads.Should().BeTrue();
@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
{ {
c.Insert.IntoTable("DownloadClients").Row(new c.Insert.IntoTable("DownloadClients").Row(new
{ {
Enable = 1, Enable = true,
Name = "RTorrent", Name = "RTorrent",
Implementation = "RTorrent", Implementation = "RTorrent",
Priority = 1, Priority = 1,
@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration
}); });
}); });
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients"); var items = db.Query<DownloadClientDefinition158>("SELECT * FROM \"DownloadClients\"");
items.Should().HaveCount(1); items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse(); items.First().RemoveCompletedDownloads.Should().BeFalse();

@ -11,9 +11,9 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore namespace NzbDrone.Core.Test.Datastore
{ {
[TestFixture] [TestFixture]
public class WhereBuilderFixture : CoreTest public class WhereBuilderPostgresFixture : CoreTest
{ {
private WhereBuilder _subject; private WhereBuilderPostgres _subject;
[OneTimeSetUp] [OneTimeSetUp]
public void MapTables() public void MapTables()
@ -22,14 +22,14 @@ namespace NzbDrone.Core.Test.Datastore
Mocker.Resolve<DbFactory>(); Mocker.Resolve<DbFactory>();
} }
private WhereBuilder Where(Expression<Func<Artist, bool>> filter) private WhereBuilderPostgres Where(Expression<Func<Artist, bool>> filter)
{ {
return new WhereBuilder(filter, true, 0); return new WhereBuilderPostgres(filter, true, 0);
} }
private WhereBuilder WhereMetadata(Expression<Func<ArtistMetadata, bool>> filter) private WhereBuilderPostgres WhereMetadata(Expression<Func<ArtistMetadata, bool>> filter)
{ {
return new WhereBuilder(filter, true, 0); return new WhereBuilderPostgres(filter, true, 0);
} }
[Test] [Test]
@ -76,7 +76,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_throws_without_concrete_condition_if_requiresConcreteCondition() public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
{ {
Expression<Func<Artist, Artist, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Artist, Artist, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilder(filter, true, 0); _subject = new WhereBuilderPostgres(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString()); Assert.Throws<InvalidOperationException>(() => _subject.ToString());
} }
@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.Datastore
public void where_allows_abstract_condition_if_not_requiresConcreteCondition() public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
{ {
Expression<Func<Artist, Artist, bool>> filter = (x, y) => x.Id == y.Id; Expression<Func<Artist, Artist, bool>> 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\")"); _subject.ToString().Should().Be($"(\"Artists\".\"Id\" = \"Artists\".\"Id\")");
} }
@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.Datastore
var test = "small"; var test = "small";
_subject = Where(x => x.CleanName.Contains(test)); _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<string>("Clause1_P1").Should().Be(test); _subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
} }
@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.Datastore
var test = "small"; var test = "small";
_subject = Where(x => test.Contains(x.CleanName)); _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<string>("Clause1_P1").Should().Be(test); _subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
} }
@ -140,7 +140,7 @@ namespace NzbDrone.Core.Test.Datastore
var test = "small"; var test = "small";
_subject = Where(x => x.CleanName.StartsWith(test)); _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<string>("Clause1_P1").Should().Be(test); _subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
} }
@ -150,7 +150,7 @@ namespace NzbDrone.Core.Test.Datastore
var test = "small"; var test = "small";
_subject = Where(x => x.CleanName.EndsWith(test)); _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<string>("Clause1_P1").Should().Be(test); _subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
} }
@ -160,7 +160,7 @@ namespace NzbDrone.Core.Test.Datastore
var list = new List<int> { 1, 2, 3 }; var list = new List<int> { 1, 2, 3 };
_subject = Where(x => list.Contains(x.Id)); _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] [Test]
@ -169,7 +169,7 @@ namespace NzbDrone.Core.Test.Datastore
var list = new List<int> { 1, 2, 3 }; var list = new List<int> { 1, 2, 3 };
_subject = Where(x => x.CleanName == "test" && list.Contains(x.Id)); _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] [Test]
@ -179,7 +179,7 @@ namespace NzbDrone.Core.Test.Datastore
_subject = Where(x => list.Contains(x.CleanName)); _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] [Test]
@ -187,7 +187,7 @@ namespace NzbDrone.Core.Test.Datastore
{ {
_subject = WhereMetadata(x => x.OldForeignArtistIds.Contains("foreignId")); _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] [Test]
@ -204,7 +204,7 @@ namespace NzbDrone.Core.Test.Datastore
var allowed = new List<ArtistStatusType> { ArtistStatusType.Continuing, ArtistStatusType.Ended }; var allowed = new List<ArtistStatusType> { ArtistStatusType.Continuing, ArtistStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status)); _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] [Test]
@ -213,7 +213,7 @@ namespace NzbDrone.Core.Test.Datastore
var allowed = new ArtistStatusType[] { ArtistStatusType.Continuing, ArtistStatusType.Ended }; var allowed = new ArtistStatusType[] { ArtistStatusType.Continuing, ArtistStatusType.Ended };
_subject = WhereMetadata(x => allowed.Contains(x.Status)); _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))");
} }
} }
} }

@ -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<DbFactory>();
}
private WhereBuilderSqlite Where(Expression<Func<Artist, bool>> filter)
{
return new WhereBuilderSqlite(filter, true, 0);
}
private WhereBuilderSqlite WhereMetadata(Expression<Func<ArtistMetadata, bool>> 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<int>("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<int>("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<int>("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<int>("Clause1_P1").Should().Be(1);
}
[Test]
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
{
Expression<Func<Artist, Artist, bool>> filter = (x, y) => x.Id == y.Id;
_subject = new WhereBuilderSqlite(filter, true, 0);
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
}
[Test]
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
{
Expression<Func<Artist, Artist, bool>> 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<string>("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<string>("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<string>("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<string>("Clause1_P1").Should().Be(test);
}
[Test]
public void where_in_list()
{
var list = new List<int> { 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<int> { 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> { 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)");
}
}
}

@ -1,14 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test.Framework namespace NzbDrone.Core.Test.Framework
{ {
@ -49,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework
public abstract class DbTest : CoreTest public abstract class DbTest : CoreTest
{ {
private ITestDatabase _db; private ITestDatabase _db;
private DatabaseType _databaseType;
protected virtual MigrationType MigrationType => MigrationType.Main; protected virtual MigrationType MigrationType => MigrationType.Main;
@ -101,17 +106,39 @@ namespace NzbDrone.Core.Test.Framework
private IDatabase CreateDatabase(MigrationContext migrationContext) private IDatabase CreateDatabase(MigrationContext migrationContext)
{ {
if (_databaseType == DatabaseType.PostgreSQL)
{
CreatePostgresDb();
}
var factory = Mocker.Resolve<DbFactory>(); var factory = Mocker.Resolve<DbFactory>();
// If a special migration test or log migration then create new // 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 factory.Create(migrationContext);
} }
return CreateSqliteDatabase(factory, migrationContext);
}
private void CreatePostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Create(options, MigrationType);
}
private void DropPostgresDb()
{
var options = Mocker.Resolve<IOptions<PostgresOptions>>().Value;
PostgresDatabase.Drop(options, MigrationType);
}
private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext)
{
// Otherwise try to use a cached migrated db // Otherwise try to use a cached migrated db
var cachedDb = GetCachedDatabase(migrationContext.MigrationType); var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
var testDb = GetTestDb(migrationContext.MigrationType); var testDb = GetTestSqliteDb(migrationContext.MigrationType);
if (File.Exists(cachedDb)) if (File.Exists(cachedDb))
{ {
TestLogger.Info($"Using cached initial database {cachedDb}"); TestLogger.Info($"Using cached initial database {cachedDb}");
@ -131,12 +158,7 @@ namespace NzbDrone.Core.Test.Framework
} }
} }
private string GetCachedDatabase(MigrationType type) private string GetTestSqliteDb(MigrationType type)
{
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
}
private string GetTestDb(MigrationType type)
{ {
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase(); return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
} }
@ -151,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework
WithTempAsAppPath(); WithTempAsAppPath();
SetupLogging(); 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<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>()); Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>()); Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@ -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) // Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
GC.Collect(); GC.Collect();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools(); SQLiteConnection.ClearAllPools();
NpgsqlConnection.ClearAllPools();
if (TestFolderInfo != null) if (TestFolderInfo != null)
{ {
DeleteTempFolder(TestFolderInfo.AppDataFolder); DeleteTempFolder(TestFolderInfo.AppDataFolder);
} }
if (_databaseType == DatabaseType.PostgreSQL)
{
DropPostgresDb();
}
} }
} }
} }

@ -1,5 +1,7 @@
using System.IO; using System.IO;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Test.Common.Datastore;
namespace NzbDrone.Core.Test namespace NzbDrone.Core.Test
{ {
@ -10,13 +12,13 @@ namespace NzbDrone.Core.Test
[OneTimeTearDown] [OneTimeTearDown]
public void ClearCachedDatabase() public void ClearCachedDatabase()
{ {
var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db"); var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main);
if (File.Exists(mainCache)) if (File.Exists(mainCache))
{ {
File.Delete(mainCache); File.Delete(mainCache);
} }
var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db"); var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log);
if (File.Exists(logCache)) if (File.Exists(logCache))
{ {
File.Delete(logCache); File.Delete(logCache);

@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework
where T : ModelBase, new(); where T : ModelBase, new();
IDirectDataMapper GetDirectDataMapper(); IDirectDataMapper GetDirectDataMapper();
IDbConnection OpenConnection(); IDbConnection OpenConnection();
DatabaseType DatabaseType { get; }
} }
public class TestDatabase : ITestDatabase public class TestDatabase : ITestDatabase
@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework
private readonly IDatabase _dbConnection; private readonly IDatabase _dbConnection;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
public TestDatabase(IDatabase dbConnection) public TestDatabase(IDatabase dbConnection)
{ {
_eventAggregator = new Mock<IEventAggregator>().Object; _eventAggregator = new Mock<IEventAggregator>().Object;

@ -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<CleanupOrphanedReleases, AlbumRelease>
{
[Test]
public void should_delete_orphaned_releases()
{
var albumRelease = Builder<AlbumRelease>.CreateNew()
.BuildNew();
Db.Insert(albumRelease);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_unorphaned_albums()
{
var album = Builder<Album>.CreateNew()
.BuildNew();
Db.Insert(album);
var albumReleases = Builder<AlbumRelease>.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);
}
}
}

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using FluentAssertions.Equivalency;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@ -23,6 +25,13 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
AssertionOptions.AssertEquivalencyUsing(options =>
{
options.Using<DateTime>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.ToUniversalTime())).WhenTypeIs<DateTime>();
options.Using<DateTime?>(ctx => ctx.Subject.Should().BeCloseTo(ctx.Expectation.Value.ToUniversalTime())).WhenTypeIs<DateTime?>();
return options;
});
_artist = new Artist _artist = new Artist
{ {
Name = "Alien Ant Farm", Name = "Alien Ant Farm",
@ -184,7 +193,7 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
GivenMultipleAlbums(); GivenMultipleAlbums();
var result = _albumRepo.GetNextAlbums(new[] { _artist.ArtistMetadataId }); var result = _albumRepo.GetNextAlbums(new[] { _artist.ArtistMetadataId });
result.Should().BeEquivalentTo(_albums.Take(1)); result.Should().BeEquivalentTo(_albums.Take(1), AlbumComparerOptions);
} }
[Test] [Test]
@ -193,7 +202,11 @@ namespace NzbDrone.Core.Test.MusicTests.AlbumRepositoryTests
GivenMultipleAlbums(); GivenMultipleAlbums();
var result = _albumRepo.GetLastAlbums(new[] { _artist.ArtistMetadataId }); 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<Album> AlbumComparerOptions(EquivalencyAssertionOptions<Album> opts) => opts.ComparingByMembers<Album>()
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
.Excluding(x => x.ArtistId);
} }
} }

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Npgsql;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Profiles.Metadata; using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
@ -159,7 +161,14 @@ namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests
_artistRepo.Insert(artist1); _artistRepo.Insert(artist1);
Action insertDupe = () => _artistRepo.Insert(artist2); Action insertDupe = () => _artistRepo.Insert(artist2);
insertDupe.Should().Throw<SQLiteException>(); if (Db.DatabaseType == DatabaseType.PostgreSQL)
{
insertDupe.Should().Throw<PostgresException>();
}
else
{
insertDupe.Should().Throw<SQLiteException>();
}
} }
} }
} }

@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.MusicTests
.BuildList(); .BuildList();
Mocker.GetMock<ITrackService>() Mocker.GetMock<ITrackService>()
.Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<IEnumerable<string>>())) .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<List<string>>()))
.Returns(_tracks); .Returns(_tracks);
} }
@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(clash); .Returns(clash);
Mocker.GetMock<ITrackService>() Mocker.GetMock<ITrackService>()
.Setup(x => x.GetTracksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetTracksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(_tracks); .Returns(_tracks);
var newInfo = existing.JsonClone(); var newInfo = existing.JsonClone();
@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.MusicTests
newInfo.Tracks = new List<Track> { newTrack }; newInfo.Tracks = new List<Track> { newTrack };
Mocker.GetMock<ITrackService>() Mocker.GetMock<ITrackService>()
.Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<IEnumerable<string>>())) .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<List<string>>()))
.Returns(new List<Track> { oldTrack }); .Returns(new List<Track> { oldTrack });
Subject.RefreshEntityInfo(_release, new List<AlbumRelease> { newInfo }, false, false, null); Subject.RefreshEntityInfo(_release, new List<AlbumRelease> { newInfo }, false, false, null);

@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(_artist); .Returns(_artist);
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny<IEnumerable<string>>())) .Setup(s => s.GetReleasesForRefresh(album1.Id, It.IsAny<List<string>>()))
.Returns(new List<AlbumRelease> { release }); .Returns(new List<AlbumRelease> { release });
Mocker.GetMock<IArtistMetadataService>() Mocker.GetMock<IArtistMetadataService>()
@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(new List<AlbumRelease>()); .Returns(new List<AlbumRelease>());
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(_releases); .Returns(_releases);
var newAlbumInfo = existing.JsonClone(); var newAlbumInfo = existing.JsonClone();
@ -209,7 +209,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Build() as List<AlbumRelease>; .Build() as List<AlbumRelease>;
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(existingReleases); .Returns(existingReleases);
Mocker.GetMock<IProvideAlbumInfo>() Mocker.GetMock<IProvideAlbumInfo>()
@ -263,7 +263,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Build() as List<AlbumRelease>; .Build() as List<AlbumRelease>;
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(existingReleases); .Returns(existingReleases);
Mocker.GetMock<IProvideAlbumInfo>() Mocker.GetMock<IProvideAlbumInfo>()
@ -318,7 +318,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Build() as List<AlbumRelease>; .Build() as List<AlbumRelease>;
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(existingReleases); .Returns(existingReleases);
Mocker.GetMock<IProvideAlbumInfo>() Mocker.GetMock<IProvideAlbumInfo>()
@ -376,7 +376,7 @@ namespace NzbDrone.Core.Test.MusicTests
.Build() as List<AlbumRelease>; .Build() as List<AlbumRelease>;
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(existingReleases); .Returns(existingReleases);
Mocker.GetMock<IProvideAlbumInfo>() Mocker.GetMock<IProvideAlbumInfo>()

@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.MusicTests
private void GivenAlbumsForRefresh(List<Album> albums) private void GivenAlbumsForRefresh(List<Album> albums)
{ {
Mocker.GetMock<IAlbumService>(MockBehavior.Strict) Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(albums); .Returns(albums);
} }
@ -229,7 +229,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IAlbumService>(MockBehavior.Strict) Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
.Returns(new List<Album>()); .Returns(new List<Album>());
// Update called twice for a move/merge // Update called twice for a move/merge
@ -289,7 +289,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IAlbumService>(MockBehavior.Strict) Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq) .InSequence(seq)
.Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny<IEnumerable<string>>())) .Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny<List<string>>()))
.Returns(_albums); .Returns(_albums);
// Update called twice for a move/merge // Update called twice for a move/merge

@ -16,7 +16,7 @@ namespace NzbDrone.Core.ArtistStats
public class ArtistStatisticsRepository : IArtistStatisticsRepository 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; private readonly IMainDatabase _database;
@ -28,14 +28,27 @@ namespace NzbDrone.Core.ArtistStats
public List<AlbumStatistics> ArtistStatistics() public List<AlbumStatistics> ArtistStatistics()
{ {
var time = DateTime.UtcNow; var time = DateTime.UtcNow;
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return Query(Builder().WherePostgres<Album>(x => x.ReleaseDate < time));
}
return Query(Builder().Where<Album>(x => x.ReleaseDate < time)); return Query(Builder().Where<Album>(x => x.ReleaseDate < time));
} }
public List<AlbumStatistics> ArtistStatistics(int artistId) public List<AlbumStatistics> ArtistStatistics(int artistId)
{ {
var time = DateTime.UtcNow; var time = DateTime.UtcNow;
if (_database.DatabaseType == DatabaseType.PostgreSQL)
{
return Query(Builder().WherePostgres<Album>(x => x.ReleaseDate < time)
.WherePostgres<Artist>(x => x.Id == artistId));
}
return Query(Builder().Where<Album>(x => x.ReleaseDate < time) return Query(Builder().Where<Album>(x => x.ReleaseDate < time)
.Where<Artist>(x => x.Id == artistId)); .Where<Artist>(x => x.Id == artistId));
} }
private List<AlbumStatistics> Query(SqlBuilder builder) private List<AlbumStatistics> Query(SqlBuilder builder)
@ -48,20 +61,23 @@ namespace NzbDrone.Core.ArtistStats
} }
} }
private SqlBuilder Builder() => new SqlBuilder() private SqlBuilder Builder()
.Select(@"Artists.Id AS ArtistId, {
Albums.Id AS AlbumId, return new SqlBuilder(_database.DatabaseType)
SUM(COALESCE(TrackFiles.Size, 0)) AS SizeOnDisk, .Select(@"""Artists"".""Id"" AS ""ArtistId"",
COUNT(Tracks.Id) AS TotalTrackCount, ""Albums"".""Id"" AS ""AlbumId"",
SUM(CASE WHEN Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount, SUM(COALESCE(""TrackFiles"".""Size"", 0)) AS ""SizeOnDisk"",
SUM(CASE WHEN Albums.Monitored = 1 OR Tracks.TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount, COUNT(""Tracks"".""Id"") AS ""TotalTrackCount"",
SUM(CASE WHEN TrackFiles.Id IS NULL THEN 0 ELSE 1 END) AS TrackFileCount") SUM(CASE WHEN ""Tracks"".""TrackFileId"" > 0 THEN 1 ELSE 0 END) AS ""AvailableTrackCount"",
.Join<Track, AlbumRelease>((t, r) => t.AlbumReleaseId == r.Id) SUM(CASE WHEN ""Albums"".""Monitored"" = true OR ""Tracks"".""TrackFileId"" > 0 THEN 1 ELSE 0 END) AS ""TrackCount"",
.Join<AlbumRelease, Album>((r, a) => r.AlbumId == a.Id) SUM(CASE WHEN ""TrackFiles"".""Id"" IS NULL THEN 0 ELSE 1 END) AS ""TrackFileCount""")
.Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) .Join<Track, AlbumRelease>((t, r) => t.AlbumReleaseId == r.Id)
.LeftJoin<Track, TrackFile>((t, f) => t.TrackFileId == f.Id) .Join<AlbumRelease, Album>((r, a) => r.AlbumId == a.Id)
.Where<AlbumRelease>(x => x.Monitored == true) .Join<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId)
.GroupBy<Artist>(x => x.Id) .LeftJoin<Track, TrackFile>((t, f) => t.TrackFileId == f.Id)
.GroupBy<Album>(x => x.Id); .Where<AlbumRelease>(x => x.Monitored == true)
.GroupBy<Artist>(x => x.Id)
.GroupBy<Album>(x => x.Id);
}
} }
} }

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Blocklisting
Delete(x => artistIds.Contains(x.ArtistId)); Delete(x => artistIds.Contains(x.ArtistId));
} }
protected override SqlBuilder PagedBuilder() => new SqlBuilder().Join<Blocklist, Artist>((b, m) => b.ArtistId == m.Id); protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType).Join<Blocklist, Artist>((b, m) => b.ArtistId == m.Id);
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Artist>(builder, (bl, artist) => protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Artist>(builder, (bl, artist) =>
{ {
bl.Artist = artist; bl.Artist = artist;

@ -4,12 +4,14 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Extensions.Options;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -49,6 +51,12 @@ namespace NzbDrone.Core.Configuration
int SyslogPort { get; } int SyslogPort { get; }
string SyslogLevel { get; } string SyslogLevel { get; }
string Theme { 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 public class ConfigFileProvider : IConfigFileProvider
@ -58,6 +66,7 @@ namespace NzbDrone.Core.Configuration
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache; private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly string _configFile; private readonly string _configFile;
@ -66,12 +75,14 @@ namespace NzbDrone.Core.Configuration
public ConfigFileProvider(IAppFolderInfo appFolderInfo, public ConfigFileProvider(IAppFolderInfo appFolderInfo,
ICacheManager cacheManager, ICacheManager cacheManager,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IDiskProvider diskProvider) IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
{ {
_cache = cacheManager.GetCache<string>(GetType()); _cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath(); _configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
} }
public Dictionary<string, object> GetConfigDictionary() public Dictionary<string, object> GetConfigDictionary()
@ -186,6 +197,12 @@ namespace NzbDrone.Core.Configuration
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string Theme => GetValue("Theme", "light", 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 bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false); public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);

@ -67,7 +67,7 @@ namespace NzbDrone.Core.Datastore
_updateSql = GetUpdateSql(_properties); _updateSql = GetUpdateSql(_properties);
} }
protected virtual SqlBuilder Builder() => new SqlBuilder(); protected virtual SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType);
protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList(); protected virtual List<TModel> Query(SqlBuilder builder) => _database.Query<TModel>(builder).ToList();
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}"); return conn.ExecuteScalar<int>($"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"; 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); SqlBuilderExtensions.LogQuery(_insertSql, model);
var multi = connection.QueryMultiple(_insertSql, model, transaction); 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); _keyProperty.SetValue(model, id);
return model; return model;
@ -293,7 +299,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var conn = _database.OpenConnection()) using (var conn = _database.OpenConnection())
{ {
conn.Execute($"DELETE FROM [{_table}]"); conn.Execute($"DELETE FROM \"{_table}\"");
} }
if (vacuum) if (vacuum)
@ -352,7 +358,7 @@ namespace NzbDrone.Core.Datastore
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate) private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendFormat("UPDATE {0} SET ", _table); sb.AppendFormat("UPDATE \"{0}\" SET ", _table);
for (var i = 0; i < propertiesToUpdate.Count; i++) for (var i = 0; i < propertiesToUpdate.Count; i++)
{ {
@ -420,9 +426,10 @@ namespace NzbDrone.Core.Datastore
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}"; pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
} }
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC"; var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize; var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}"); builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
return queryFunc(builder).ToList(); return queryFunc(builder).ToList();
} }

@ -1,7 +1,9 @@
using System; using System;
using System.Data.SQLite; using System.Data.SQLite;
using Npgsql;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
@ -14,10 +16,17 @@ namespace NzbDrone.Core.Datastore
public class ConnectionStringFactory : IConnectionStringFactory public class ConnectionStringFactory : IConnectionStringFactory
{ {
public ConnectionStringFactory(IAppFolderInfo appFolderInfo) private readonly IConfigFileProvider _configFileProvider;
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
{ {
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase()); _configFileProvider = configFileProvider;
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
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; } public string MainDbConnectionString { get; private set; }
@ -48,5 +57,19 @@ namespace NzbDrone.Core.Datastore
return connectionBuilder.ConnectionString; 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;
}
} }
} }

@ -1,5 +1,6 @@
using System; using System;
using System.Data; using System.Data;
using System.Text.RegularExpressions;
using Dapper; using Dapper;
using NLog; using NLog;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
@ -11,6 +12,7 @@ namespace NzbDrone.Core.Datastore
IDbConnection OpenConnection(); IDbConnection OpenConnection();
Version Version { get; } Version Version { get; }
int Migration { get; } int Migration { get; }
DatabaseType DatabaseType { get; }
void Vacuum(); void Vacuum();
} }
@ -32,13 +34,44 @@ namespace NzbDrone.Core.Datastore
return _datamapperFactory(); 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 public Version Version
{ {
get get
{ {
using (var db = _datamapperFactory()) using (var db = _datamapperFactory())
{ {
var version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()"); string version;
try
{
version = db.QueryFirstOrDefault<string>("SHOW server_version");
//Postgres can return extra info about operating system on version call, ignore this
version = Regex.Replace(version, @"\(.*?\)", "");
}
catch
{
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
}
return new Version(version); return new Version(version);
} }
} }
@ -50,7 +83,7 @@ namespace NzbDrone.Core.Datastore
{ {
using (var db = _datamapperFactory()) using (var db = _datamapperFactory())
{ {
return db.QueryFirstOrDefault<int>("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1"); return db.QueryFirstOrDefault<int>("SELECT \"Version\" from \"VersionInfo\" ORDER BY \"Version\" DESC LIMIT 1");
} }
} }
} }
@ -73,4 +106,10 @@ namespace NzbDrone.Core.Datastore
} }
} }
} }
public enum DatabaseType
{
SQLite,
PostgreSQL
}
} }

@ -1,6 +1,8 @@
using System; using System;
using System.Data.Common;
using System.Data.SQLite; using System.Data.SQLite;
using NLog; using NLog;
using Npgsql;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
@ -85,10 +87,19 @@ namespace NzbDrone.Core.Datastore
var db = new Database(migrationContext.MigrationType.ToString(), () => var db = new Database(migrationContext.MigrationType.ToString(), () =>
{ {
var conn = SQLiteFactory.Instance.CreateConnection(); DbConnection conn;
conn.ConnectionString = connectionString;
conn.Open();
if (connectionString.Contains(".db"))
{
conn = SQLiteFactory.Instance.CreateConnection();
conn.ConnectionString = connectionString;
}
else
{
conn = new NpgsqlConnection(connectionString);
}
conn.Open();
return conn; return conn;
}); });

@ -20,12 +20,12 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder Select(this SqlBuilder builder, params Type[] types) 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) 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) public static SqlBuilder SelectCount(this SqlBuilder builder)
@ -42,41 +42,48 @@ namespace NzbDrone.Core.Datastore
public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter) public static SqlBuilder Where<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> 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<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter)
{
var wb = new WhereBuilderPostgres(filter, true, builder.Sequence);
return builder.Where(wb.ToString(), wb.Parameters); return builder.Where(wb.ToString(), wb.Parameters);
} }
public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> filter) public static SqlBuilder OrWhere<TModel>(this SqlBuilder builder, Expression<Func<TModel, bool>> 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); return builder.OrWhere(wb.ToString(), wb.Parameters);
} }
public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) public static SqlBuilder Join<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> 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)); 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<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> filter) public static SqlBuilder LeftJoin<TLeft, TRight>(this SqlBuilder builder, Expression<Func<TLeft, TRight, bool>> 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)); 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<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property) public static SqlBuilder GroupBy<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property)
{ {
var table = TableMapping.Mapper.TableNameMapping(typeof(TModel)); var table = TableMapping.Mapper.TableNameMapping(typeof(TModel));
var propName = property.GetMemberName().Name; 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) public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type)
@ -138,6 +145,18 @@ namespace NzbDrone.Core.Datastore
return sb.ToString(); 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<string, object> ToDictionary(this DynamicParameters dynamicParams) private static Dictionary<string, object> ToDictionary(this DynamicParameters dynamicParams)
{ {
var argsDictionary = new Dictionary<string, object>(); var argsDictionary = new Dictionary<string, object>();
@ -150,11 +169,14 @@ namespace NzbDrone.Core.Datastore
} }
var templates = dynamicParams.GetType().GetField("templates", BindingFlags.NonPublic | BindingFlags.Instance); var templates = dynamicParams.GetType().GetField("templates", BindingFlags.NonPublic | BindingFlags.Instance);
if (templates != null && templates.GetValue(dynamicParams) is List<object> list) if (templates != null)
{ {
foreach (var objProps in list.Select(obj => obj.GetPropertyValuePairs().ToList())) if (templates.GetValue(dynamicParams) is List<object> 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));
}
} }
} }

@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
public class LogDatabase : ILogDatabase public class LogDatabase : ILogDatabase
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public LogDatabase(IDatabase database) public LogDatabase(IDatabase database)
{ {
_database = database; _database = database;
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
} }
public IDbConnection OpenConnection() public IDbConnection OpenConnection()
@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
public int Migration => _database.Migration; public int Migration => _database.Migration;
public DatabaseType DatabaseType => _databaseType;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

@ -10,10 +10,12 @@ namespace NzbDrone.Core.Datastore
public class MainDatabase : IMainDatabase public class MainDatabase : IMainDatabase
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly DatabaseType _databaseType;
public MainDatabase(IDatabase database) public MainDatabase(IDatabase database)
{ {
_database = database; _database = database;
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
} }
public IDbConnection OpenConnection() public IDbConnection OpenConnection()
@ -25,6 +27,8 @@ namespace NzbDrone.Core.Datastore
public int Migration => _database.Migration; public int Migration => _database.Migration;
public DatabaseType DatabaseType => _databaseType;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

@ -288,8 +288,8 @@ namespace NzbDrone.Core.Datastore.Migration
Insert.IntoTable("DelayProfiles").Row(new Insert.IntoTable("DelayProfiles").Row(new
{ {
EnableUsenet = 1, EnableUsenet = true,
EnableTorrent = 1, EnableTorrent = true,
PreferredProtocol = 1, PreferredProtocol = 1,
UsenetDelay = 0, UsenetDelay = 0,
TorrentDelay = 0, TorrentDelay = 0,

@ -12,7 +12,7 @@ namespace NzbDrone.Core.Datastore.Migration
Alter.Table("Tracks").AddColumn("MediumNumber").AsInt32().WithDefaultValue(0); Alter.Table("Tracks").AddColumn("MediumNumber").AsInt32().WithDefaultValue(0);
Alter.Table("Tracks").AddColumn("AbsoluteTrackNumber").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"); Delete.Column("TrackNumber").FromTable("Tracks");
Alter.Table("Tracks").AddColumn("TrackNumber").AsString().Nullable(); Alter.Table("Tracks").AddColumn("TrackNumber").AsString().Nullable();

@ -13,7 +13,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() 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); Execute.WithConnection(ConvertProfile);
} }
@ -172,8 +172,17 @@ namespace NzbDrone.Core.Datastore.Migration
using (var updateProfileCmd = _connection.CreateCommand()) using (var updateProfileCmd = _connection.CreateCommand())
{ {
updateProfileCmd.Transaction = _transaction; updateProfileCmd.Transaction = _transaction;
updateProfileCmd.CommandText = if (_connection.GetType().FullName == "Npgsql.NpgsqlConnection")
"UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ? WHERE Id = ?"; {
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.Name);
updateProfileCmd.AddParameter(profile.Cutoff); updateProfileCmd.AddParameter(profile.Cutoff);
updateProfileCmd.AddParameter(profile.Items.ToJson()); updateProfileCmd.AddParameter(profile.Items.ToJson());
@ -323,7 +332,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var getProfilesCmd = _connection.CreateCommand()) using (var getProfilesCmd = _connection.CreateCommand())
{ {
getProfilesCmd.Transaction = _transaction; 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()) using (var profileReader = getProfilesCmd.ExecuteReader())
{ {

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Datastore.Migration
Rename.Column("EnableSearch").OnTable("Indexers").To("EnableAutomaticSearch"); Rename.Column("EnableSearch").OnTable("Indexers").To("EnableAutomaticSearch");
Alter.Table("Indexers").AddColumn("EnableInteractiveSearch").AsBoolean().Nullable(); 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(); Alter.Table("Indexers").AlterColumn("EnableInteractiveSearch").AsBoolean().NotNullable();
} }

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() 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 " + "WHEN (CAST(MaxSize AS FLOAT) / 60) * 8 * 1024 < 1500 THEN " +
"ROUND((CAST(MaxSize AS FLOAT) / 60) * 8 * 1024, 0) " + "ROUND((CAST(MaxSize AS FLOAT) / 60) * 8 * 1024, 0) " +
"ELSE NULL " + "ELSE NULL " +

@ -115,7 +115,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var getProfilesCmd = _connection.CreateCommand()) using (var getProfilesCmd = _connection.CreateCommand())
{ {
getProfilesCmd.Transaction = _transaction; getProfilesCmd.Transaction = _transaction;
getProfilesCmd.CommandText = @"SELECT Id, Name FROM MetadataProfiles"; getProfilesCmd.CommandText = @"SELECT ""Id"", ""Name"" FROM ""MetadataProfiles""";
using (var profileReader = getProfilesCmd.ExecuteReader()) using (var profileReader = getProfilesCmd.ExecuteReader())
{ {

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("Notifications").AddColumn("OnAlbumDownload").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("OnAlbumDownload").AsBoolean().WithDefaultValue(false);
} }
} }
} }

@ -8,17 +8,17 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("UPDATE artists SET metadataProfileId = " + Execute.Sql("UPDATE \"Artists\" SET \"MetadataProfileId\" = " +
"CASE WHEN ((SELECT COUNT(*) FROM metadataprofiles) > 0) " + "CASE WHEN ((SELECT COUNT(*) FROM \"MetadataProfiles\") > 0) " +
"THEN (SELECT id FROM metadataprofiles ORDER BY id ASC LIMIT 1) " + "THEN (SELECT \"Id\" FROM \"MetadataProfiles\" ORDER BY \"Id\" ASC LIMIT 1) " +
"ELSE 0 END " + "ELSE 0 END " +
"WHERE artists.metadataProfileId == 0"); "WHERE \"Artists\".\"MetadataProfileId\" = 0");
Execute.Sql("UPDATE artists SET languageProfileId = " + Execute.Sql("UPDATE \"Artists\" SET \"LanguageProfileId\" = " +
"CASE WHEN ((SELECT COUNT(*) FROM languageprofiles) > 0) " + "CASE WHEN ((SELECT COUNT(*) FROM \"LanguageProfiles\") > 0) " +
"THEN (SELECT id FROM languageprofiles ORDER BY id ASC LIMIT 1) " + "THEN (SELECT \"Id\" FROM \"LanguageProfiles\" ORDER BY \"Id\" ASC LIMIT 1) " +
"ELSE 0 END " + "ELSE 0 END " +
"WHERE artists.languageProfileId == 0"); "WHERE \"Artists\".\"LanguageProfileId\" = 0");
} }
} }
} }

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Fanzub';"); Execute.Sql("DELETE FROM \"Indexers\" WHERE \"Implementation\" = 'Fanzub';");
} }
} }
} }

@ -71,7 +71,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var updateProfileCmd = _connection.CreateCommand()) using (var updateProfileCmd = _connection.CreateCommand())
{ {
updateProfileCmd.Transaction = _transaction; 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.Name);
updateProfileCmd.AddParameter(profile.Cutoff); updateProfileCmd.AddParameter(profile.Cutoff);
updateProfileCmd.AddParameter(profile.Items.ToJson()); updateProfileCmd.AddParameter(profile.Items.ToJson());
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var getProfilesCmd = _connection.CreateCommand()) using (var getProfilesCmd = _connection.CreateCommand())
{ {
getProfilesCmd.Transaction = _transaction; 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()) using (var profileReader = getProfilesCmd.ExecuteReader())
{ {

@ -2,11 +2,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using Dapper;
using FluentMigrator; using FluentMigrator;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.Datastore.Migration namespace NzbDrone.Core.Datastore.Migration
{ {
@ -30,18 +30,18 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("Members").AsString().Nullable(); .WithColumn("Members").AsString().Nullable();
// we want to preserve the artist ID. Shove all the metadata into the metadata table. // 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) 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 SELECT ""ForeignArtistId"", ""Name"", ""Overview"", ""Disambiguation"", ""ArtistType"", ""Status"", ""Images"", ""Links"", ""Genres"", ""Ratings"", ""Members""
FROM Artists"); FROM ""Artists""");
// Add an ArtistMetadataId column to Artists // Add an ArtistMetadataId column to Artists
Alter.Table("Artists").AddColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0); Alter.Table("Artists").AddColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0);
// Update artistmetadataId // Update artistmetadataId
Execute.Sql(@"UPDATE Artists Execute.Sql(@"UPDATE ""Artists""
SET ArtistMetadataId = (SELECT ArtistMetadata.Id SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id""
FROM ArtistMetadata FROM ""ArtistMetadata""
WHERE ArtistMetadata.ForeignArtistId = Artists.ForeignArtistId)"); WHERE ""ArtistMetadata"".""ForeignArtistId"" = ""Artists"".""ForeignArtistId"")");
// ALBUM RELEASES TABLE - Do this before we mess with the Albums table // ALBUM RELEASES TABLE - Do this before we mess with the Albums table
Create.TableForModel("AlbumReleases") Create.TableForModel("AlbumReleases")
@ -68,11 +68,11 @@ namespace NzbDrone.Core.Datastore.Migration
Alter.Table("Albums").AddColumn("Links").AsString().Nullable(); Alter.Table("Albums").AddColumn("Links").AsString().Nullable();
// Set metadata ID // Set metadata ID
Execute.Sql(@"UPDATE Albums Execute.Sql(@"UPDATE ""Albums""
SET ArtistMetadataId = (SELECT ArtistMetadata.Id SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id""
FROM ArtistMetadata FROM ""ArtistMetadata""
JOIN Artists ON ArtistMetadata.Id = Artists.ArtistMetadataId JOIN ""Artists"" ON ""ArtistMetadata"".""Id"" = ""Artists"".""ArtistMetadataId""
WHERE Albums.ArtistId = Artists.Id)"); WHERE ""Albums"".""ArtistId"" = ""Artists"".""Id"")");
// TRACKS TABLE // TRACKS TABLE
Alter.Table("Tracks").AddColumn("ForeignRecordingId").AsString().WithDefaultValue("0"); 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); Alter.Table("Tracks").AddColumn("ArtistMetadataId").AsInt32().WithDefaultValue(0);
// Set track release to the only release we've bothered populating // Set track release to the only release we've bothered populating
Execute.Sql(@"UPDATE Tracks Execute.Sql(@"UPDATE ""Tracks""
SET AlbumReleaseId = (SELECT AlbumReleases.Id SET ""AlbumReleaseId"" = (SELECT ""AlbumReleases"".""Id""
FROM AlbumReleases FROM ""AlbumReleases""
JOIN Albums ON AlbumReleases.AlbumId = Albums.Id JOIN ""Albums"" ON ""AlbumReleases"".""AlbumId"" = ""Albums"".""Id""
WHERE Albums.Id = Tracks.AlbumId)"); WHERE ""Albums"".""Id"" = ""Tracks"".""AlbumId"")");
// Set metadata ID // Set metadata ID
Execute.Sql(@"UPDATE Tracks Execute.Sql(@"UPDATE ""Tracks""
SET ArtistMetadataId = (SELECT ArtistMetadata.Id SET ""ArtistMetadataId"" = (SELECT ""ArtistMetadata"".""Id""
FROM ArtistMetadata FROM ""ArtistMetadata""
JOIN Albums ON ArtistMetadata.Id = Albums.ArtistMetadataId JOIN ""Albums"" ON ""ArtistMetadata"".""Id"" = ""Albums"".""ArtistMetadataId""
WHERE Tracks.AlbumId = Albums.Id)"); WHERE ""Tracks"".""AlbumId"" = ""Albums"".""Id"")");
// CLEAR OUT OLD COLUMNS // CLEAR OUT OLD COLUMNS
@ -188,15 +188,15 @@ namespace NzbDrone.Core.Datastore.Migration
public List<string> Label { get; set; } public List<string> Label { get; set; }
} }
private List<AlbumRelease> ReadReleasesFromAlbums(IDbConnection conn, IDbTransaction tran) private List<AlbumRelease023> ReadReleasesFromAlbums(IDbConnection conn, IDbTransaction tran)
{ {
// need to get all the old albums // need to get all the old albums
var releases = new List<AlbumRelease>(); var releases = new List<AlbumRelease023>();
using (var getReleasesCmd = conn.CreateCommand()) using (var getReleasesCmd = conn.CreateCommand())
{ {
getReleasesCmd.Transaction = tran; getReleasesCmd.Transaction = tran;
getReleasesCmd.CommandText = @"SELECT Id, CurrentRelease FROM Albums"; getReleasesCmd.CommandText = @"SELECT ""Id"", ""CurrentRelease"" FROM ""Albums""";
using (var releaseReader = getReleasesCmd.ExecuteReader()) using (var releaseReader = getReleasesCmd.ExecuteReader())
{ {
@ -205,16 +205,16 @@ namespace NzbDrone.Core.Datastore.Migration
int albumId = releaseReader.GetInt32(0); int albumId = releaseReader.GetInt32(0);
var albumRelease = Json.Deserialize<LegacyAlbumRelease>(releaseReader.GetString(1)); var albumRelease = Json.Deserialize<LegacyAlbumRelease>(releaseReader.GetString(1));
AlbumRelease toInsert = null; AlbumRelease023 toInsert = null;
if (albumRelease != null) if (albumRelease != null)
{ {
var media = new List<Medium>(); var media = new List<Medium023>();
for (var i = 1; i <= Math.Max(albumRelease.MediaCount, 1); i++) 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, AlbumId = albumId,
ForeignReleaseId = albumRelease.Id.IsNotNullOrWhiteSpace() ? albumRelease.Id : albumId.ToString(), ForeignReleaseId = albumRelease.Id.IsNotNullOrWhiteSpace() ? albumRelease.Id : albumId.ToString(),
@ -231,7 +231,7 @@ namespace NzbDrone.Core.Datastore.Migration
} }
else else
{ {
toInsert = new AlbumRelease toInsert = new AlbumRelease023
{ {
AlbumId = albumId, AlbumId = albumId,
ForeignReleaseId = albumId.ToString(), ForeignReleaseId = albumId.ToString(),
@ -239,7 +239,7 @@ namespace NzbDrone.Core.Datastore.Migration
Status = "", Status = "",
Label = new List<string>(), Label = new List<string>(),
Country = new List<string>(), Country = new List<string>(),
Media = new List<Medium> { new Medium { Name = "Unknown", Number = 1, Format = "Unknown" } }, Media = new List<Medium023> { new Medium023 { Name = "Unknown", Number = 1, Format = "Unknown" } },
Monitored = true Monitored = true
}; };
} }
@ -252,31 +252,54 @@ namespace NzbDrone.Core.Datastore.Migration
return releases; return releases;
} }
private void WriteReleasesToReleases(List<AlbumRelease> releases, IDbConnection conn, IDbTransaction tran) private void WriteReleasesToReleases(List<AlbumRelease023> releases, IDbConnection conn, IDbTransaction tran)
{ {
var dbReleases = new List<dynamic>();
foreach (var release in releases) foreach (var release in releases)
{ {
using (var writeReleaseCmd = conn.CreateCommand()) dbReleases.Add(new
{ {
writeReleaseCmd.Transaction = tran; AlbumId = release.AlbumId,
writeReleaseCmd.CommandText = ForeignReleaseId = release.ForeignReleaseId,
"INSERT INTO AlbumReleases (AlbumId, ForeignReleaseId, Title, Status, Duration, Label, Disambiguation, Country, Media, TrackCount, Monitored) " + Title = release.Title,
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; Status = release.Status,
writeReleaseCmd.AddParameter(release.AlbumId); Duration = release.Duration,
writeReleaseCmd.AddParameter(release.ForeignReleaseId); Label = release.Label.ToJson(),
writeReleaseCmd.AddParameter(release.Title); Disambiguation = release.Disambiguation,
writeReleaseCmd.AddParameter(release.Status); Country = release.Country.ToJson(),
writeReleaseCmd.AddParameter(release.Duration); Media = release.Media.ToJson(),
writeReleaseCmd.AddParameter(release.Label.ToJson()); TrackCount = release.TrackCount,
writeReleaseCmd.AddParameter(release.Disambiguation); Monitored = release.Monitored
writeReleaseCmd.AddParameter(release.Country.ToJson()); });
writeReleaseCmd.AddParameter(release.Media.ToJson());
writeReleaseCmd.AddParameter(release.TrackCount);
writeReleaseCmd.AddParameter(release.Monitored);
writeReleaseCmd.ExecuteNonQuery();
}
} }
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<string> Label { get; set; }
public string Disambiguation { get; set; }
public List<string> Country { get; set; }
public List<Medium023> 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; }
} }
} }
} }

@ -10,8 +10,8 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
Rename.Table("Profiles").To("QualityProfiles"); Rename.Table("Profiles").To("QualityProfiles");
Alter.Table("QualityProfiles").AddColumn("UpgradeAllowed").AsInt32().Nullable(); Alter.Table("QualityProfiles").AddColumn("UpgradeAllowed").AsBoolean().Nullable();
Alter.Table("LanguageProfiles").AddColumn("UpgradeAllowed").AsInt32().Nullable(); Alter.Table("LanguageProfiles").AddColumn("UpgradeAllowed").AsBoolean().Nullable();
// Set upgrade allowed for existing profiles (default will be false for new profiles) // Set upgrade allowed for existing profiles (default will be false for new profiles)
Update.Table("QualityProfiles").Set(new { UpgradeAllowed = true }).AllRows(); Update.Table("QualityProfiles").Set(new { UpgradeAllowed = true }).AllRows();

@ -9,47 +9,47 @@ namespace NzbDrone.Core.Datastore.Migration
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
// Remove any artists linked to missing metadata // Remove any artists linked to missing metadata
Execute.Sql(@"DELETE FROM Artists Execute.Sql(@"DELETE FROM ""Artists""
WHERE Id in ( WHERE ""Id"" in (
SELECT Artists.Id from Artists SELECT ""Artists"".""Id"" from ""Artists""
LEFT OUTER JOIN ArtistMetadata ON Artists.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN ""ArtistMetadata"" ON ""Artists"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
WHERE ArtistMetadata.Id IS NULL)"); WHERE ""ArtistMetadata"".""Id"" IS NULL)");
// Remove any albums linked to missing metadata // Remove any albums linked to missing metadata
Execute.Sql(@"DELETE FROM Albums Execute.Sql(@"DELETE FROM ""Albums""
WHERE Id in ( WHERE ""Id"" in (
SELECT Albums.Id from Albums SELECT ""Albums"".""Id"" from ""Albums""
LEFT OUTER JOIN ArtistMetadata ON Albums.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN ""ArtistMetadata"" ON ""Albums"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
WHERE ArtistMetadata.Id IS NULL)"); WHERE ""ArtistMetadata"".""Id"" IS NULL)");
// Remove any album releases linked to albums that were deleted // Remove any album releases linked to albums that were deleted
Execute.Sql(@"DELETE FROM AlbumReleases Execute.Sql(@"DELETE FROM ""AlbumReleases""
WHERE Id in ( WHERE ""Id"" in (
SELECT AlbumReleases.Id from AlbumReleases SELECT ""AlbumReleases"".""Id"" from ""AlbumReleases""
LEFT OUTER JOIN Albums ON Albums.Id = AlbumReleases.AlbumId LEFT OUTER JOIN ""Albums"" ON ""Albums"".""Id"" = ""AlbumReleases"".""AlbumId""
WHERE Albums.Id IS NULL)"); WHERE ""Albums"".""Id"" IS NULL)");
// Remove any tracks linked to album releases that were deleted // Remove any tracks linked to album releases that were deleted
Execute.Sql(@"DELETE FROM Tracks Execute.Sql(@"DELETE FROM ""Tracks""
WHERE Id in ( WHERE ""Id"" in (
SELECT Tracks.Id from Tracks SELECT ""Tracks"".""Id"" from ""Tracks""
LEFT OUTER JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id LEFT OUTER JOIN ""AlbumReleases"" ON ""Tracks"".""AlbumReleaseId"" = ""AlbumReleases"".""Id""
WHERE AlbumReleases.Id IS NULL)"); WHERE ""AlbumReleases"".""Id"" IS NULL)");
// Remove any tracks linked to the original missing metadata // Remove any tracks linked to the original missing metadata
Execute.Sql(@"DELETE FROM Tracks Execute.Sql(@"DELETE FROM ""Tracks""
WHERE Id in ( WHERE ""Id"" in (
SELECT Tracks.Id from Tracks SELECT ""Tracks"".""Id"" from ""Tracks""
LEFT OUTER JOIN ArtistMetadata ON Tracks.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN ""ArtistMetadata"" ON ""Tracks"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
WHERE ArtistMetadata.Id IS NULL)"); WHERE ""ArtistMetadata"".""Id"" IS NULL)");
// Remove any trackfiles linked to the deleted tracks // Remove any trackfiles linked to the deleted tracks
Execute.Sql(@"DELETE FROM TrackFiles Execute.Sql(@"DELETE FROM ""TrackFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT TrackFiles.Id FROM TrackFiles SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles""
LEFT OUTER JOIN Tracks LEFT OUTER JOIN ""Tracks""
ON TrackFiles.Id = Tracks.TrackFileId ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId""
WHERE Tracks.Id IS NULL)"); WHERE ""Tracks"".""Id"" IS NULL)");
} }
} }
} }

@ -9,11 +9,11 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("Notifications").AddColumn("OnHealthIssue").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("OnHealthIssue").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("OnImportFailure").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("OnImportFailure").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("OnTrackRetag").AsBoolean().WithDefaultValue(0); Alter.Table("Notifications").AddColumn("OnTrackRetag").AsBoolean().WithDefaultValue(false);
Delete.Column("OnDownload").FromTable("Notifications"); Delete.Column("OnDownload").FromTable("Notifications");

@ -13,45 +13,45 @@ namespace NzbDrone.Core.Datastore.Migration
Alter.Table("TrackFiles").AddColumn("Path").AsString().Nullable(); Alter.Table("TrackFiles").AddColumn("Path").AsString().Nullable();
// Remove anything where RelativePath is null // 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) // Remove anything not linked to a track (these shouldn't be present in version < 30)
Execute.Sql(@"DELETE FROM TrackFiles Execute.Sql(@"DELETE FROM ""TrackFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT TrackFiles.Id FROM TrackFiles SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles""
LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId LEFT JOIN ""Tracks"" ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId""
WHERE Tracks.Id IS NULL)"); WHERE ""Tracks"".""Id"" IS NULL)");
// Remove anything where we can't get an artist path (i.e. we don't know where it is) // Remove anything where we can't get an artist path (i.e. we don't know where it is)
Execute.Sql(@"DELETE FROM TrackFiles Execute.Sql(@"DELETE FROM ""TrackFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT TrackFiles.Id FROM TrackFiles SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles""
LEFT JOIN Albums ON TrackFiles.AlbumId = Albums.Id LEFT JOIN ""Albums"" ON ""TrackFiles"".""AlbumId"" = ""Albums"".""Id""
LEFT JOIN Artists on Artists.ArtistMetadataId = Albums.ArtistMetadataId LEFT JOIN ""Artists"" ON ""Artists"".""ArtistMetadataId"" = ""Albums"".""ArtistMetadataId""
WHERE Artists.Path IS NULL)"); WHERE ""Artists"".""Path"" IS NULL)");
// Remove anything linked to unmonitored or unidentified releases. This should ensure uniqueness of track files. // Remove anything linked to unmonitored or unidentified releases. This should ensure uniqueness of track files.
Execute.Sql(@"DELETE FROM TrackFiles Execute.Sql(@"DELETE FROM ""TrackFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT TrackFiles.Id FROM TrackFiles SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles""
LEFT JOIN Tracks ON TrackFiles.Id = Tracks.TrackFileId LEFT JOIN ""Tracks"" ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId""
LEFT JOIN AlbumReleases ON Tracks.AlbumReleaseId = AlbumReleases.Id LEFT JOIN ""AlbumReleases"" ON ""Tracks"".""AlbumReleaseId"" = ""AlbumReleases"".""Id""
WHERE AlbumReleases.Monitored = 0 WHERE ""AlbumReleases"".""Monitored"" = false
OR AlbumReleases.Monitored IS NULL)"); OR ""AlbumReleases"".""Monitored"" IS NULL)");
// Populate the full paths // Populate the full paths
Execute.Sql(@"UPDATE TrackFiles Execute.Sql(@"UPDATE ""TrackFiles""
SET Path = (SELECT Artists.Path || '" + System.IO.Path.DirectorySeparatorChar + @"' || TrackFiles.RelativePath SET ""Path"" = (SELECT ""Artists"".""Path"" || '" + System.IO.Path.DirectorySeparatorChar + @"' || ""TrackFiles"".""RelativePath""
FROM Artists FROM ""Artists""
JOIN Albums ON Albums.ArtistMetadataId = Artists.ArtistMetadataId JOIN ""Albums"" ON ""Albums"".""ArtistMetadataId"" = ""Artists"".""ArtistMetadataId""
WHERE TrackFiles.AlbumId = Albums.Id)"); WHERE ""TrackFiles"".""AlbumId"" = ""Albums"".""Id"")");
// Belt and braces to ensure uniqueness // Belt and braces to ensure uniqueness
Execute.Sql(@"DELETE FROM TrackFiles Execute.Sql(@"DELETE FROM ""TrackFiles""
WHERE rowid NOT IN ( WHERE ""Id"" NOT IN (
SELECT min(rowid) SELECT MIN(""Id"")
FROM TrackFiles FROM ""TrackFiles""
GROUP BY Path GROUP BY ""Path""
)"); )");
// Now enforce the uniqueness constraint // Now enforce the uniqueness constraint

@ -9,11 +9,11 @@ namespace NzbDrone.Core.Datastore.Migration
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
// Remove any duplicate artists // Remove any duplicate artists
Execute.Sql(@"DELETE FROM Artists Execute.Sql(@"DELETE FROM ""Artists""
WHERE Id NOT IN ( WHERE ""Id"" NOT IN (
SELECT MIN(Artists.id) from Artists SELECT MIN(""Artists"".""Id"") from ""Artists""
JOIN ArtistMetadata ON Artists.ArtistMetadataId = ArtistMetadata.Id JOIN ""ArtistMetadata"" ON ""Artists"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
GROUP BY ArtistMetadata.Id)"); GROUP BY ""ArtistMetadata"".""Id"")");
// The index exists but will be recreated as part of unique constraint // The index exists but will be recreated as part of unique constraint
Delete.Index().OnTable("Artists").OnColumn("ArtistMetadataId"); Delete.Index().OnTable("Artists").OnColumn("ArtistMetadataId");

@ -10,7 +10,7 @@ namespace NzbDrone.Core.Datastore.Migration
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.WithConnection(SetConfigValue); 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) private void SetConfigValue(IDbConnection conn, IDbTransaction tran)
@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var cmd = conn.CreateCommand()) using (var cmd = conn.CreateCommand())
{ {
cmd.Transaction = tran; 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()) using (var reader = cmd.ExecuteReader())
{ {
@ -30,7 +30,7 @@ namespace NzbDrone.Core.Datastore.Migration
using (var updateCmd = conn.CreateCommand()) using (var updateCmd = conn.CreateCommand())
{ {
updateCmd.Transaction = tran; 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.AddParameter(newValue);
updateCmd.ExecuteNonQuery(); updateCmd.ExecuteNonQuery();

@ -9,7 +9,7 @@ namespace NzbDrone.Core.Datastore.Migration
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("NamingConfig").AddColumn("MultiDiscTrackFormat").AsString().Nullable(); 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}'");
} }
} }
} }

@ -1,11 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq;
using Dapper;
using FluentMigrator; using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration namespace NzbDrone.Core.Datastore.Migration
{ {
[Migration(36)] [Migration(036)]
public class add_download_client_priority : NzbDroneMigrationBase public class add_download_client_priority : NzbDroneMigrationBase
{ {
// Need snapshot in time without having to instantiate. // Need snapshot in time without having to instantiate.
@ -22,34 +24,43 @@ namespace NzbDrone.Core.Datastore.Migration
private void InitPriorityForBackwardCompatibility(IDbConnection conn, IDbTransaction tran) private void InitPriorityForBackwardCompatibility(IDbConnection conn, IDbTransaction tran)
{ {
using (var cmd = conn.CreateCommand()) var downloadClients = conn.Query<DownloadClients036>($"SELECT \"Id\", \"Implementation\" FROM \"DownloadClients\" WHERE \"Enable\"");
if (!downloadClients.Any())
{ {
cmd.Transaction = tran; return;
cmd.CommandText = "SELECT Id, Implementation FROM DownloadClients WHERE Enable = 1"; }
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; updateCmd.Transaction = tran;
int nextTorrent = 1; if (conn.GetType().FullName == "Npgsql.NpgsqlConnection")
while (reader.Read())
{ {
var id = reader.GetInt32(0); updateCmd.CommandText = "UPDATE \"DownloadClients\" SET \"Priority\" = $1 WHERE \"Id\" = $2";
var implName = reader.GetString(1); }
else
var isUsenet = _usenetImplementations.Contains(implName); {
updateCmd.CommandText = "UPDATE \"DownloadClients\" SET \"Priority\" = ? WHERE \"Id\" = ?";
}
using (var updateCmd = conn.CreateCommand()) updateCmd.AddParameter(isUsenet ? nextUsenet++ : nextTorrent++);
{ updateCmd.AddParameter(downloadClient.Id);
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE DownloadClients SET Priority = ? WHERE Id = ?";
updateCmd.AddParameter(isUsenet ? nextUsenet++ : nextTorrent++);
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery(); updateCmd.ExecuteNonQuery();
}
}
} }
} }
} }
} }
public class DownloadClients036
{
public int Id { get; set; }
public string Implementation { get; set; }
}
} }

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Datastore.Migration
Alter.Table("RootFolders").AddColumn("DefaultMonitorOption").AsInt32().WithDefaultValue(0); Alter.Table("RootFolders").AddColumn("DefaultMonitorOption").AsInt32().WithDefaultValue(0);
Alter.Table("RootFolders").AddColumn("DefaultTags").AsString().Nullable(); Alter.Table("RootFolders").AddColumn("DefaultTags").AsString().Nullable();
Execute.WithConnection(SetDefaultOptions); IfDatabase("sqlite").Execute.WithConnection(SetDefaultOptions);
} }
private void SetDefaultOptions(IDbConnection conn, IDbTransaction tran) private void SetDefaultOptions(IDbConnection conn, IDbTransaction tran)

@ -10,8 +10,8 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
Delete.Column("AlbumFolder").FromTable("Artists"); Delete.Column("AlbumFolder").FromTable("Artists");
Execute.Sql("UPDATE NamingConfig SET StandardTrackFormat = AlbumFolderFormat || '/' || StandardTrackFormat"); Execute.Sql("UPDATE \"NamingConfig\" SET \"StandardTrackFormat\" = \"AlbumFolderFormat\" || '/' || \"StandardTrackFormat\"");
Execute.Sql("UPDATE NamingConfig SET MultiDiscTrackFormat = AlbumFolderFormat || '/' || MultiDiscTrackFormat"); Execute.Sql("UPDATE \"NamingConfig\" SET \"MultiDiscTrackFormat\" = \"AlbumFolderFormat\" || '/' || \"MultiDiscTrackFormat\"");
Delete.Column("AlbumFolderFormat").FromTable("NamingConfig"); Delete.Column("AlbumFolderFormat").FromTable("NamingConfig");
} }

@ -11,8 +11,8 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')"); IfDatabase("sqlite").Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')");
Execute.WithConnection(ConvertFileChmodToFolderChmod); IfDatabase("sqlite").Execute.WithConnection(ConvertFileChmodToFolderChmod);
} }
private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran) private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tran)

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() 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';");
} }
} }
} }

@ -34,7 +34,7 @@ namespace NzbDrone.Core.Datastore.Migration
private void ChangeEmailAddressType(IDbConnection conn, IDbTransaction tran) private void ChangeEmailAddressType(IDbConnection conn, IDbTransaction tran)
{ {
var rows = conn.Query<ProviderDefinition166>($"SELECT Id, Settings FROM Notifications WHERE Implementation = 'Email'"); var rows = conn.Query<ProviderDefinition166>($"SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Email'");
var corrected = new List<ProviderDefinition166>(); var corrected = new List<ProviderDefinition166>();
@ -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); conn.Execute(updateSql, corrected, transaction: tran);
} }

@ -1,8 +1,5 @@
using System.Data; using System.Data;
using System.Linq;
using FluentMigrator; using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration namespace NzbDrone.Core.Datastore.Migration
@ -24,7 +21,7 @@ namespace NzbDrone.Core.Datastore.Migration
var removeCompletedDownloads = false; var removeCompletedDownloads = false;
var removeFailedDownloads = true; 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") 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") 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(removeCompletedDownloads);
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0); updateClientCmd.AddParameter(removeFailedDownloads);
updateClientCmd.ExecuteNonQuery(); 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(); removeConfigCmd.ExecuteNonQuery();
} }

@ -28,7 +28,7 @@ namespace NzbDrone.Core.Datastore.Migration
Create.Index().OnTable("DownloadHistory").OnColumn("ArtistId"); Create.Index().OnTable("DownloadHistory").OnColumn("ArtistId");
Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId"); Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId");
Execute.WithConnection(InitialImportedDownloadHistory); IfDatabase("sqlite").Execute.WithConnection(InitialImportedDownloadHistory);
} }
private static readonly Dictionary<int, int> EventTypeMap = new Dictionary<int, int>() private static readonly Dictionary<int, int> EventTypeMap = new Dictionary<int, int>()

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("ImportLists").AddColumn("ShouldSearch").AsInt32().WithDefaultValue(1); Alter.Table("ImportLists").AddColumn("ShouldSearch").AsBoolean().WithDefaultValue(true);
} }
} }
} }

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsInt32().WithDefaultValue(0); Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsBoolean().WithDefaultValue(false);
} }
} }
} }

@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
{ {
protected override void MainDbUpgrade() protected override void MainDbUpgrade()
{ {
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Omgwtfnzbs'"); Delete.FromTable("Indexers").Row(new { Implementation = "Omgwtfnzbs" });
Execute.Sql("DELETE FROM Indexers WHERE Implementation = 'Waffles'"); Delete.FromTable("Indexers").Row(new { Implementation = "Waffles" });
Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable(); Alter.Table("Indexers").AddColumn("Tags").AsString().Nullable();
} }

@ -2,6 +2,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using FluentMigrator.Runner; using FluentMigrator.Runner;
using FluentMigrator.Runner.Generators;
using FluentMigrator.Runner.Initialization; using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.Processors; using FluentMigrator.Runner.Processors;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -34,11 +35,16 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_logger.Info("*** Migrating {0} ***", connectionString); _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()) .AddLogging(b => b.AddNLog())
.AddFluentMigratorCore() .AddFluentMigratorCore()
.ConfigureRunner( .ConfigureRunner(
builder => builder builder => builder
.AddPostgres()
.AddNzbDroneSQLite() .AddNzbDroneSQLite()
.WithGlobalConnectionString(connectionString) .WithGlobalConnectionString(connectionString)
.WithMigrationsIn(Assembly.GetExecutingAssembly())) .WithMigrationsIn(Assembly.GetExecutingAssembly()))
@ -48,6 +54,14 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
opt.PreviewOnly = false; opt.PreviewOnly = false;
opt.Timeout = TimeSpan.FromSeconds(60); opt.Timeout = TimeSpan.FromSeconds(60);
}) })
.Configure<SelectingProcessorAccessorOptions>(cfg =>
{
cfg.ProcessorId = db;
})
.Configure<SelectingGeneratorAccessorOptions>(cfg =>
{
cfg.GeneratorId = db;
})
.BuildServiceProvider(); .BuildServiceProvider();
using (var scope = serviceProvider.CreateScope()) using (var scope = serviceProvider.CreateScope())

@ -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;
}
}
}

@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore
public class SqlBuilder public class SqlBuilder
{ {
private readonly Dictionary<string, Clauses> _data = new Dictionary<string, Clauses>(); private readonly Dictionary<string, Clauses> _data = new Dictionary<string, Clauses>();
private readonly DatabaseType _databaseType;
public SqlBuilder(DatabaseType databaseType)
{
_databaseType = databaseType;
}
public int Sequence { get; private set; } public int Sequence { get; private set; }
public DatabaseType DatabaseType => _databaseType;
public Template AddTemplate(string sql, dynamic parameters = null) => public Template AddTemplate(string sql, dynamic parameters = null) =>
new Template(this, sql, parameters); new Template(this, sql, parameters);

@ -49,17 +49,17 @@ namespace NzbDrone.Core.Datastore
public string SelectTemplate(Type x) 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) public string DeleteTemplate(Type x)
{ {
return $"DELETE FROM {TableMap[x]} /**where**/"; return $"DELETE FROM \"{TableMap[x]}\" /**where**/";
} }
public string PageCountTemplate(Type x) 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) public bool IsValidSortKey(string sortKey)
@ -90,6 +90,35 @@ namespace NzbDrone.Core.Datastore
return true; 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 public class LazyLoadedProperty
@ -154,7 +183,7 @@ namespace NzbDrone.Core.Datastore
(db, parent) => (db, parent) =>
{ {
var id = childIdSelector(parent); var id = childIdSelector(parent);
return db.Query<TChild>(new SqlBuilder().Where<TChild>(x => x.Id == id)).SingleOrDefault(); return db.Query<TChild>(new SqlBuilder(db.DatabaseType).Where<TChild>(x => x.Id == id)).SingleOrDefault();
}, },
parent => childIdSelector(parent) > 0); parent => childIdSelector(parent) > 0);
} }

@ -105,24 +105,24 @@ namespace NzbDrone.Core.Datastore
.HasOne(a => a.Metadata, a => a.ArtistMetadataId) .HasOne(a => a.Metadata, a => a.ArtistMetadataId)
.HasOne(a => a.QualityProfile, a => a.QualityProfileId) .HasOne(a => a.QualityProfile, a => a.QualityProfileId)
.HasOne(s => s.MetadataProfile, s => s.MetadataProfileId) .HasOne(s => s.MetadataProfile, s => s.MetadataProfileId)
.LazyLoad(a => a.Albums, (db, a) => db.Query<Album>(new SqlBuilder().Where<Album>(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0); .LazyLoad(a => a.Albums, (db, a) => db.Query<Album>(new SqlBuilder(db.DatabaseType).Where<Album>(rg => rg.ArtistMetadataId == a.Id)).ToList(), a => a.Id > 0);
Mapper.Entity<ArtistMetadata>("ArtistMetadata").RegisterModel(); Mapper.Entity<ArtistMetadata>("ArtistMetadata").RegisterModel();
Mapper.Entity<Album>("Albums").RegisterModel() Mapper.Entity<Album>("Albums").RegisterModel()
.Ignore(x => x.ArtistId) .Ignore(x => x.ArtistId)
.HasOne(r => r.ArtistMetadata, r => r.ArtistMetadataId) .HasOne(r => r.ArtistMetadata, r => r.ArtistMetadataId)
.LazyLoad(a => a.AlbumReleases, (db, album) => db.Query<AlbumRelease>(new SqlBuilder().Where<AlbumRelease>(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0) .LazyLoad(a => a.AlbumReleases, (db, album) => db.Query<AlbumRelease>(new SqlBuilder(db.DatabaseType).Where<AlbumRelease>(r => r.AlbumId == album.Id)).ToList(), a => a.Id > 0)
.LazyLoad(a => a.Artist, .LazyLoad(a => a.Artist,
(db, album) => ArtistRepository.Query(db, (db, album) => ArtistRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id) .Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id)
.Where<Artist>(a => a.ArtistMetadataId == album.ArtistMetadataId)).SingleOrDefault(), .Where<Artist>(a => a.ArtistMetadataId == album.ArtistMetadataId)).SingleOrDefault(),
a => a.ArtistMetadataId > 0); a => a.ArtistMetadataId > 0);
Mapper.Entity<AlbumRelease>("AlbumReleases").RegisterModel() Mapper.Entity<AlbumRelease>("AlbumReleases").RegisterModel()
.HasOne(r => r.Album, r => r.AlbumId) .HasOne(r => r.Album, r => r.AlbumId)
.LazyLoad(x => x.Tracks, (db, release) => db.Query<Track>(new SqlBuilder().Where<Track>(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0); .LazyLoad(x => x.Tracks, (db, release) => db.Query<Track>(new SqlBuilder(db.DatabaseType).Where<Track>(t => t.AlbumReleaseId == release.Id)).ToList(), r => r.Id > 0);
Mapper.Entity<Track>("Tracks").RegisterModel() Mapper.Entity<Track>("Tracks").RegisterModel()
.Ignore(t => t.HasFile) .Ignore(t => t.HasFile)
@ -131,7 +131,7 @@ namespace NzbDrone.Core.Datastore
.HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId) .HasOne(track => track.ArtistMetadata, track => track.ArtistMetadataId)
.LazyLoad(t => t.TrackFile, .LazyLoad(t => t.TrackFile,
(db, track) => MediaFileRepository.Query(db, (db, track) => MediaFileRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<TrackFile, Track>((l, r) => l.Id == r.TrackFileId) .Join<TrackFile, Track>((l, r) => l.Id == r.TrackFileId)
.Join<TrackFile, Album>((l, r) => l.AlbumId == r.Id) .Join<TrackFile, Album>((l, r) => l.AlbumId == r.Id)
.Join<Album, Artist>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) .Join<Album, Artist>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId)
@ -140,7 +140,7 @@ namespace NzbDrone.Core.Datastore
t => t.TrackFileId > 0) t => t.TrackFileId > 0)
.LazyLoad(x => x.Artist, .LazyLoad(x => x.Artist,
(db, t) => ArtistRepository.Query(db, (db, t) => ArtistRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id) .Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id)
.Join<Artist, Album>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) .Join<Artist, Album>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId)
.Join<Album, AlbumRelease>((l, r) => l.Id == r.AlbumId) .Join<Album, AlbumRelease>((l, r) => l.Id == r.AlbumId)
@ -149,10 +149,10 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<TrackFile>("TrackFiles").RegisterModel() Mapper.Entity<TrackFile>("TrackFiles").RegisterModel()
.HasOne(f => f.Album, f => f.AlbumId) .HasOne(f => f.Album, f => f.AlbumId)
.LazyLoad(x => x.Tracks, (db, file) => db.Query<Track>(new SqlBuilder().Where<Track>(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0) .LazyLoad(x => x.Tracks, (db, file) => db.Query<Track>(new SqlBuilder(db.DatabaseType).Where<Track>(t => t.TrackFileId == file.Id)).ToList(), x => x.Id > 0)
.LazyLoad(x => x.Artist, .LazyLoad(x => x.Artist,
(db, f) => ArtistRepository.Query(db, (db, f) => ArtistRepository.Query(db,
new SqlBuilder() new SqlBuilder(db.DatabaseType)
.Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id) .Join<Artist, ArtistMetadata>((a, m) => a.ArtistMetadataId == m.Id)
.Join<Artist, Album>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId) .Join<Artist, Album>((l, r) => l.ArtistMetadataId == r.ArtistMetadataId)
.Where<Album>(a => a.Id == f.AlbumId)).SingleOrDefault(), .Where<Album>(a => a.Id == f.AlbumId)).SingleOrDefault(),

@ -1,391 +1,10 @@
using System; using Dapper;
using System.Collections.Generic; using NzbDrone.Common.Extensions;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Dapper;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
public class WhereBuilder : ExpressionVisitor public abstract class WhereBuilder : ExpressionVisitor
{ {
protected StringBuilder _sb; public DynamicParameters Parameters { get; protected set; }
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<Char> 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<string>) && !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<int>)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;
}
} }
} }

@ -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<Char> 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<string>) && !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<int>)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;
}
}
}

@ -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<Char> 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<string>) && !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<int>)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;
}
}
}

@ -90,11 +90,11 @@ namespace NzbDrone.Core.History
public List<EntityHistory> FindDownloadHistory(int idArtistId, QualityModel quality) public List<EntityHistory> 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 && return Query(h => h.ArtistId == idArtistId &&
h.Quality == quality && h.Quality == quality &&
allowed.Contains(h.EventType)); allowed.Contains((int)h.EventType));
} }
public void DeleteForArtists(List<int> artistIds) public void DeleteForArtists(List<int> artistIds)
@ -102,7 +102,7 @@ namespace NzbDrone.Core.History
Delete(c => artistIds.Contains(c.ArtistId)); Delete(c => artistIds.Contains(c.ArtistId));
} }
protected override SqlBuilder PagedBuilder() => new SqlBuilder() protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
.Join<EntityHistory, Artist>((h, a) => h.ArtistId == a.Id) .Join<EntityHistory, Artist>((h, a) => h.ArtistId == a.Id)
.Join<EntityHistory, Album>((h, a) => h.AlbumId == a.Id) .Join<EntityHistory, Album>((h, a) => h.AlbumId == a.Id)
.LeftJoin<EntityHistory, Track>((h, t) => h.TrackId == t.Id); .LeftJoin<EntityHistory, Track>((h, t) => h.TrackId == t.Id);

@ -1,4 +1,4 @@
using Dapper; using Dapper;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
@ -16,16 +16,32 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles if (_database.DatabaseType == DatabaseType.PostgreSQL)
WHERE Id IN ( {
SELECT Id FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE RelativePath 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 '_:\%' LIKE '_:\%'
OR RelativePath OR ""RelativePath""
LIKE '\%' LIKE '\%'
OR RelativePath OR ""RelativePath""
LIKE '/%' LIKE '/%'
)"); )");
}
} }
} }
} }

@ -1,4 +1,4 @@
using Dapper; using Dapper;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM NamingConfig mapper.Execute(@"DELETE FROM ""NamingConfig""
WHERE ID NOT IN ( WHERE ""Id"" NOT IN (
SELECT ID FROM NamingConfig SELECT ""Id"" FROM ""NamingConfig""
LIMIT 1)"); LIMIT 1)");
} }
} }

@ -16,9 +16,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM Users mapper.Execute(@"DELETE FROM ""Users""
WHERE ID NOT IN ( WHERE ""Id"" NOT IN (
SELECT ID FROM Users SELECT ""Id"" FROM ""Users""
LIMIT 1)"); LIMIT 1)");
} }
} }

@ -1,4 +1,4 @@
using System; using System;
using Dapper; using Dapper;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
@ -16,16 +16,29 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public void Clean() 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 mapper.Execute(@"DELETE FROM ""PendingReleases""
WHERE Added < @TwoWeeksAgo WHERE ""Added"" < @TwoWeeksAgo
AND REASON IN @Reasons", AND ""REASON"" IN @Reasons",
new new
{ {
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14), TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback } Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
}); });
} }
} }
} }

@ -24,12 +24,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE Type = 1 WHERE ""Type"" = 1
GROUP BY ArtistId, Consumer GROUP BY ""ArtistId"", ""Consumer""
HAVING COUNT(ArtistId) > 1 HAVING COUNT(""ArtistId"") > 1
)"); )");
} }
} }
@ -38,12 +38,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE Type = 6 WHERE ""Type"" = 6
GROUP BY AlbumId, Consumer GROUP BY ""AlbumId"", ""Consumer""
HAVING COUNT(AlbumId) > 1 HAVING COUNT(""AlbumId"") > 1
)"); )");
} }
} }
@ -52,12 +52,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE Type = 2 WHERE ""Type"" = 2
GROUP BY TrackFileId, Consumer GROUP BY ""TrackFileId"", ""Consumer""
HAVING COUNT(TrackFileId) > 1 HAVING COUNT(""TrackFileId"") > 1
)"); )");
} }
} }
@ -66,12 +66,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT MIN(""Id"") FROM ""MetadataFiles""
WHERE Type = 5 WHERE ""Type"" = 5
GROUP BY TrackFileId, Consumer GROUP BY ""TrackFileId"", ""Consumer""
HAVING COUNT(TrackFileId) > 1 HAVING COUNT(""TrackFileId"") > 1
)"); )");
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM Albums mapper.Execute(@"DELETE FROM ""Albums""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Albums.Id FROM Albums SELECT ""Albums"".""Id"" FROM ""Albums""
LEFT OUTER JOIN Artists LEFT OUTER JOIN ""Artists""
ON Albums.ArtistMetadataId = Artists.ArtistMetadataId ON ""Albums"".""ArtistMetadataId"" = ""Artists"".""ArtistMetadataId""
WHERE Artists.Id IS NULL)"); WHERE ""Artists"".""Id"" IS NULL)");
} }
} }
} }

@ -16,13 +16,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM ArtistMetadata mapper.Execute(@"DELETE FROM ""ArtistMetadata""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT ArtistMetadata.Id FROM ArtistMetadata SELECT ""ArtistMetadata"".""Id"" FROM ""ArtistMetadata""
LEFT OUTER JOIN Albums ON Albums.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN ""Albums"" ON ""Albums"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
LEFT OUTER JOIN Tracks ON Tracks.ArtistMetadataId = ArtistMetadata.Id LEFT OUTER JOIN ""Tracks"" ON ""Tracks"".""ArtistMetadataId"" = ""ArtistMetadata"".""Id""
LEFT OUTER JOIN Artists ON Artists.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)"); WHERE ""Albums"".""Id"" IS NULL AND ""Tracks"".""Id"" IS NULL AND ""Artists"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM Blocklist mapper.Execute(@"DELETE FROM ""Blocklist""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Blocklist.Id FROM Blocklist SELECT ""Blocklist"".""Id"" FROM ""Blocklist""
LEFT OUTER JOIN Artists LEFT OUTER JOIN ""Artists""
ON Blocklist.ArtistId = Artists.Id ON ""Blocklist"".""ArtistId"" = ""Artists"".""Id""
WHERE Artists.Id IS NULL)"); WHERE ""Artists"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM DownloadClientStatus mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT DownloadClientStatus.Id FROM DownloadClientStatus SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
LEFT OUTER JOIN DownloadClients LEFT OUTER JOIN ""DownloadClients""
ON DownloadClientStatus.ProviderId = DownloadClients.Id ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
WHERE DownloadClients.Id IS NULL)"); WHERE ""DownloadClients"".""Id"" IS NULL)");
} }
} }
} }

@ -22,12 +22,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM History mapper.Execute(@"DELETE FROM ""History""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT History.Id FROM History SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN Artists LEFT OUTER JOIN ""Artists""
ON History.ArtistId = Artists.Id ON ""History"".""ArtistId"" = ""Artists"".""Id""
WHERE Artists.Id IS NULL)"); WHERE ""Artists"".""Id"" IS NULL)");
} }
} }
@ -35,12 +35,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM History mapper.Execute(@"DELETE FROM ""History""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT History.Id FROM History SELECT ""History"".""Id"" FROM ""History""
LEFT OUTER JOIN Albums LEFT OUTER JOIN ""Albums""
ON History.AlbumId = Albums.Id ON ""History"".""AlbumId"" = ""Albums"".""Id""
WHERE Albums.Id IS NULL)"); WHERE ""Albums"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM ImportListStatus mapper.Execute(@"DELETE FROM ""ImportListStatus""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT ImportListStatus.Id FROM ImportListStatus SELECT ""ImportListStatus"".""Id"" FROM ""ImportListStatus""
LEFT OUTER JOIN ImportLists LEFT OUTER JOIN ""ImportLists""
ON ImportListStatus.ProviderId = ImportLists.Id ON ""ImportListStatus"".""ProviderId"" = ""ImportLists"".""Id""
WHERE ImportLists.Id IS NULL)"); WHERE ""ImportLists"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM IndexerStatus mapper.Execute(@"DELETE FROM ""IndexerStatus""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT IndexerStatus.Id FROM IndexerStatus SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
LEFT OUTER JOIN Indexers LEFT OUTER JOIN ""Indexers""
ON IndexerStatus.ProviderId = Indexers.Id ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
WHERE Indexers.Id IS NULL)"); WHERE ""Indexers"".""Id"" IS NULL)");
} }
} }
} }

@ -25,12 +25,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN Artists LEFT OUTER JOIN ""Artists""
ON MetadataFiles.ArtistId = Artists.Id ON ""MetadataFiles"".""ArtistId"" = ""Artists"".""Id""
WHERE Artists.Id IS NULL)"); WHERE ""Artists"".""Id"" IS NULL)");
} }
} }
@ -38,13 +38,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN Albums LEFT OUTER JOIN ""Albums""
ON MetadataFiles.AlbumId = Albums.Id ON ""MetadataFiles"".""AlbumId"" = ""Albums"".""Id""
WHERE MetadataFiles.AlbumId > 0 WHERE ""MetadataFiles"".""AlbumId"" > 0
AND Albums.Id IS NULL)"); AND ""Albums"".""Id"" IS NULL)");
} }
} }
@ -52,13 +52,13 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
LEFT OUTER JOIN TrackFiles LEFT OUTER JOIN ""TrackFiles""
ON MetadataFiles.TrackFileId = TrackFiles.Id ON ""MetadataFiles"".""TrackFileId"" = ""TrackFiles"".""Id""
WHERE MetadataFiles.TrackFileId > 0 WHERE ""MetadataFiles"".""TrackFileId"" > 0
AND TrackFiles.Id IS NULL)"); AND ""TrackFiles"".""Id"" IS NULL)");
} }
} }
@ -66,11 +66,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT ""Id"" FROM ""MetadataFiles""
WHERE Type IN (4, 6) WHERE ""Type"" IN (4, 6)
AND AlbumId = 0)"); AND ""AlbumId"" = 0)");
} }
} }
@ -78,11 +78,11 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM MetadataFiles mapper.Execute(@"DELETE FROM ""MetadataFiles""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Id FROM MetadataFiles SELECT ""Id"" FROM ""MetadataFiles""
WHERE Type IN (2, 5) WHERE ""Type"" IN (2, 5)
AND TrackFileId = 0)"); AND ""TrackFileId"" = 0)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM PendingReleases mapper.Execute(@"DELETE FROM ""PendingReleases""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT PendingReleases.Id FROM PendingReleases SELECT ""PendingReleases"".""Id"" FROM ""PendingReleases""
LEFT OUTER JOIN Artists LEFT OUTER JOIN ""Artists""
ON PendingReleases.ArtistId = Artists.Id ON ""PendingReleases"".""ArtistId"" = ""Artists"".""Id""
WHERE Artists.Id IS NULL)"); WHERE ""Artists"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM AlbumReleases mapper.Execute(@"DELETE FROM ""AlbumReleases""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT AlbumReleases.Id FROM AlbumReleases SELECT ""AlbumReleases"".""Id"" FROM ""AlbumReleases""
LEFT OUTER JOIN Albums LEFT OUTER JOIN ""Albums""
ON AlbumReleases.AlbumId = Albums.Id ON ""AlbumReleases"".""AlbumId"" = ""Albums"".""Id""
WHERE Albums.Id IS NULL)"); WHERE ""Albums"".""Id"" IS NULL)");
} }
} }
} }

@ -17,22 +17,22 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
// Unlink where track no longer exists // Unlink where track no longer exists
mapper.Execute(@"UPDATE TrackFiles mapper.Execute(@"UPDATE ""TrackFiles""
SET AlbumId = 0 SET ""AlbumId"" = 0
WHERE Id IN ( WHERE ""Id"" IN (
SELECT TrackFiles.Id FROM TrackFiles SELECT ""TrackFiles"".""Id"" FROM ""TrackFiles""
LEFT OUTER JOIN Tracks LEFT OUTER JOIN ""Tracks""
ON TrackFiles.Id = Tracks.TrackFileId ON ""TrackFiles"".""Id"" = ""Tracks"".""TrackFileId""
WHERE Tracks.Id IS NULL)"); WHERE ""Tracks"".""Id"" IS NULL)");
// Unlink Tracks where the Trackfiles entry no longer exists // Unlink Tracks where the Trackfiles entry no longer exists
mapper.Execute(@"UPDATE Tracks mapper.Execute(@"UPDATE ""Tracks""
SET TrackFileId = 0 SET ""TrackFileId"" = 0
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Tracks.Id FROM Tracks SELECT ""Tracks"".""Id"" FROM ""Tracks""
LEFT OUTER JOIN TrackFiles LEFT OUTER JOIN ""TrackFiles""
ON Tracks.TrackFileId = TrackFiles.Id ON ""Tracks"".""TrackFileId"" = ""TrackFiles"".""Id""
WHERE TrackFiles.Id IS NULL)"); WHERE ""TrackFiles"".""Id"" IS NULL)");
} }
} }
} }

@ -16,12 +16,12 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"DELETE FROM Tracks mapper.Execute(@"DELETE FROM ""Tracks""
WHERE Id IN ( WHERE ""Id"" IN (
SELECT Tracks.Id FROM Tracks SELECT ""Tracks"".""Id"" FROM ""Tracks""
LEFT OUTER JOIN AlbumReleases LEFT OUTER JOIN ""AlbumReleases""
ON Tracks.AlbumReleaseId = AlbumReleases.Id ON ""Tracks"".""AlbumReleaseId"" = ""AlbumReleases"".""Id""
WHERE AlbumReleases.Id IS NULL)"); WHERE ""AlbumReleases"".""Id"" IS NULL)");
} }
} }
} }

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using Dapper; using Dapper;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
@ -24,15 +25,29 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
.Distinct() .Distinct()
.ToArray(); .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) private int[] GetUsedTags(string table, IDbConnection mapper)
{ {
return mapper.Query<List<int>>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]' AND NOT Tags IS NULL") return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
.SelectMany(x => x) .SelectMany(x => x)
.Distinct() .Distinct()
.ToArray(); .ToArray();

@ -26,9 +26,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
using (var mapper = _database.OpenConnection()) using (var mapper = _database.OpenConnection())
{ {
mapper.Execute(@"UPDATE ScheduledTasks mapper.Execute(@"UPDATE ""ScheduledTasks""
SET LastExecution = @time SET ""LastExecution"" = @time
WHERE LastExecution > @time", WHERE ""LastExecution"" > @time",
new { time = DateTime.UtcNow }); new { time = DateTime.UtcNow });
} }
} }

@ -1,9 +1,11 @@
using System.Data; using System;
using System.Data;
using System.Data.SQLite; using System.Data.SQLite;
using NLog; using NLog;
using NLog.Common; using NLog.Common;
using NLog.Config; using NLog.Config;
using NLog.Targets; using NLog.Targets;
using Npgsql;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
@ -13,7 +15,7 @@ namespace NzbDrone.Core.Instrumentation
{ {
public class DatabaseTarget : TargetWithLayout, IHandle<ApplicationShutdownRequested> public class DatabaseTarget : TargetWithLayout, IHandle<ApplicationShutdownRequested>
{ {
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)"; "VALUES(@Message,@Time,@Logger,@Exception,@ExceptionType,@Level)";
private readonly IConnectionStringFactory _connectionStringFactory; private readonly IConnectionStringFactory _connectionStringFactory;
@ -55,7 +57,6 @@ namespace NzbDrone.Core.Instrumentation
{ {
try try
{ {
using var connection = new SQLiteConnection(_connectionStringFactory.LogDbConnectionString).OpenAndReturn();
var log = new Log(); var log = new Log();
log.Time = logEvent.TimeStamp; log.Time = logEvent.TimeStamp;
log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage); log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage);
@ -84,16 +85,17 @@ namespace NzbDrone.Core.Instrumentation
log.Level = logEvent.Level.Name; 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 }); //TODO: Probably need more robust way to differentiate what's being used
sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() }); if (connectionString.Contains(".db"))
sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger }); {
sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception }); WriteSqliteLog(log, connectionString);
sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType }); }
sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level }); else
{
sqlCommand.ExecuteNonQuery(); WritePostgresLog(log, connectionString);
}
} }
catch (SQLiteException ex) 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) public void Handle(ApplicationShutdownRequested message)
{ {
if (LogManager.Configuration != null && LogManager.Configuration.LoggingRules.Contains(Rule)) if (LogManager.Configuration != null && LogManager.Configuration.LoggingRules.Contains(Rule))

@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" /> <PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" /> <PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="FluentValidation" Version="8.6.2" /> <PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="MailKit" Version="2.15.0" /> <PackageReference Include="MailKit" Version="2.15.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
@ -20,6 +21,7 @@
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" /> <PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" /> <PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" />
<PackageReference Include="Kveer.XmlRPC" Version="1.2.0" /> <PackageReference Include="Kveer.XmlRPC" Version="1.2.0" />
<PackageReference Include="Npgsql" Version="6.0.3" />
<PackageReference Include="SpotifyAPI.Web" Version="4.2.2" /> <PackageReference Include="SpotifyAPI.Web" Version="4.2.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="Equ" Version="2.3.0" /> <PackageReference Include="Equ" Version="2.3.0" />

@ -152,6 +152,7 @@
"CustomFilters": "Custom Filters", "CustomFilters": "Custom Filters",
"CutoffHelpText": "Once this quality is reached Lidarr will no longer download albums", "CutoffHelpText": "Once this quality is reached Lidarr will no longer download albums",
"CutoffUnmet": "Cutoff Unmet", "CutoffUnmet": "Cutoff Unmet",
"Database": "Database",
"Date": "Date", "Date": "Date",
"DateAdded": "Date Added", "DateAdded": "Date Added",
"Dates": "Dates", "Dates": "Dates",

@ -31,7 +31,7 @@ namespace NzbDrone.Core.MediaFiles
// always join with all the other good stuff // always join with all the other good stuff
// needed more often than not so better to load it all now // 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<TrackFile, Track>((t, x) => t.Id == x.TrackFileId) .LeftJoin<TrackFile, Track>((t, x) => t.Id == x.TrackFileId)
.LeftJoin<TrackFile, Album>((t, a) => t.AlbumId == a.Id) .LeftJoin<TrackFile, Album>((t, a) => t.AlbumId == a.Id)
.LeftJoin<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId) .LeftJoin<Album, Artist>((album, artist) => album.ArtistMetadataId == artist.ArtistMetadataId)
@ -90,7 +90,7 @@ namespace NzbDrone.Core.MediaFiles
{ {
//x.Id == null is converted to SQL, so warning incorrect //x.Id == null is converted to SQL, so warning incorrect
#pragma warning disable CS0472 #pragma warning disable CS0472
return _database.Query<TrackFile>(new SqlBuilder().Select(typeof(TrackFile)) return _database.Query<TrackFile>(new SqlBuilder(_database.DatabaseType).Select(typeof(TrackFile))
.LeftJoin<TrackFile, Track>((f, t) => f.Id == t.TrackFileId) .LeftJoin<TrackFile, Track>((f, t) => f.Id == t.TrackFileId)
.Where<Track>(t => t.Id == null)).ToList(); .Where<Track>(t => t.Id == null)).ToList();
#pragma warning restore CS0472 #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 // ensure path ends with a single trailing path separator to avoid matching partial paths
var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
return _database.Query<TrackFile>(new SqlBuilder().Where<TrackFile>(x => x.Path.StartsWith(safePath))).ToList(); return _database.Query<TrackFile>(new SqlBuilder(_database.DatabaseType).Where<TrackFile>(x => x.Path.StartsWith(safePath))).ToList();
} }
public TrackFile GetFileWithPath(string path) public TrackFile GetFileWithPath(string path)
@ -128,7 +128,7 @@ namespace NzbDrone.Core.MediaFiles
public List<TrackFile> GetFileWithPath(List<string> paths) public List<TrackFile> GetFileWithPath(List<string> paths)
{ {
// use more limited join for speed // use more limited join for speed
var builder = new SqlBuilder() var builder = new SqlBuilder(_database.DatabaseType)
.LeftJoin<TrackFile, Track>((f, t) => f.Id == t.TrackFileId); .LeftJoin<TrackFile, Track>((f, t) => f.Id == t.TrackFileId);
var dict = new Dictionary<int, TrackFile>(); var dict = new Dictionary<int, TrackFile>();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save