mirror of
https://github.com/Readarr/Readarr
synced 2026-02-12 01:33:20 +01:00
New: Postgres Support
Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com> Co-authored-by: ta264 <ta264@users.noreply.github.com> (cherry picked from commit 80b1aa9a2c81617bdda7ef551c19a2f114e49204)
This commit is contained in:
parent
8616373f96
commit
46c2e0ba82
94 changed files with 2059 additions and 726 deletions
|
|
@ -529,6 +529,56 @@ stages:
|
|||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: LinuxCoreTests
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
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: 'linux-x64-Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.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=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
|
|
@ -597,6 +647,65 @@ stages:
|
|||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
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/Readarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
workspace:
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class About extends Component {
|
|||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
|
|
@ -77,6 +79,11 @@ class About extends Component {
|
|||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('AppDataDirectory')}
|
||||
data={appData}
|
||||
|
|
@ -118,6 +125,8 @@ About.propTypes = {
|
|||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="FluentMigrator.Runner" Version="3.3.2" />
|
||||
<PackageVersion Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
|
||||
<PackageVersion Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
|
||||
<PackageVersion Include="FluentValidation" Version="8.6.2" />
|
||||
<PackageVersion Include="Ical.Net" Version="4.2.0" />
|
||||
<PackageVersion Include="ImpromptuInterface" Version="7.0.1" />
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
<PackageVersion Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageVersion Include="NLog" Version="4.7.14" />
|
||||
<PackageVersion Include="NLog.Targets.Syslog" Version="6.0.2" />
|
||||
<PackageVersion Include="Npgsql" Version="6.0.3" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||
<PackageVersion Include="NunitXml.TestLogger" Version="3.0.117" />
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public void SmokeTestSetup()
|
|||
|
||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public class CleanseLogMessageFixture
|
|||
[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("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=readarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
|
@ -29,7 +31,8 @@ public void event_handlers_should_be_unique()
|
|||
.AddDummyDatabase()
|
||||
.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();
|
||||
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class CleanseLogMessage
|
|||
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(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=[?& ;])[^=]*?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// 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"),
|
||||
|
|
|
|||
|
|
@ -127,7 +127,18 @@ public Process Start(string path, string args = null, StringDictionary environme
|
|||
try
|
||||
{
|
||||
_logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value);
|
||||
startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString());
|
||||
|
||||
var key = environmentVariable.Key.ToString();
|
||||
var value = environmentVariable.Value?.ToString();
|
||||
|
||||
if (startInfo.EnvironmentVariables.ContainsKey(key))
|
||||
{
|
||||
startInfo.EnvironmentVariables[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.EnvironmentVariables.Add(key, value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public class DatabaseFixture : DbTest
|
|||
public void SingleOrDefault_should_return_null_on_empty_db()
|
||||
{
|
||||
Mocker.Resolve<IDatabase>()
|
||||
.OpenConnection().Query<Author>("SELECT * FROM Authors")
|
||||
.OpenConnection().Query<Author>("SELECT * FROM \"Authors\"")
|
||||
.SingleOrDefault(c => c.CleanName == "SomeTitle")
|
||||
.Should()
|
||||
.BeNull();
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ public void Setup()
|
|||
public void should_lazy_load_author_for_trackfile()
|
||||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var tracks = db.Query<BookFile>(new SqlBuilder()).ToList();
|
||||
var tracks = db.Query<BookFile>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||
|
||||
Assert.IsNotEmpty(tracks);
|
||||
foreach (var track in tracks)
|
||||
|
|
@ -94,7 +94,7 @@ public void should_lazy_load_author_for_trackfile()
|
|||
public void should_lazy_load_trackfile_if_not_joined()
|
||||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var tracks = db.Query<Book>(new SqlBuilder()).ToList();
|
||||
var tracks = db.Query<Book>(new SqlBuilder(db.DatabaseType)).ToList();
|
||||
|
||||
foreach (var track in tracks)
|
||||
{
|
||||
|
|
@ -109,7 +109,7 @@ public void should_explicit_load_everything_if_joined()
|
|||
{
|
||||
var db = Mocker.Resolve<IDatabase>();
|
||||
var files = MediaFileRepository.Query(db,
|
||||
new SqlBuilder()
|
||||
new SqlBuilder(db.DatabaseType)
|
||||
.Join<BookFile, Edition>((t, a) => t.EditionId == a.Id)
|
||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||
|
|
|
|||
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
211
src/NzbDrone.Core.Test/Datastore/WhereBuilderPostgresFixture.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class WhereBuilderPostgresFixture : CoreTest
|
||||
{
|
||||
private WhereBuilderPostgres _subject;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void MapTables()
|
||||
{
|
||||
// Generate table mapping
|
||||
Mocker.Resolve<DbFactory>();
|
||||
}
|
||||
|
||||
private WhereBuilderPostgres Where(Expression<Func<Author, bool>> filter)
|
||||
{
|
||||
return new WhereBuilderPostgres(filter, true, 0);
|
||||
}
|
||||
|
||||
private WhereBuilderPostgres WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
{
|
||||
return new WhereBuilderPostgres(filter, true, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_const()
|
||||
{
|
||||
_subject = Where(x => x.Id == 10);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_variable()
|
||||
{
|
||||
var id = 10;
|
||||
_subject = Where(x => x.Id == id);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_property()
|
||||
{
|
||||
var author = new Author { Id = 10 };
|
||||
_subject = Where(x => x.Id == author.Id);
|
||||
|
||||
_subject.Parameters.ParameterNames.Should().HaveCount(1);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = @Clause1_P1)");
|
||||
_subject.Parameters.Get<int>("Clause1_P1").Should().Be(author.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_joined_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 postgres_where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilderPostgres(filter, true, 0);
|
||||
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilderPostgres(filter, false, 0);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_is_null()
|
||||
{
|
||||
_subject = Where(x => x.CleanName == null);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_is_null_value()
|
||||
{
|
||||
string cleanName = null;
|
||||
_subject = Where(x => x.CleanName == cleanName);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_equal_null_property()
|
||||
{
|
||||
var author = new Author { CleanName = null };
|
||||
_subject = Where(x => x.CleanName == author.CleanName);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" IS NULL)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_contains_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.Contains(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1 || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_string_contains_column()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => test.Contains(x.CleanName));
|
||||
|
||||
_subject.ToString().Should().Be($"(@Clause1_P1 ILIKE '%' || \"Authors\".\"CleanName\" || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_starts_with_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.StartsWith(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE @Clause1_P1 || '%')");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_column_ends_with_string()
|
||||
{
|
||||
var test = "small";
|
||||
_subject = Where(x => x.CleanName.EndsWith(test));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" ILIKE '%' || @Clause1_P1)");
|
||||
_subject.Parameters.Get<string>("Clause1_P1").Should().Be(test);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_in_list()
|
||||
{
|
||||
var list = new List<int> { 1, 2, 3 };
|
||||
_subject = Where(x => list.Contains(x.Id));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}')))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_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($"((\"Authors\".\"CleanName\" = @Clause1_P1) AND (\"Authors\".\"Id\" = ANY (('{{1, 2, 3}}'))))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void postgres_where_in_string_list()
|
||||
{
|
||||
var list = new List<string> { "first", "second", "third" };
|
||||
|
||||
_subject = Where(x => list.Contains(x.CleanName));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"CleanName\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_as_int()
|
||||
{
|
||||
_subject = WhereMetadata(x => x.Status == AuthorStatusType.Continuing);
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = @Clause1_P1)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_in_list()
|
||||
{
|
||||
var allowed = new List<AuthorStatusType> { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void enum_in_array()
|
||||
{
|
||||
var allowed = new AuthorStatusType[] { AuthorStatusType.Continuing, AuthorStatusType.Ended };
|
||||
_subject = WhereMetadata(x => allowed.Contains(x.Status));
|
||||
|
||||
_subject.ToString().Should().Be($"(\"AuthorMetadata\".\"Status\" = ANY (@Clause1_P1))");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@
|
|||
namespace NzbDrone.Core.Test.Datastore
|
||||
{
|
||||
[TestFixture]
|
||||
public class WhereBuilderFixture : CoreTest
|
||||
public class WhereBuilderSqliteFixture : CoreTest
|
||||
{
|
||||
private WhereBuilder _subject;
|
||||
private WhereBuilderSqlite _subject;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void MapTables()
|
||||
|
|
@ -22,14 +22,14 @@ public void MapTables()
|
|||
Mocker.Resolve<DbFactory>();
|
||||
}
|
||||
|
||||
private WhereBuilder Where(Expression<Func<Author, bool>> filter)
|
||||
private WhereBuilderSqlite Where(Expression<Func<Author, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(filter, true, 0);
|
||||
}
|
||||
|
||||
private WhereBuilder WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
private WhereBuilderSqlite WhereMetadata(Expression<Func<AuthorMetadata, bool>> filter)
|
||||
{
|
||||
return new WhereBuilder(filter, true, 0);
|
||||
return new WhereBuilderSqlite(filter, true, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -76,7 +76,7 @@ public void where_equal_lazy_property()
|
|||
public void where_throws_without_concrete_condition_if_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilder(filter, true, 0);
|
||||
_subject = new WhereBuilderSqlite(filter, true, 0);
|
||||
Assert.Throws<InvalidOperationException>(() => _subject.ToString());
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ public void where_throws_without_concrete_condition_if_requiresConcreteCondition
|
|||
public void where_allows_abstract_condition_if_not_requiresConcreteCondition()
|
||||
{
|
||||
Expression<Func<Author, Author, bool>> filter = (x, y) => x.Id == y.Id;
|
||||
_subject = new WhereBuilder(filter, false, 0);
|
||||
_subject = new WhereBuilderSqlite(filter, false, 0);
|
||||
_subject.ToString().Should().Be($"(\"Authors\".\"Id\" = \"Authors\".\"Id\")");
|
||||
}
|
||||
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Test.Common.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
|
|
@ -47,6 +53,7 @@ protected TSubject Subject
|
|||
public abstract class DbTest : CoreTest
|
||||
{
|
||||
private ITestDatabase _db;
|
||||
private DatabaseType _databaseType;
|
||||
|
||||
protected virtual MigrationType MigrationType => MigrationType.Main;
|
||||
|
||||
|
|
@ -65,8 +72,7 @@ protected ITestDatabase Db
|
|||
|
||||
protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
||||
{
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
var database = factory.Create(migrationContext);
|
||||
var database = CreateDatabase(migrationContext);
|
||||
Mocker.SetConstant(database);
|
||||
|
||||
switch (MigrationType)
|
||||
|
|
@ -98,6 +104,65 @@ protected virtual ITestDatabase WithTestDb(MigrationContext migrationContext)
|
|||
return testDb;
|
||||
}
|
||||
|
||||
private IDatabase CreateDatabase(MigrationContext migrationContext)
|
||||
{
|
||||
if (_databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
CreatePostgresDb();
|
||||
}
|
||||
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
|
||||
// If a special migration test or log migration then create new
|
||||
if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
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
|
||||
var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType);
|
||||
var testDb = GetTestSqliteDb(migrationContext.MigrationType);
|
||||
if (File.Exists(cachedDb))
|
||||
{
|
||||
TestLogger.Info($"Using cached initial database {cachedDb}");
|
||||
File.Copy(cachedDb, testDb);
|
||||
return factory.Create(migrationContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
var db = factory.Create(migrationContext);
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
SQLiteConnection.ClearAllPools();
|
||||
|
||||
TestLogger.Info("Caching database");
|
||||
File.Copy(testDb, cachedDb);
|
||||
return db;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTestSqliteDb(MigrationType type)
|
||||
{
|
||||
return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase();
|
||||
}
|
||||
|
||||
protected virtual void SetupLogging()
|
||||
{
|
||||
Mocker.SetConstant<ILoggerProvider>(NullLoggerProvider.Instance);
|
||||
|
|
@ -108,6 +173,13 @@ protected void SetupContainer()
|
|||
WithTempAsAppPath();
|
||||
SetupLogging();
|
||||
|
||||
// populate the possible postgres options
|
||||
var postgresOptions = PostgresDatabase.GetTestOptions();
|
||||
_databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite;
|
||||
|
||||
// Set up remaining container services
|
||||
Mocker.SetConstant(Options.Create(postgresOptions));
|
||||
Mocker.SetConstant<IConfigFileProvider>(Mocker.Resolve<ConfigFileProvider>());
|
||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||
|
||||
|
|
@ -127,12 +199,19 @@ public void TearDown()
|
|||
// Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
SQLiteConnection.ClearAllPools();
|
||||
NpgsqlConnection.ClearAllPools();
|
||||
|
||||
if (TestFolderInfo != null)
|
||||
{
|
||||
DeleteTempFolder(TestFolderInfo.AppDataFolder);
|
||||
}
|
||||
|
||||
if (_databaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
DropPostgresDb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ void Delete<T>(T childModel)
|
|||
where T : ModelBase, new();
|
||||
IDirectDataMapper GetDirectDataMapper();
|
||||
IDbConnection OpenConnection();
|
||||
DatabaseType DatabaseType { get; }
|
||||
}
|
||||
|
||||
public class TestDatabase : ITestDatabase
|
||||
|
|
@ -30,6 +31,8 @@ public class TestDatabase : ITestDatabase
|
|||
private readonly IDatabase _dbConnection;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public DatabaseType DatabaseType => _dbConnection.DatabaseType;
|
||||
|
||||
public TestDatabase(IDatabase dbConnection)
|
||||
{
|
||||
_eventAggregator = new Mock<IEventAggregator>().Object;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Equivalency;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
|
|
@ -21,6 +23,13 @@ public class BookRepositoryFixture : DbTest<BookService, Book>
|
|||
[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;
|
||||
});
|
||||
|
||||
_author = new Author
|
||||
{
|
||||
Name = "Alien Ant Farm",
|
||||
|
|
@ -143,7 +152,7 @@ public void get_next_books_should_return_next_book()
|
|||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetNextBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -152,7 +161,11 @@ public void get_last_books_should_return_next_book()
|
|||
GivenMultipleBooks();
|
||||
|
||||
var result = _bookRepo.GetLastBooks(new[] { _author.AuthorMetadataId });
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1));
|
||||
result.Should().BeEquivalentTo(_books.Skip(2).Take(1), BookComparerOptions);
|
||||
}
|
||||
|
||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||
.Excluding(x => x.AuthorId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
using System.Data.SQLite;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Npgsql;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
|
@ -145,7 +147,14 @@ public void should_throw_sql_exception_adding_duplicate_author()
|
|||
_authorRepo.Insert(author1);
|
||||
|
||||
Action insertDupe = () => _authorRepo.Insert(author2);
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
if (Db.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
insertDupe.Should().Throw<PostgresException>();
|
||||
}
|
||||
else
|
||||
{
|
||||
insertDupe.Should().Throw<SQLiteException>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ private void GivenAuthorFiles()
|
|||
private void GivenBooksForRefresh(List<Book> books)
|
||||
{
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(s => s.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||
.Returns(books);
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ public void should_update_if_musicbrainz_id_changed_and_no_clash()
|
|||
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.InSequence(seq)
|
||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(It.IsAny<int>(), It.IsAny<List<string>>()))
|
||||
.Returns(new List<Book>());
|
||||
|
||||
// Update called twice for a move/merge
|
||||
|
|
@ -298,7 +298,7 @@ public void should_merge_if_musicbrainz_id_changed_and_new_id_already_exists()
|
|||
|
||||
Mocker.GetMock<IBookService>(MockBehavior.Strict)
|
||||
.InSequence(seq)
|
||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<IEnumerable<string>>()))
|
||||
.Setup(x => x.GetBooksForRefresh(clash.AuthorMetadataId, It.IsAny<List<string>>()))
|
||||
.Returns(_books);
|
||||
|
||||
// Update called twice for a move/merge
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public interface IAuthorStatisticsRepository
|
|||
|
||||
public class AuthorStatisticsRepository : IAuthorStatisticsRepository
|
||||
{
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM Editions /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
private const string _selectTemplate = "SELECT /**select**/ FROM \"Editions\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/";
|
||||
|
||||
private readonly IMainDatabase _database;
|
||||
|
||||
|
|
@ -45,14 +45,14 @@ private List<BookStatistics> Query(SqlBuilder builder)
|
|||
}
|
||||
}
|
||||
|
||||
private SqlBuilder Builder() => new SqlBuilder()
|
||||
.Select(@"Authors.Id AS AuthorId,
|
||||
Books.Id AS BookId,
|
||||
SUM(COALESCE(BookFiles.Size, 0)) AS SizeOnDisk,
|
||||
1 AS TotalBookCount,
|
||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE 1 END AS AvailableBookCount,
|
||||
CASE WHEN (Books.Monitored = 1 AND (Books.ReleaseDate < @currentDate) OR Books.ReleaseDate IS NULL) OR BookFiles.Id IS NOT NULL THEN 1 ELSE 0 END AS BookCount,
|
||||
CASE WHEN BookFiles.Id IS NULL THEN 0 ELSE COUNT(BookFiles.Id) END AS BookFileCount")
|
||||
private SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Select(@"""Authors"".""Id"" AS ""AuthorId"",
|
||||
""Books"".""Id"" AS ""BookId"",
|
||||
SUM(COALESCE(""BookFiles"".""Size"", 0)) AS ""SizeOnDisk"",
|
||||
1 AS ""TotalBookCount"",
|
||||
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE 1 END AS ""AvailableBookCount"",
|
||||
CASE WHEN (""Books"".""Monitored"" = true AND (""Books"".""ReleaseDate"" < @currentDate) OR ""Books"".""ReleaseDate"" IS NULL) OR MIN(""BookFiles"".""Id"") IS NOT NULL THEN 1 ELSE 0 END AS ""BookCount"",
|
||||
CASE WHEN MIN(""BookFiles"".""Id"") IS NULL THEN 0 ELSE COUNT(""BookFiles"".""Id"") END AS ""BookFileCount""")
|
||||
.Join<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.Join<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||
.LeftJoin<Edition, BookFile>((t, f) => t.Id == f.EditionId)
|
||||
|
|
|
|||
|
|
@ -183,9 +183,12 @@ private void Cleanup()
|
|||
|
||||
private void BackupDatabase()
|
||||
{
|
||||
_logger.ProgressDebug("Backing up database");
|
||||
if (_maindDb.DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
_logger.ProgressDebug("Backing up database");
|
||||
|
||||
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
|
||||
_makeDatabaseBackup.BackupDatabase(_maindDb, _backupTempFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void BackupConfigFile()
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public List<Blocklist> BlocklistedByAuthor(int authorId)
|
|||
return Query(b => b.AuthorId == authorId);
|
||||
}
|
||||
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder()
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<Blocklist, Author>((b, m) => b.AuthorId == m.Id)
|
||||
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id);
|
||||
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) => _database.QueryJoined<Blocklist, Author, AuthorMetadata>(builder,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public AuthorRepository(IMainDatabase database,
|
|||
{
|
||||
}
|
||||
|
||||
protected override SqlBuilder Builder() => new SqlBuilder()
|
||||
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id);
|
||||
|
||||
protected override List<Author> Query(SqlBuilder builder) => Query(_database, builder).ToList();
|
||||
|
|
@ -60,7 +60,7 @@ public Dictionary<int, string> AllAuthorPaths()
|
|||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
var strSql = "SELECT Id AS [Key], Path AS [Value] FROM Authors";
|
||||
var strSql = "SELECT \"Id\" AS \"Key\", \"Path\" AS \"Value\" FROM \"Authors\"";
|
||||
return conn.Query<KeyValuePair<int, string>>(strSql).ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public interface IBookRepository : IBasicRepository<Book>
|
|||
List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
||||
List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
|
||||
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
|
||||
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
||||
Book FindByTitle(int authorMetadataId, string title);
|
||||
Book FindById(string foreignBookId);
|
||||
|
|
@ -44,17 +44,35 @@ public List<Book> GetBooks(int authorId)
|
|||
public List<Book> GetLastBooks(IEnumerable<int> authorMetadataIds)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.Having("Books.ReleaseDate = MAX(Books.ReleaseDate)"));
|
||||
|
||||
var inner = Builder()
|
||||
.Select("MIN(\"Books\".\"Id\") as id, MAX(\"Books\".\"ReleaseDate\") as date")
|
||||
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate < now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.AddSelectTemplate(typeof(Book));
|
||||
|
||||
var outer = Builder()
|
||||
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
|
||||
.AddParameters(inner.Parameters);
|
||||
|
||||
return Query(outer);
|
||||
}
|
||||
|
||||
public List<Book> GetNextBooks(IEnumerable<int> authorMetadataIds)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Query(Builder().Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.Having("Books.ReleaseDate = MIN(Books.ReleaseDate)"));
|
||||
|
||||
var inner = Builder()
|
||||
.Select("MIN(\"Books\".\"Id\") as id, MIN(\"Books\".\"ReleaseDate\") as date")
|
||||
.Where<Book>(x => authorMetadataIds.Contains(x.AuthorMetadataId) && x.ReleaseDate > now)
|
||||
.GroupBy<Book>(x => x.AuthorMetadataId)
|
||||
.AddSelectTemplate(typeof(Book));
|
||||
|
||||
var outer = Builder()
|
||||
.Join($"({inner.RawSql}) ids on ids.id = \"Books\".\"Id\" and ids.date = \"Books\".\"ReleaseDate\"")
|
||||
.AddParameters(inner.Parameters);
|
||||
|
||||
return Query(outer);
|
||||
}
|
||||
|
||||
public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
||||
|
|
@ -62,14 +80,14 @@ public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
|||
return Query(s => s.AuthorMetadataId == authorMetadataId);
|
||||
}
|
||||
|
||||
public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
|
||||
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
|
||||
{
|
||||
return Query(a => a.AuthorMetadataId == authorMetadataId || foreignIds.Contains(a.ForeignBookId));
|
||||
}
|
||||
|
||||
public List<Book> GetBooksByFileIds(IEnumerable<int> fileIds)
|
||||
{
|
||||
return Query(new SqlBuilder()
|
||||
return Query(new SqlBuilder(_database.DatabaseType)
|
||||
.Join<Book, Edition>((b, e) => b.Id == e.BookId)
|
||||
.Join<Edition, BookFile>((l, r) => l.Id == r.EditionId)
|
||||
.Where<BookFile>(f => fileIds.Contains(f.Id)))
|
||||
|
|
@ -125,7 +143,7 @@ private string BuildQualityCutoffWhereClause(List<QualitiesBelowCutoff> qualitie
|
|||
{
|
||||
foreach (var belowCutoff in profile.QualityIds)
|
||||
{
|
||||
clauses.Add(string.Format("(Authors.[QualityProfileId] = {0} AND BookFiles.Quality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
|
||||
clauses.Add(string.Format("(\"Authors\".\"QualityProfileId\" = {0} AND \"BookFiles\".\"Quality\" LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +154,7 @@ public PagingSpec<Book> BooksWhereCutoffUnmet(PagingSpec<Book> pagingSpec, List<
|
|||
{
|
||||
pagingSpec.Records = GetPagedRecords(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff), pagingSpec, PagedQuery);
|
||||
|
||||
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM {TableMapping.Mapper.TableNameMapping(typeof(Book))} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/)";
|
||||
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Book))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
|
||||
pagingSpec.TotalRecords = GetPagedRecordCount(BooksWhereCutoffUnmetBuilder(qualitiesBelowCutoff).Select(typeof(Book)), pagingSpec, countTemplate);
|
||||
|
||||
return pagingSpec;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public interface IEditionRepository : IBasicRepository<Edition>
|
|||
List<Edition> FindByAuthor(int id);
|
||||
List<Edition> FindByAuthorMetadataId(int id, bool onlyMonitored);
|
||||
Edition FindByTitle(int authorMetadataId, string title);
|
||||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds);
|
||||
List<Edition> SetMonitored(Edition edition);
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ public Edition FindByForeignEditionId(string foreignEditionId)
|
|||
return edition;
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
|
||||
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds)
|
||||
{
|
||||
return Query(r => r.BookId == bookId || foreignEditionIds.Contains(r.ForeignEditionId));
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ public List<Edition> FindByBook(int id)
|
|||
{
|
||||
// populate the books and author metadata also
|
||||
// this hopefully speeds up the track matching a lot
|
||||
var builder = new SqlBuilder()
|
||||
var builder = new SqlBuilder(_database.DatabaseType)
|
||||
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.LeftJoin<Book, AuthorMetadata>((b, a) => b.AuthorMetadataId == a.Id)
|
||||
.Where<Edition>(r => r.BookId == id);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Books
|
|||
public interface ISeriesRepository : IBasicRepository<Series>
|
||||
{
|
||||
Series FindById(string foreignSeriesId);
|
||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
||||
List<Series> FindById(List<string> foreignSeriesId);
|
||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||
List<Series> GetByAuthorId(int authorId);
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public Series FindById(string foreignSeriesId)
|
|||
return Query(x => x.ForeignSeriesId == foreignSeriesId).SingleOrDefault();
|
||||
}
|
||||
|
||||
public List<Series> FindById(IEnumerable<string> foreignSeriesId)
|
||||
public List<Series> FindById(List<string> foreignSeriesId)
|
||||
{
|
||||
return Query(x => foreignSeriesId.Contains(x.ForeignSeriesId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public interface IBookService
|
|||
List<Book> GetNextBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetLastBooksByAuthorMetadataId(IEnumerable<int> authorMetadataIds);
|
||||
List<Book> GetBooksByAuthorMetadataId(int authorMetadataId);
|
||||
List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds);
|
||||
List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds);
|
||||
List<Book> GetBooksByFileIds(IEnumerable<int> fileIds);
|
||||
Book AddBook(Book newBook, bool doRefresh = true);
|
||||
Book FindById(string foreignId);
|
||||
|
|
@ -206,7 +206,7 @@ public List<Book> GetBooksByAuthorMetadataId(int authorMetadataId)
|
|||
return _bookRepository.GetBooksByAuthorMetadataId(authorMetadataId).ToList();
|
||||
}
|
||||
|
||||
public List<Book> GetBooksForRefresh(int authorMetadataId, IEnumerable<string> foreignIds)
|
||||
public List<Book> GetBooksForRefresh(int authorMetadataId, List<string> foreignIds)
|
||||
{
|
||||
return _bookRepository.GetBooksForRefresh(authorMetadataId, foreignIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public interface IEditionService
|
|||
void InsertMany(List<Edition> editions);
|
||||
void UpdateMany(List<Edition> editions);
|
||||
void DeleteMany(List<Edition> editions);
|
||||
List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds);
|
||||
List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds);
|
||||
List<Edition> GetEditionsByBook(int bookId);
|
||||
List<Edition> GetEditionsByAuthor(int authorId);
|
||||
Edition FindByTitle(int authorMetadataId, string title);
|
||||
|
|
@ -72,7 +72,7 @@ public void DeleteMany(List<Edition> editions)
|
|||
}
|
||||
}
|
||||
|
||||
public List<Edition> GetEditionsForRefresh(int bookId, IEnumerable<string> foreignEditionIds)
|
||||
public List<Edition> GetEditionsForRefresh(int bookId, List<string> foreignEditionIds)
|
||||
{
|
||||
return _editionRepository.GetEditionsForRefresh(bookId, foreignEditionIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ protected override List<Book> GetRemoteChildren(Author local, Author remote)
|
|||
protected override List<Book> GetLocalChildren(Author entity, List<Book> remoteChildren)
|
||||
{
|
||||
return _bookService.GetBooksForRefresh(entity.AuthorMetadataId,
|
||||
remoteChildren.Select(x => x.ForeignBookId));
|
||||
remoteChildren.Select(x => x.ForeignBookId).ToList());
|
||||
}
|
||||
|
||||
protected override Tuple<Book, List<Book>> GetMatchingExistingChildren(List<Book> existingChildren, Book remote)
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ protected override List<Edition> GetRemoteChildren(Book local, Book remote)
|
|||
|
||||
protected override List<Edition> GetLocalChildren(Book entity, List<Edition> remoteChildren)
|
||||
{
|
||||
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId));
|
||||
return _editionService.GetEditionsForRefresh(entity.Id, remoteChildren.Select(x => x.ForeignEditionId).ToList());
|
||||
}
|
||||
|
||||
protected override Tuple<Edition, List<Edition>> GetMatchingExistingChildren(List<Edition> existingChildren, Edition remote)
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ public bool RefreshSeriesInfo(int authorMetadataId, List<Series> remoteSeries, A
|
|||
var updated = false;
|
||||
|
||||
var existingByAuthor = _seriesService.GetByAuthorMetadataId(authorMetadataId);
|
||||
var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId));
|
||||
var existingBySeries = _seriesService.FindById(remoteSeries.Select(x => x.ForeignSeriesId).ToList());
|
||||
var existing = existingByAuthor.Concat(existingBySeries).GroupBy(x => x.ForeignSeriesId).Select(x => x.First()).ToList();
|
||||
|
||||
var books = _bookService.GetBooksByAuthorMetadataId(authorMetadataId);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace NzbDrone.Core.Books
|
|||
public interface ISeriesService
|
||||
{
|
||||
Series FindById(string foreignSeriesId);
|
||||
List<Series> FindById(IEnumerable<string> foreignSeriesId);
|
||||
List<Series> FindById(List<string> foreignSeriesId);
|
||||
List<Series> GetByAuthorMetadataId(int authorMetadataId);
|
||||
List<Series> GetByAuthorId(int authorId);
|
||||
void Delete(int seriesId);
|
||||
|
|
@ -27,7 +27,7 @@ public Series FindById(string foreignSeriesId)
|
|||
return _seriesRepository.FindById(foreignSeriesId);
|
||||
}
|
||||
|
||||
public List<Series> FindById(IEnumerable<string> foreignSeriesId)
|
||||
public List<Series> FindById(List<string> foreignSeriesId)
|
||||
{
|
||||
return _seriesRepository.FindById(foreignSeriesId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
|
@ -18,7 +20,7 @@
|
|||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||
IExecute<ResetApiKeyCommand>
|
||||
IExecute<ResetApiKeyCommand>
|
||||
{
|
||||
Dictionary<string, object> GetConfigDictionary();
|
||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||
|
|
@ -48,6 +50,13 @@ public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
|||
string SyslogServer { get; }
|
||||
int SyslogPort { get; }
|
||||
string SyslogLevel { get; }
|
||||
string PostgresHost { get; }
|
||||
int PostgresPort { get; }
|
||||
string PostgresUser { get; }
|
||||
string PostgresPassword { get; }
|
||||
string PostgresMainDb { get; }
|
||||
string PostgresLogDb { get; }
|
||||
string PostgresCacheDb { get; }
|
||||
}
|
||||
|
||||
public class ConfigFileProvider : IConfigFileProvider
|
||||
|
|
@ -57,6 +66,7 @@ public class ConfigFileProvider : IConfigFileProvider
|
|||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly ICached<string> _cache;
|
||||
private readonly PostgresOptions _postgresOptions;
|
||||
|
||||
private readonly string _configFile;
|
||||
|
||||
|
|
@ -65,12 +75,14 @@ public class ConfigFileProvider : IConfigFileProvider
|
|||
public ConfigFileProvider(IAppFolderInfo appFolderInfo,
|
||||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator,
|
||||
IDiskProvider diskProvider)
|
||||
IDiskProvider diskProvider,
|
||||
IOptions<PostgresOptions> postgresOptions)
|
||||
{
|
||||
_cache = cacheManager.GetCache<string>(GetType());
|
||||
_eventAggregator = eventAggregator;
|
||||
_diskProvider = diskProvider;
|
||||
_configFile = appFolderInfo.GetConfigPath();
|
||||
_postgresOptions = postgresOptions.Value;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfigDictionary()
|
||||
|
|
@ -184,6 +196,13 @@ public AuthenticationType AuthenticationMethod
|
|||
|
||||
public string LogLevel => GetValue("LogLevel", "info");
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, 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", "readarr-main", persist: false);
|
||||
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "readarr-log", persist: false);
|
||||
public string PostgresCacheDb => _postgresOptions?.CacheDb ?? GetValue("PostgresCacheDb", "readarr-cache", persist: false);
|
||||
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
|
||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public BasicRepository(IDatabase database, IEventAggregator eventAggregator)
|
|||
_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();
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ public int Count()
|
|||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM {_table}");
|
||||
return conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM \"{_table}\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +176,11 @@ private string GetInsertSql()
|
|||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +199,8 @@ private TModel Insert(IDbConnection connection, IDbTransaction transaction, TMod
|
|||
throw;
|
||||
}
|
||||
|
||||
var id = (int)multi.Read().First().id;
|
||||
var multiRead = multi.Read();
|
||||
var id = (int)(multiRead.First().id ?? multiRead.First().Id);
|
||||
_keyProperty.SetValue(model, id);
|
||||
|
||||
return model;
|
||||
|
|
@ -305,7 +311,7 @@ public void Purge(bool vacuum = false)
|
|||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
conn.Execute($"DELETE FROM [{_table}]");
|
||||
conn.Execute($"DELETE FROM \"{_table}\"");
|
||||
}
|
||||
|
||||
if (vacuum)
|
||||
|
|
@ -364,7 +370,7 @@ public void SetFields(IList<TModel> models, params Expression<Func<TModel, objec
|
|||
private string GetUpdateSql(List<PropertyInfo> propertiesToUpdate)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("UPDATE {0} SET ", _table);
|
||||
sb.AppendFormat("UPDATE \"{0}\" SET ", _table);
|
||||
|
||||
for (var i = 0; i < propertiesToUpdate.Count; i++)
|
||||
{
|
||||
|
|
@ -446,9 +452,10 @@ protected List<TModel> GetPagedRecords(SqlBuilder builder, PagingSpec<TModel> pa
|
|||
pagingSpec.SortKey = $"{_table}.{_keyProperty.Name}";
|
||||
}
|
||||
|
||||
var sortKey = TableMapping.Mapper.GetSortKey(pagingSpec.SortKey);
|
||||
var sortDirection = pagingSpec.SortDirection == SortDirection.Descending ? "DESC" : "ASC";
|
||||
var pagingOffset = (pagingSpec.Page - 1) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"{pagingSpec.SortKey} {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
var pagingOffset = Math.Max(pagingSpec.Page - 1, 0) * pagingSpec.PageSize;
|
||||
builder.OrderBy($"\"{sortKey}\" {sortDirection} LIMIT {pagingSpec.PageSize} OFFSET {pagingOffset}");
|
||||
|
||||
return queryFunc(builder).ToList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
|
|
@ -10,10 +10,12 @@ public interface ICacheDatabase : IDatabase
|
|||
public class CacheDatabase : ICacheDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public CacheDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
|
|
@ -25,6 +27,8 @@ public IDbConnection OpenConnection()
|
|||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Data.SQLite;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
|
|
@ -15,11 +17,20 @@ public interface IConnectionStringFactory
|
|||
|
||||
public class ConnectionStringFactory : IConnectionStringFactory
|
||||
{
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo)
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public ConnectionStringFactory(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
MainDbConnectionString = GetConnectionString(appFolderInfo.GetDatabase());
|
||||
LogDbConnectionString = GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
CacheDbConnectionString = GetConnectionString(appFolderInfo.GetCacheDatabase());
|
||||
_configFileProvider = configFileProvider;
|
||||
|
||||
MainDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresMainDb) :
|
||||
GetConnectionString(appFolderInfo.GetDatabase());
|
||||
|
||||
LogDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresLogDb) :
|
||||
GetConnectionString(appFolderInfo.GetLogDatabase());
|
||||
|
||||
CacheDbConnectionString = _configFileProvider.PostgresHost.IsNotNullOrWhiteSpace() ? GetPostgresConnectionString(_configFileProvider.PostgresCacheDb) :
|
||||
GetConnectionString(appFolderInfo.GetCacheDatabase());
|
||||
}
|
||||
|
||||
public string MainDbConnectionString { get; private set; }
|
||||
|
|
@ -51,5 +62,19 @@ private static string GetConnectionString(string dbPath)
|
|||
|
||||
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.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
|
@ -11,6 +12,7 @@ public interface IDatabase
|
|||
IDbConnection OpenConnection();
|
||||
Version Version { get; }
|
||||
int Migration { get; }
|
||||
DatabaseType DatabaseType { get; }
|
||||
void Vacuum();
|
||||
}
|
||||
|
||||
|
|
@ -32,13 +34,44 @@ public IDbConnection OpenConnection()
|
|||
return _datamapperFactory();
|
||||
}
|
||||
|
||||
public DatabaseType DatabaseType
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Version Version
|
||||
{
|
||||
get
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
var version = db.QueryFirstOrDefault<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);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +83,7 @@ public int Migration
|
|||
{
|
||||
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 @@ public void Vacuum()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DatabaseType
|
||||
{
|
||||
SQLite,
|
||||
PostgreSQL
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Data.SQLite;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
|
@ -92,10 +94,19 @@ public IDatabase Create(MigrationContext migrationContext)
|
|||
|
||||
var db = new Database(migrationContext.MigrationType.ToString(), () =>
|
||||
{
|
||||
var conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
conn.Open();
|
||||
DbConnection conn;
|
||||
|
||||
if (connectionString.Contains(".db"))
|
||||
{
|
||||
conn = SQLiteFactory.Instance.CreateConnection();
|
||||
conn.ConnectionString = connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn = new NpgsqlConnection(connectionString);
|
||||
}
|
||||
|
||||
conn.Open();
|
||||
return conn;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ public static class SqlBuilderExtensions
|
|||
|
||||
public static SqlBuilder Select(this SqlBuilder builder, params Type[] types)
|
||||
{
|
||||
return builder.Select(types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", "));
|
||||
return builder.Select(types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", "));
|
||||
}
|
||||
|
||||
public static SqlBuilder SelectDistinct(this SqlBuilder builder, params Type[] types)
|
||||
{
|
||||
return builder.Select("DISTINCT " + types.Select(x => TableMapping.Mapper.TableNameMapping(x) + ".*").Join(", "));
|
||||
return builder.Select("DISTINCT " + types.Select(x => $"\"{TableMapping.Mapper.TableNameMapping(x)}\".*").Join(", "));
|
||||
}
|
||||
|
||||
public static SqlBuilder SelectCount(this SqlBuilder builder)
|
||||
|
|
@ -42,41 +42,48 @@ public static SqlBuilder SelectCountDistinct<TModel>(this SqlBuilder builder, Ex
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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)
|
||||
{
|
||||
var wb = new WhereBuilder(filter, false, builder.Sequence);
|
||||
var wb = GetWhereBuilder(builder.DatabaseType, filter, false, builder.Sequence);
|
||||
|
||||
var rightTable = TableMapping.Mapper.TableNameMapping(typeof(TRight));
|
||||
|
||||
return builder.LeftJoin($"{rightTable} ON {wb.ToString()}");
|
||||
return builder.LeftJoin($"\"{rightTable}\" ON {wb.ToString()}");
|
||||
}
|
||||
|
||||
public static SqlBuilder GroupBy<TModel>(this SqlBuilder builder, Expression<Func<TModel, object>> property)
|
||||
{
|
||||
var table = TableMapping.Mapper.TableNameMapping(typeof(TModel));
|
||||
var propName = property.GetMemberName().Name;
|
||||
return builder.GroupBy($"{table}.{propName}");
|
||||
return builder.GroupBy($"\"{table}\".\"{propName}\"");
|
||||
}
|
||||
|
||||
public static SqlBuilder.Template AddSelectTemplate(this SqlBuilder builder, Type type)
|
||||
|
|
@ -138,6 +145,18 @@ public static string GetSqlLogString(string sql, object paramsObject)
|
|||
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)
|
||||
{
|
||||
var argsDictionary = new Dictionary<string, object>();
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ public interface ILogDatabase : IDatabase
|
|||
public class LogDatabase : ILogDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public LogDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
|
|
@ -25,6 +27,8 @@ public IDbConnection OpenConnection()
|
|||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ public interface IMainDatabase : IDatabase
|
|||
public class MainDatabase : IMainDatabase
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly DatabaseType _databaseType;
|
||||
|
||||
public MainDatabase(IDatabase database)
|
||||
{
|
||||
_database = database;
|
||||
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
|
||||
}
|
||||
|
||||
public IDbConnection OpenConnection()
|
||||
|
|
@ -25,6 +27,8 @@ public IDbConnection OpenConnection()
|
|||
|
||||
public int Migration => _database.Migration;
|
||||
|
||||
public DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public void Vacuum()
|
||||
{
|
||||
_database.Vacuum();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ protected override void MainDbUpgrade()
|
|||
.WithColumn("MetadataProfileId").AsInt32().WithDefaultValue(1)
|
||||
.WithColumn("AuthorMetadataId").AsInt32().Unique();
|
||||
|
||||
Create.TableForModel("Books")
|
||||
.WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0)
|
||||
.WithColumn("ForeignBookId").AsString().Indexed()
|
||||
.WithColumn("TitleSlug").AsString().Unique()
|
||||
.WithColumn("Title").AsString()
|
||||
.WithColumn("ReleaseDate").AsDateTime().Nullable()
|
||||
.WithColumn("Links").AsString().Nullable()
|
||||
.WithColumn("Genres").AsString().Nullable()
|
||||
.WithColumn("Ratings").AsString().Nullable()
|
||||
.WithColumn("CleanTitle").AsString().Indexed()
|
||||
.WithColumn("Monitored").AsBoolean()
|
||||
.WithColumn("AnyEditionOk").AsBoolean()
|
||||
.WithColumn("LastInfoSync").AsDateTime().Nullable()
|
||||
.WithColumn("Added").AsDateTime().Nullable()
|
||||
.WithColumn("AddOptions").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("Series")
|
||||
.WithColumn("ForeignSeriesId").AsString().Unique()
|
||||
.WithColumn("Title").AsString()
|
||||
|
|
@ -68,22 +84,6 @@ protected override void MainDbUpgrade()
|
|||
.WithColumn("Ratings").AsString().Nullable()
|
||||
.WithColumn("Aliases").AsString().WithDefaultValue("[]");
|
||||
|
||||
Create.TableForModel("Books")
|
||||
.WithColumn("AuthorMetadataId").AsInt32().WithDefaultValue(0)
|
||||
.WithColumn("ForeignBookId").AsString().Indexed()
|
||||
.WithColumn("TitleSlug").AsString().Unique()
|
||||
.WithColumn("Title").AsString()
|
||||
.WithColumn("ReleaseDate").AsDateTime().Nullable()
|
||||
.WithColumn("Links").AsString().Nullable()
|
||||
.WithColumn("Genres").AsString().Nullable()
|
||||
.WithColumn("Ratings").AsString().Nullable()
|
||||
.WithColumn("CleanTitle").AsString().Indexed()
|
||||
.WithColumn("Monitored").AsBoolean()
|
||||
.WithColumn("AnyEditionOk").AsBoolean()
|
||||
.WithColumn("LastInfoSync").AsDateTime().Nullable()
|
||||
.WithColumn("Added").AsDateTime().Nullable()
|
||||
.WithColumn("AddOptions").AsString().Nullable();
|
||||
|
||||
Create.TableForModel("Editions")
|
||||
.WithColumn("BookId").AsInt32().WithDefaultValue(0)
|
||||
.WithColumn("ForeignEditionId").AsString().Unique()
|
||||
|
|
@ -136,12 +136,12 @@ protected override void MainDbUpgrade()
|
|||
.WithColumn("OnUpgrade").AsBoolean().Nullable()
|
||||
.WithColumn("Tags").AsString().Nullable()
|
||||
.WithColumn("OnRename").AsBoolean().NotNullable()
|
||||
.WithColumn("OnReleaseImport").AsBoolean().WithDefaultValue(0)
|
||||
.WithColumn("OnHealthIssue").AsBoolean().WithDefaultValue(0)
|
||||
.WithColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(0)
|
||||
.WithColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(0)
|
||||
.WithColumn("OnImportFailure").AsBoolean().WithDefaultValue(0)
|
||||
.WithColumn("OnTrackRetag").AsBoolean().WithDefaultValue(0);
|
||||
.WithColumn("OnReleaseImport").AsBoolean().WithDefaultValue(false)
|
||||
.WithColumn("OnHealthIssue").AsBoolean().WithDefaultValue(false)
|
||||
.WithColumn("IncludeHealthWarnings").AsBoolean().WithDefaultValue(false)
|
||||
.WithColumn("OnDownloadFailure").AsBoolean().WithDefaultValue(false)
|
||||
.WithColumn("OnImportFailure").AsBoolean().WithDefaultValue(false)
|
||||
.WithColumn("OnTrackRetag").AsBoolean().WithDefaultValue(false);
|
||||
|
||||
Create.TableForModel("ScheduledTasks")
|
||||
.WithColumn("TypeName").AsString().Unique()
|
||||
|
|
@ -327,8 +327,8 @@ protected override void MainDbUpgrade()
|
|||
.WithColumn("Label").AsString().NotNullable()
|
||||
.WithColumn("Filters").AsString().NotNullable();
|
||||
|
||||
Create.Index().OnTable("Books").OnColumn("AuthorId");
|
||||
Create.Index().OnTable("Books").OnColumn("AuthorId").Ascending()
|
||||
IfDatabase("sqlite").Create.Index().OnTable("Books").OnColumn("AuthorId");
|
||||
IfDatabase("sqlite").Create.Index().OnTable("Books").OnColumn("AuthorId").Ascending()
|
||||
.OnColumn("ReleaseDate").Ascending();
|
||||
|
||||
Delete.Index().OnTable("History").OnColumn("BookId");
|
||||
|
|
@ -340,12 +340,15 @@ protected override void MainDbUpgrade()
|
|||
.OnColumn("Date").Descending();
|
||||
|
||||
Create.Index().OnTable("Authors").OnColumn("Monitored").Ascending();
|
||||
|
||||
Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending();
|
||||
Create.Index().OnTable("Books").OnColumn("AuthorMetadataId").Ascending()
|
||||
.OnColumn("ReleaseDate").Ascending();
|
||||
|
||||
Insert.IntoTable("DelayProfiles").Row(new
|
||||
{
|
||||
EnableUsenet = 1,
|
||||
EnableTorrent = 1,
|
||||
EnableUsenet = true,
|
||||
EnableTorrent = true,
|
||||
PreferredProtocol = 1,
|
||||
UsenetDelay = 0,
|
||||
TorrentDelay = 0,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public class ImportListSearch : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ImportLists").AddColumn("ShouldSearch").AsInt32().WithDefaultValue(1);
|
||||
Alter.Table("ImportLists").AddColumn("ShouldSearch").AsBoolean().WithDefaultValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public class remove_chown_and_folderchmod_config : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("DELETE FROM config WHERE Key IN ('folderchmod', 'chownuser')");
|
||||
Execute.Sql("DELETE FROM \"Config\" WHERE \"Key\" IN ('folderchmod', 'chownuser')");
|
||||
Execute.WithConnection(ConvertFileChmodToFolderChmod);
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tr
|
|||
using (IDbCommand getFileChmodCmd = conn.CreateCommand())
|
||||
{
|
||||
getFileChmodCmd.Transaction = tran;
|
||||
getFileChmodCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'filechmod'";
|
||||
getFileChmodCmd.CommandText = @"SELECT ""Value"" FROM ""Config"" WHERE ""Key"" = 'filechmod'";
|
||||
|
||||
var fileChmod = getFileChmodCmd.ExecuteScalar() as string;
|
||||
if (fileChmod != null)
|
||||
|
|
@ -35,7 +35,7 @@ private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tr
|
|||
using (IDbCommand insertCmd = conn.CreateCommand())
|
||||
{
|
||||
insertCmd.Transaction = tran;
|
||||
insertCmd.CommandText = "INSERT INTO Config (Key, Value) VALUES ('chmodfolder', ?)";
|
||||
insertCmd.CommandText = "INSERT INTO \"Config\" (\"Key\", \"Value\") VALUES ('chmodfolder', ?)";
|
||||
insertCmd.AddParameter(folderChmod);
|
||||
|
||||
insertCmd.ExecuteNonQuery();
|
||||
|
|
@ -45,7 +45,7 @@ private void ConvertFileChmodToFolderChmod(IDbConnection conn, IDbTransaction tr
|
|||
using (IDbCommand deleteCmd = conn.CreateCommand())
|
||||
{
|
||||
deleteCmd.Transaction = tran;
|
||||
deleteCmd.CommandText = "DELETE FROM Config WHERE Key = 'filechmod'";
|
||||
deleteCmd.CommandText = "DELETE FROM \"Config\" WHERE \"Key\" = 'filechmod'";
|
||||
|
||||
deleteCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public class update_notifiarr : NzbDroneMigrationBase
|
|||
{
|
||||
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';");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ protected override void MainDbUpgrade()
|
|||
|
||||
private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var rows = conn.Query<AuthorName>("SELECT AuthorMetadata.Id, AuthorMetadata.Name FROM AuthorMetadata", transaction: tran);
|
||||
var rows = conn.Query<AuthorName>("SELECT \"AuthorMetadata\".\"Id\", \"AuthorMetadata\".\"Name\" FROM \"AuthorMetadata\"", transaction: tran);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
row.SortName = row.Name.ToLastFirst().ToLower();
|
||||
}
|
||||
|
||||
var sql = "UPDATE AuthorMetadata SET SortName = @SortName WHERE Id = @Id";
|
||||
var sql = "UPDATE \"AuthorMetadata\" SET \"SortName\" = @SortName WHERE \"Id\" = @Id";
|
||||
conn.Execute(sql, rows, transaction: tran);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,13 +54,13 @@ public ProfileUpdater10(IDbConnection conn, IDbTransaction tran)
|
|||
_connection = conn;
|
||||
_transaction = tran;
|
||||
|
||||
_profiles = _connection.Query<Profile10>(@"SELECT Id, Name, Cutoff, Items FROM QualityProfiles",
|
||||
_profiles = _connection.Query<Profile10>(@"SELECT ""Id"", ""Name"", ""Cutoff"", ""Items"" FROM ""QualityProfiles""",
|
||||
transaction: _transaction).ToList();
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
{
|
||||
var sql = "UPDATE QualityProfiles SET Name = @Name, Cutoff = @Cutoff, Items = @Items WHERE Id = @Id";
|
||||
var sql = "UPDATE \"QualityProfiles\" SET \"Name\" = @Name, \"Cutoff\" = @Cutoff, \"Items\" = @Items WHERE \"Id\" = @Id";
|
||||
_connection.Execute(sql, _changedProfiles, transaction: _transaction);
|
||||
|
||||
_changedProfiles.Clear();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public class add_bookfile_part_naming_token : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("UPDATE NamingConfig SET StandardBookFormat = StandardBookFormat || '{ (PartNumber)}'");
|
||||
Execute.Sql("UPDATE \"NamingConfig\" SET \"StandardBookFormat\" = \"StandardBookFormat\" || '{ (PartNumber)}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ protected override void MainDbUpgrade()
|
|||
|
||||
private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var rows = conn.Query<AuthorName>("SELECT AuthorMetadata.Id, AuthorMetadata.Name FROM AuthorMetadata", transaction: tran);
|
||||
var rows = conn.Query<AuthorName>("SELECT \"AuthorMetadata\".\"Id\", \"AuthorMetadata\".\"Name\" FROM \"AuthorMetadata\"", transaction: tran);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
|
|
@ -30,7 +30,7 @@ private void MigrateAuthorSortName(IDbConnection conn, IDbTransaction tran)
|
|||
row.SortNameLastFirst = row.Name.ToLastFirst().ToLower();
|
||||
}
|
||||
|
||||
var sql = "UPDATE AuthorMetadata SET NameLastFirst = @NameLastFirst, SortName = @SortName, SortNameLastFirst = @SortNameLastFirst WHERE Id = @Id";
|
||||
var sql = "UPDATE \"AuthorMetadata\" SET \"NameLastFirst\" = @NameLastFirst, \"SortName\" = @SortName, \"SortNameLastFirst\" = @SortNameLastFirst WHERE \"Id\" = @Id";
|
||||
conn.Execute(sql, rows, transaction: tran);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ public class FixIndexes : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Delete.Index().OnTable("Books").OnColumn("AuthorId");
|
||||
Delete.Index().OnTable("Books").OnColumns("AuthorId", "ReleaseDate");
|
||||
IfDatabase("sqlite").Delete.Index().OnTable("Books").OnColumn("AuthorId");
|
||||
IfDatabase("sqlite").Delete.Index().OnTable("Books").OnColumns("AuthorId", "ReleaseDate");
|
||||
|
||||
Create.Index().OnTable("Editions").OnColumn("BookId");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public class ImportListMonitorExisting : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsInt32().WithDefaultValue(0);
|
||||
Alter.Table("ImportLists").AddColumn("ShouldMonitorExisting").AsBoolean().WithDefaultValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ protected override void MainDbUpgrade()
|
|||
Create.Index().OnTable("DownloadHistory").OnColumn("AuthorId");
|
||||
Create.Index().OnTable("DownloadHistory").OnColumn("DownloadId");
|
||||
|
||||
Execute.WithConnection(InitialImportedDownloadHistory);
|
||||
|
||||
Execute.Sql("DELETE From History where EventType = 8;");
|
||||
IfDatabase("sqlite").Execute.WithConnection(InitialImportedDownloadHistory);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<int, int> EventTypeMap = new Dictionary<int, int>()
|
||||
|
|
@ -56,7 +54,7 @@ private void InitialImportedDownloadHistory(IDbConnection conn, IDbTransaction t
|
|||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT AuthorId, DownloadId, EventType, SourceTitle, Date, Data FROM History WHERE DownloadId IS NOT NULL AND EventType IN (1, 8, 4, 10, 7) GROUP BY EventType, DownloadId";
|
||||
cmd.CommandText = "SELECT \"AuthorId\", \"DownloadId\", \"EventType\", \"SourceTitle\", \"Date\", \"Data\" FROM \"History\" WHERE \"DownloadId\" IS NOT NULL AND \"EventType\" IN (1, 8, 4, 10, 7) GROUP BY \"EventType\", \"DownloadId\"";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
|
|
@ -87,7 +85,15 @@ private void InitialImportedDownloadHistory(IDbConnection conn, IDbTransaction t
|
|||
using (var updateCmd = conn.CreateCommand())
|
||||
{
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = @"INSERT INTO DownloadHistory (EventType, AuthorId, DownloadId, SourceTitle, Date, Protocol, Data) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
if (conn.GetType().FullName == "Npgsql.NpgsqlConnection")
|
||||
{
|
||||
updateCmd.CommandText = @"INSERT INTO ""DownloadHistory"" (""EventType"", ""AuthorId"", ""DownloadId"", ""SourceTitle"", ""Date"", ""Protocol"", ""Data"") VALUES ($1, $2, $3, $4, $5, $6, $7)";
|
||||
}
|
||||
else
|
||||
{
|
||||
updateCmd.CommandText = @"INSERT INTO ""DownloadHistory"" (""EventType"", ""AuthorId"", ""DownloadId"", ""SourceTitle"", ""Date"", ""Protocol"", ""Data"") VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
}
|
||||
|
||||
updateCmd.AddParameter(downloadHistoryEventType);
|
||||
updateCmd.AddParameter(seriesId);
|
||||
updateCmd.AddParameter(downloadId);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ public class add_on_delete_to_notifications : NzbDroneMigrationBase
|
|||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Notifications").AddColumn("OnAuthorDelete").AsBoolean().WithDefaultValue(0);
|
||||
Alter.Table("Notifications").AddColumn("OnBookDelete").AsBoolean().WithDefaultValue(0);
|
||||
Alter.Table("Notifications").AddColumn("OnBookFileDelete").AsBoolean().WithDefaultValue(0);
|
||||
Alter.Table("Notifications").AddColumn("OnBookFileDeleteForUpgrade").AsBoolean().WithDefaultValue(0);
|
||||
Alter.Table("Notifications").AddColumn("OnAuthorDelete").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("Notifications").AddColumn("OnBookDelete").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("Notifications").AddColumn("OnBookFileDelete").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("Notifications").AddColumn("OnBookFileDeleteForUpgrade").AsBoolean().WithDefaultValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using FluentMigrator.Runner;
|
||||
using FluentMigrator.Runner.Generators;
|
||||
using FluentMigrator.Runner.Initialization;
|
||||
using FluentMigrator.Runner.Processors;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -34,11 +35,16 @@ public void Migrate(string connectionString, MigrationContext migrationContext)
|
|||
|
||||
_logger.Info("*** Migrating {0} ***", connectionString);
|
||||
|
||||
var serviceProvider = new ServiceCollection()
|
||||
ServiceProvider serviceProvider;
|
||||
|
||||
var db = connectionString.Contains(".db") ? "sqlite" : "postgres";
|
||||
|
||||
serviceProvider = new ServiceCollection()
|
||||
.AddLogging(b => b.AddNLog())
|
||||
.AddFluentMigratorCore()
|
||||
.ConfigureRunner(
|
||||
builder => builder
|
||||
.AddPostgres()
|
||||
.AddNzbDroneSQLite()
|
||||
.WithGlobalConnectionString(connectionString)
|
||||
.WithMigrationsIn(Assembly.GetExecutingAssembly()))
|
||||
|
|
@ -48,6 +54,14 @@ public void Migrate(string connectionString, MigrationContext migrationContext)
|
|||
opt.PreviewOnly = false;
|
||||
opt.Timeout = TimeSpan.FromSeconds(60);
|
||||
})
|
||||
.Configure<SelectingProcessorAccessorOptions>(cfg =>
|
||||
{
|
||||
cfg.ProcessorId = db;
|
||||
})
|
||||
.Configure<SelectingGeneratorAccessorOptions>(cfg =>
|
||||
{
|
||||
cfg.GeneratorId = db;
|
||||
})
|
||||
.BuildServiceProvider();
|
||||
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
|
|
|
|||
27
src/NzbDrone.Core/Datastore/PostgresOptions.cs
Normal file
27
src/NzbDrone.Core/Datastore/PostgresOptions.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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 string CacheDb { get; set; }
|
||||
|
||||
public static PostgresOptions GetOptions()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var postgresOptions = new PostgresOptions();
|
||||
config.GetSection("Readarr:Postgres").Bind(postgresOptions);
|
||||
|
||||
return postgresOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,17 @@ namespace NzbDrone.Core.Datastore
|
|||
public class SqlBuilder
|
||||
{
|
||||
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 DatabaseType DatabaseType => _databaseType;
|
||||
|
||||
public Template AddTemplate(string sql, dynamic parameters = null) =>
|
||||
new Template(this, sql, parameters);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,17 +50,17 @@ public string TableNameMapping(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)
|
||||
{
|
||||
return $"DELETE FROM {TableMap[x]} /**where**/";
|
||||
return $"DELETE FROM \"{TableMap[x]}\" /**where**/";
|
||||
}
|
||||
|
||||
public string PageCountTemplate(Type x)
|
||||
{
|
||||
return $"SELECT /**select**/ FROM {TableMap[x]} /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
|
||||
return $"SELECT /**select**/ FROM \"{TableMap[x]}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/";
|
||||
}
|
||||
|
||||
public bool IsValidSortKey(string sortKey)
|
||||
|
|
@ -91,6 +91,35 @@ public bool IsValidSortKey(string sortKey)
|
|||
|
||||
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
|
||||
|
|
@ -155,7 +184,7 @@ public ColumnMapper<T> HasOne<TChild>(Expression<Func<T, LazyLoaded<TChild>>> po
|
|||
(db, 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,15 +108,15 @@ public static void Map()
|
|||
.HasOne(a => a.Metadata, a => a.AuthorMetadataId)
|
||||
.HasOne(a => a.QualityProfile, a => a.QualityProfileId)
|
||||
.HasOne(s => s.MetadataProfile, s => s.MetadataProfileId)
|
||||
.LazyLoad(a => a.Books, (db, a) => db.Query<Book>(new SqlBuilder().Where<Book>(b => b.AuthorMetadataId == a.AuthorMetadataId)).ToList(), a => a.AuthorMetadataId > 0);
|
||||
.LazyLoad(a => a.Books, (db, a) => db.Query<Book>(new SqlBuilder(db.DatabaseType).Where<Book>(b => b.AuthorMetadataId == a.AuthorMetadataId)).ToList(), a => a.AuthorMetadataId > 0);
|
||||
|
||||
Mapper.Entity<Series>("Series").RegisterModel()
|
||||
.Ignore(s => s.ForeignAuthorId)
|
||||
.LazyLoad(s => s.LinkItems,
|
||||
(db, series) => db.Query<SeriesBookLink>(new SqlBuilder().Where<SeriesBookLink>(s => s.SeriesId == series.Id)).ToList(),
|
||||
(db, series) => db.Query<SeriesBookLink>(new SqlBuilder(db.DatabaseType).Where<SeriesBookLink>(s => s.SeriesId == series.Id)).ToList(),
|
||||
s => s.Id > 0)
|
||||
.LazyLoad(s => s.Books,
|
||||
(db, series) => db.Query<Book>(new SqlBuilder()
|
||||
(db, series) => db.Query<Book>(new SqlBuilder(db.DatabaseType)
|
||||
.Join<Book, SeriesBookLink>((l, r) => l.Id == r.BookId)
|
||||
.Join<SeriesBookLink, Series>((l, r) => l.SeriesId == r.Id)
|
||||
.Where<Series>(s => s.Id == series.Id)).ToList(),
|
||||
|
|
@ -132,27 +132,27 @@ public static void Map()
|
|||
.Ignore(x => x.AuthorId)
|
||||
.HasOne(r => r.AuthorMetadata, r => r.AuthorMetadataId)
|
||||
.LazyLoad(x => x.BookFiles,
|
||||
(db, book) => db.Query<BookFile>(new SqlBuilder()
|
||||
(db, book) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType)
|
||||
.Join<BookFile, Edition>((l, r) => l.EditionId == r.Id)
|
||||
.Where<Edition>(b => b.BookId == book.Id)).ToList(),
|
||||
b => b.Id > 0)
|
||||
.LazyLoad(x => x.Editions,
|
||||
(db, book) => db.Query<Edition>(new SqlBuilder().Where<Edition>(e => e.BookId == book.Id)).ToList(),
|
||||
(db, book) => db.Query<Edition>(new SqlBuilder(db.DatabaseType).Where<Edition>(e => e.BookId == book.Id)).ToList(),
|
||||
b => b.Id > 0)
|
||||
.LazyLoad(a => a.Author,
|
||||
(db, book) => AuthorRepository.Query(db,
|
||||
new SqlBuilder()
|
||||
new SqlBuilder(db.DatabaseType)
|
||||
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id)
|
||||
.Where<Author>(a => a.AuthorMetadataId == book.AuthorMetadataId)).SingleOrDefault(),
|
||||
a => a.AuthorMetadataId > 0)
|
||||
.LazyLoad(b => b.SeriesLinks,
|
||||
(db, book) => db.Query<SeriesBookLink>(new SqlBuilder().Where<SeriesBookLink>(s => s.BookId == book.Id)).ToList(),
|
||||
(db, book) => db.Query<SeriesBookLink>(new SqlBuilder(db.DatabaseType).Where<SeriesBookLink>(s => s.BookId == book.Id)).ToList(),
|
||||
b => b.Id > 0);
|
||||
|
||||
Mapper.Entity<Edition>("Editions").RegisterModel()
|
||||
.HasOne(r => r.Book, r => r.BookId)
|
||||
.LazyLoad(x => x.BookFiles,
|
||||
(db, edition) => db.Query<BookFile>(new SqlBuilder().Where<BookFile>(f => f.EditionId == edition.Id)).ToList(),
|
||||
(db, edition) => db.Query<BookFile>(new SqlBuilder(db.DatabaseType).Where<BookFile>(f => f.EditionId == edition.Id)).ToList(),
|
||||
b => b.Id > 0);
|
||||
|
||||
Mapper.Entity<BookFile>("BookFiles").RegisterModel()
|
||||
|
|
@ -160,7 +160,7 @@ public static void Map()
|
|||
.HasOne(f => f.Edition, f => f.EditionId)
|
||||
.LazyLoad(x => x.Author,
|
||||
(db, f) => AuthorRepository.Query(db,
|
||||
new SqlBuilder()
|
||||
new SqlBuilder(db.DatabaseType)
|
||||
.Join<Author, AuthorMetadata>((a, m) => a.AuthorMetadataId == m.Id)
|
||||
.Join<Author, Book>((l, r) => l.AuthorMetadataId == r.AuthorMetadataId)
|
||||
.Join<Book, Edition>((l, r) => l.Id == r.BookId)
|
||||
|
|
|
|||
|
|
@ -1,390 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Dapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
public class WhereBuilder : ExpressionVisitor
|
||||
public abstract class WhereBuilder : ExpressionVisitor
|
||||
{
|
||||
protected StringBuilder _sb;
|
||||
|
||||
private const DbType EnumerableMultiParameter = (DbType)(-1);
|
||||
private readonly string _paramNamePrefix;
|
||||
private readonly bool _requireConcreteValue = false;
|
||||
private int _paramCount = 0;
|
||||
private bool _gotConcreteValue = false;
|
||||
|
||||
public WhereBuilder(Expression filter, bool requireConcreteValue, int seq)
|
||||
{
|
||||
_paramNamePrefix = string.Format("Clause{0}", seq + 1);
|
||||
_requireConcreteValue = requireConcreteValue;
|
||||
_sb = new StringBuilder();
|
||||
|
||||
Parameters = new DynamicParameters();
|
||||
|
||||
if (filter != null)
|
||||
{
|
||||
Visit(filter);
|
||||
}
|
||||
}
|
||||
|
||||
public DynamicParameters Parameters { get; private set; }
|
||||
|
||||
private string AddParameter(object value, DbType? dbType = null)
|
||||
{
|
||||
_gotConcreteValue = true;
|
||||
_paramCount++;
|
||||
var name = _paramNamePrefix + "_P" + _paramCount;
|
||||
Parameters.Add(name, value, dbType);
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
protected override Expression VisitBinary(BinaryExpression expression)
|
||||
{
|
||||
_sb.Append("(");
|
||||
|
||||
Visit(expression.Left);
|
||||
|
||||
_sb.AppendFormat(" {0} ", Decode(expression));
|
||||
|
||||
Visit(expression.Right);
|
||||
|
||||
_sb.Append(")");
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMethodCall(MethodCallExpression expression)
|
||||
{
|
||||
var method = expression.Method.Name;
|
||||
|
||||
switch (expression.Method.Name)
|
||||
{
|
||||
case "Contains":
|
||||
ParseContainsExpression(expression);
|
||||
break;
|
||||
|
||||
case "StartsWith":
|
||||
ParseStartsWith(expression);
|
||||
break;
|
||||
|
||||
case "EndsWith":
|
||||
ParseEndsWith(expression);
|
||||
break;
|
||||
|
||||
default:
|
||||
var msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method);
|
||||
throw new NotImplementedException(msg);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
protected override Expression VisitMemberAccess(MemberExpression expression)
|
||||
{
|
||||
var tableName = expression?.Expression?.Type != null ? TableMapping.Mapper.TableNameMapping(expression.Expression.Type) : null;
|
||||
var gotValue = TryGetRightValue(expression, out var value);
|
||||
|
||||
// Only use the SQL condition if the expression didn't resolve to an actual value
|
||||
if (tableName != null && !gotValue)
|
||||
{
|
||||
_sb.Append($"\"{tableName}\".\"{expression.Member.Name}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
// string is IEnumerable<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>)))
|
||||
{
|
||||
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;
|
||||
}
|
||||
public DynamicParameters Parameters { get; protected set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
387
src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs
Normal file
387
src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
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)))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
387
src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs
Normal file
387
src/NzbDrone.Core/Datastore/WhereBuilderSqlite.cs
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
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)))
|
||||
{
|
||||
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 @@ public List<EntityHistory> GetByBook(int bookId, EntityHistoryEventType? eventTy
|
|||
|
||||
public List<EntityHistory> FindDownloadHistory(int idAuthorId, QualityModel quality)
|
||||
{
|
||||
var allowed = new[] { EntityHistoryEventType.Grabbed, EntityHistoryEventType.DownloadFailed, EntityHistoryEventType.BookFileImported };
|
||||
var allowed = new[] { (int)EntityHistoryEventType.Grabbed, (int)EntityHistoryEventType.DownloadFailed, (int)EntityHistoryEventType.BookFileImported };
|
||||
|
||||
return Query(h => h.AuthorId == idAuthorId &&
|
||||
h.Quality == quality &&
|
||||
allowed.Contains(h.EventType));
|
||||
allowed.Contains((int)h.EventType));
|
||||
}
|
||||
|
||||
public void DeleteForAuthor(int authorId)
|
||||
|
|
@ -102,7 +102,7 @@ public void DeleteForAuthor(int authorId)
|
|||
Delete(c => c.AuthorId == authorId);
|
||||
}
|
||||
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder()
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
|
||||
.Join<Author, AuthorMetadata>((l, r) => l.AuthorMetadataId == r.Id)
|
||||
.Join<EntityHistory, Book>((h, a) => h.BookId == a.Id);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
|
@ -16,16 +16,32 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE RelativePath
|
||||
LIKE '_:\%'
|
||||
OR RelativePath
|
||||
LIKE '\%'
|
||||
OR RelativePath
|
||||
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" = ANY (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""RelativePath""
|
||||
LIKE '_:\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '\%'
|
||||
OR ""RelativePath""
|
||||
LIKE '/%'
|
||||
)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM NamingConfig
|
||||
WHERE ID NOT IN (
|
||||
SELECT ID FROM NamingConfig
|
||||
mapper.Execute(@"DELETE FROM ""NamingConfig""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""NamingConfig""
|
||||
LIMIT 1)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM Users
|
||||
WHERE ID NOT IN (
|
||||
SELECT ID FROM Users
|
||||
mapper.Execute(@"DELETE FROM ""Users""
|
||||
WHERE ""Id"" NOT IN (
|
||||
SELECT ""Id"" FROM ""Users""
|
||||
LIMIT 1)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,29 @@ public CleanupDownloadClientUnavailablePendingReleases(IMainDatabase database)
|
|||
|
||||
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 IN @Reasons",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""Reason"" = ANY (@Reasons)",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Added"" < @TwoWeeksAgo
|
||||
AND ""REASON"" IN @Reasons",
|
||||
new
|
||||
{
|
||||
TwoWeeksAgo = DateTime.UtcNow.AddDays(-14),
|
||||
Reasons = new[] { (int)PendingReleaseReason.DownloadClientUnavailable, (int)PendingReleaseReason.Fallback }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ private void DeleteDuplicateAuthorMetadata()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE Type = 1
|
||||
GROUP BY AuthorId, Consumer
|
||||
HAVING COUNT(AuthorId) > 1
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" = 1
|
||||
GROUP BY ""AuthorId"", ""Consumer""
|
||||
HAVING COUNT(""AuthorId"") > 1
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
|
@ -37,12 +37,12 @@ private void DeleteDuplicateBookMetadata()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE Type IN (2, 4)
|
||||
GROUP BY BookId, Consumer
|
||||
HAVING COUNT(BookId) > 1
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (2, 4)
|
||||
GROUP BY ""BookId"", ""Consumer""
|
||||
HAVING COUNT(""BookId"") > 1
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
|
@ -51,12 +51,12 @@ private void DeleteDuplicateBookFileMetadata()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE Type IN (2, 4)
|
||||
GROUP BY BookFileId, Consumer
|
||||
HAVING COUNT(BookFileId) > 1
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT MIN(""Id"") FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (2, 4)
|
||||
GROUP BY ""BookFileId"", ""Consumer""
|
||||
HAVING COUNT(""BookFileId"") > 1
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM AuthorMetadata
|
||||
WHERE Id IN (
|
||||
SELECT AuthorMetadata.Id FROM AuthorMetadata
|
||||
LEFT OUTER JOIN Books ON Books.AuthorMetadataId = AuthorMetadata.Id
|
||||
LEFT OUTER JOIN Authors ON Authors.AuthorMetadataId = AuthorMetadata.Id
|
||||
WHERE Books.Id IS NULL AND Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""AuthorMetadata""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""AuthorMetadata"".""Id"" FROM ""AuthorMetadata""
|
||||
LEFT OUTER JOIN ""Books"" ON ""Books"".""AuthorMetadataId"" = ""AuthorMetadata"".""Id""
|
||||
LEFT OUTER JOIN ""Authors"" ON ""Authors"".""AuthorMetadataId"" = ""AuthorMetadata"".""Id""
|
||||
WHERE ""Books"".""Id"" IS NULL AND ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM Blocklist
|
||||
WHERE Id IN (
|
||||
SELECT Blocklist.Id FROM Blocklist
|
||||
LEFT OUTER JOIN Authors
|
||||
ON Blocklist.AuthorId = Authors.Id
|
||||
WHERE Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""Blocklist""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Blocklist"".""Id"" FROM ""Blocklist""
|
||||
LEFT OUTER JOIN ""Authors""
|
||||
ON ""Blocklist"".""AuthorId"" = ""Authors"".""Id""
|
||||
WHERE ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ public void Clean()
|
|||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
// Unlink where track no longer exists
|
||||
mapper.Execute(@"UPDATE BookFiles
|
||||
SET EditionId = 0
|
||||
WHERE Id IN (
|
||||
SELECT BookFiles.Id FROM BookFiles
|
||||
LEFT OUTER JOIN Editions
|
||||
ON BookFiles.EditionId = Editions.Id
|
||||
WHERE Editions.Id IS NULL)");
|
||||
mapper.Execute(@"UPDATE ""BookFiles""
|
||||
SET ""EditionId"" = 0
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""BookFiles"".""Id"" FROM ""BookFiles""
|
||||
LEFT OUTER JOIN ""Editions""
|
||||
ON ""BookFiles"".""EditionId"" = ""Editions"".""Id""
|
||||
WHERE ""Editions"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM Books
|
||||
WHERE Id IN (
|
||||
SELECT Books.Id FROM Books
|
||||
LEFT OUTER JOIN Authors
|
||||
ON Books.AuthorMetadataId = Authors.AuthorMetadataId
|
||||
WHERE Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""Books""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Books"".""Id"" FROM ""Books""
|
||||
LEFT OUTER JOIN ""Authors""
|
||||
ON ""Books"".""AuthorMetadataId"" = ""Authors"".""AuthorMetadataId""
|
||||
WHERE ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@ public CleanupOrphanedDownloadClientStatus(IMainDatabase database)
|
|||
|
||||
public void Clean()
|
||||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM DownloadClientStatus
|
||||
WHERE Id IN (
|
||||
SELECT DownloadClientStatus.Id FROM DownloadClientStatus
|
||||
LEFT OUTER JOIN DownloadClients
|
||||
ON DownloadClientStatus.ProviderId = DownloadClients.Id
|
||||
WHERE DownloadClients.Id IS NULL)");
|
||||
}
|
||||
var mapper = _database.OpenConnection();
|
||||
|
||||
mapper.Execute(@"DELETE FROM ""DownloadClientStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""DownloadClientStatus"".""Id"" FROM ""DownloadClientStatus""
|
||||
LEFT OUTER JOIN ""DownloadClients""
|
||||
ON ""DownloadClientStatus"".""ProviderId"" = ""DownloadClients"".""Id""
|
||||
WHERE ""DownloadClients"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM Editions
|
||||
WHERE Id IN (
|
||||
SELECT Editions.Id FROM Editions
|
||||
LEFT OUTER JOIN Books
|
||||
ON Editions.BookId = Books.Id
|
||||
WHERE Books.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""Editions""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Editions"".""Id"" FROM ""Editions""
|
||||
LEFT OUTER JOIN ""Books""
|
||||
ON ""Editions"".""BookId"" = ""Books"".""Id""
|
||||
WHERE ""Books"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Dapper;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
|
@ -22,12 +22,12 @@ private void CleanupOrphanedByAuthor()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM History
|
||||
WHERE Id IN (
|
||||
SELECT History.Id FROM History
|
||||
LEFT OUTER JOIN Authors
|
||||
ON History.AuthorId = Authors.Id
|
||||
WHERE Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Authors""
|
||||
ON ""History"".""AuthorId"" = ""Authors"".""Id""
|
||||
WHERE ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,12 +35,12 @@ private void CleanupOrphanedByBook()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM History
|
||||
WHERE Id IN (
|
||||
SELECT History.Id FROM History
|
||||
LEFT OUTER JOIN Books
|
||||
ON History.BookId = Books.Id
|
||||
WHERE Books.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""History""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""History"".""Id"" FROM ""History""
|
||||
LEFT OUTER JOIN ""Books""
|
||||
ON ""History"".""BookId"" = ""Books"".""Id""
|
||||
WHERE ""Books"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM ImportListStatus
|
||||
WHERE Id IN (
|
||||
SELECT ImportListStatus.Id FROM ImportListStatus
|
||||
LEFT OUTER JOIN ImportLists
|
||||
ON ImportListStatus.ProviderId = ImportLists.Id
|
||||
WHERE ImportLists.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""ImportListStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""ImportListStatus"".""Id"" FROM ""ImportListStatus""
|
||||
LEFT OUTER JOIN ""ImportLists""
|
||||
ON ""ImportListStatus"".""ProviderId"" = ""ImportLists"".""Id""
|
||||
WHERE ""ImportLists"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM IndexerStatus
|
||||
WHERE Id IN (
|
||||
SELECT IndexerStatus.Id FROM IndexerStatus
|
||||
LEFT OUTER JOIN Indexers
|
||||
ON IndexerStatus.ProviderId = Indexers.Id
|
||||
WHERE Indexers.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""IndexerStatus""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""IndexerStatus"".""Id"" FROM ""IndexerStatus""
|
||||
LEFT OUTER JOIN ""Indexers""
|
||||
ON ""IndexerStatus"".""ProviderId"" = ""Indexers"".""Id""
|
||||
WHERE ""Indexers"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ private void DeleteOrphanedByAuthor()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT MetadataFiles.Id FROM MetadataFiles
|
||||
LEFT OUTER JOIN Authors
|
||||
ON MetadataFiles.AuthorId = Authors.Id
|
||||
WHERE Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""Authors""
|
||||
ON ""MetadataFiles"".""AuthorId"" = ""Authors"".""Id""
|
||||
WHERE ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +38,13 @@ private void DeleteOrphanedByBook()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT MetadataFiles.Id FROM MetadataFiles
|
||||
LEFT OUTER JOIN Books
|
||||
ON MetadataFiles.BookId = Books.Id
|
||||
WHERE MetadataFiles.BookId > 0
|
||||
AND Books.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""Books""
|
||||
ON ""MetadataFiles"".""BookId"" = ""Books"".""Id""
|
||||
WHERE ""MetadataFiles"".""BookId"" > 0
|
||||
AND ""Books"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,13 +52,13 @@ private void DeleteOrphanedByTrackFile()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT MetadataFiles.Id FROM MetadataFiles
|
||||
LEFT OUTER JOIN BookFiles
|
||||
ON MetadataFiles.BookFileId = BookFiles.Id
|
||||
WHERE MetadataFiles.BookFileId > 0
|
||||
AND BookFiles.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""MetadataFiles"".""Id"" FROM ""MetadataFiles""
|
||||
LEFT OUTER JOIN ""BookFiles""
|
||||
ON ""MetadataFiles"".""BookFileId"" = ""BookFiles"".""Id""
|
||||
WHERE ""MetadataFiles"".""BookFileId"" > 0
|
||||
AND ""BookFiles"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,11 +66,11 @@ private void DeleteWhereBookIdIsZero()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE Type IN (2, 4)
|
||||
AND BookId = 0)");
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (2, 4)
|
||||
AND ""BookId"" = 0)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +78,11 @@ private void DeleteWhereTrackFileIsZero()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM MetadataFiles
|
||||
WHERE Id IN (
|
||||
SELECT Id FROM MetadataFiles
|
||||
WHERE Type IN (2, 4)
|
||||
AND BookFileId = 0)");
|
||||
mapper.Execute(@"DELETE FROM ""MetadataFiles""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""Id"" FROM ""MetadataFiles""
|
||||
WHERE ""Type"" IN (2, 4)
|
||||
AND ""BookFileId"" = 0)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM PendingReleases
|
||||
WHERE Id IN (
|
||||
SELECT PendingReleases.Id FROM PendingReleases
|
||||
LEFT OUTER JOIN Authors
|
||||
ON PendingReleases.AuthorId = Authors.Id
|
||||
WHERE Authors.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""PendingReleases""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""PendingReleases"".""Id"" FROM ""PendingReleases""
|
||||
LEFT OUTER JOIN ""Authors""
|
||||
ON ""PendingReleases"".""AuthorId"" = ""Authors"".""Id""
|
||||
WHERE ""Authors"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,19 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM SeriesBookLink
|
||||
WHERE Id IN (
|
||||
SELECT SeriesBookLink.Id FROM SeriesBookLink
|
||||
LEFT OUTER JOIN Books
|
||||
ON SeriesBookLink.BookId = Books.Id
|
||||
WHERE Books.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""SeriesBookLink""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SeriesBookLink"".""Id"" FROM ""SeriesBookLink""
|
||||
LEFT OUTER JOIN ""Books""
|
||||
ON ""SeriesBookLink"".""BookId"" = ""Books"".""Id""
|
||||
WHERE ""Books"".""Id"" IS NULL)");
|
||||
|
||||
mapper.Execute(@"DELETE FROM SeriesBookLink
|
||||
WHERE Id IN (
|
||||
SELECT SeriesBookLink.Id FROM SeriesBookLink
|
||||
LEFT OUTER JOIN Series
|
||||
ON SeriesBookLink.SeriesId = Series.Id
|
||||
WHERE Series.Id IS NULL)");
|
||||
mapper.Execute(@"DELETE FROM ""SeriesBookLink""
|
||||
WHERE ""Id"" IN (
|
||||
SELECT ""SeriesBookLink"".""Id"" FROM ""SeriesBookLink""
|
||||
LEFT OUTER JOIN ""Series""
|
||||
ON ""SeriesBookLink"".""SeriesId"" = ""Series"".""Id""
|
||||
WHERE ""Series"".""Id"" IS NULL)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Data;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
|
@ -24,15 +25,29 @@ public void Clean()
|
|||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
var usedTagsList = string.Join(",", usedTags.Select(d => d.ToString()).ToArray());
|
||||
if (usedTags.Any())
|
||||
{
|
||||
var usedTagsList = usedTags.Select(d => d.ToString()).Join(",");
|
||||
|
||||
mapper.Execute($"DELETE FROM Tags WHERE NOT Id IN ({usedTagsList})");
|
||||
if (_database.DatabaseType == DatabaseType.PostgreSQL)
|
||||
{
|
||||
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" = ANY (\'{{{usedTagsList}}}\'::int[])");
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute($"DELETE FROM \"Tags\" WHERE NOT \"Id\" IN ({usedTagsList})");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mapper.Execute("DELETE FROM \"Tags\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] GetUsedTags(string table, IDbConnection mapper)
|
||||
{
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT Tags FROM {table} WHERE NOT Tags = '[]'")
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
|
||||
.SelectMany(x => x)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ public void Clean()
|
|||
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"UPDATE ScheduledTasks
|
||||
SET LastExecution = @time
|
||||
WHERE LastExecution > @time",
|
||||
mapper.Execute(@"UPDATE ""ScheduledTasks""
|
||||
SET ""LastExecution"" = @time
|
||||
WHERE ""LastExecution"" > @time",
|
||||
new { time = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public void Clean()
|
|||
{
|
||||
using (var mapper = _database.OpenConnection())
|
||||
{
|
||||
mapper.Execute(@"DELETE FROM HttpResponse WHERE Expiry < date('now')");
|
||||
mapper.Execute(@"DELETE FROM ""HttpResponse"" WHERE ""Expiry"" < date('now')");
|
||||
}
|
||||
|
||||
_database.Vacuum();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Data;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using NLog;
|
||||
using NLog.Common;
|
||||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Npgsql;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
|
|
@ -13,7 +15,7 @@ namespace NzbDrone.Core.Instrumentation
|
|||
{
|
||||
public class DatabaseTarget : TargetWithLayout, IHandle<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)";
|
||||
|
||||
private readonly IConnectionStringFactory _connectionStringFactory;
|
||||
|
|
@ -55,7 +57,6 @@ protected override void Write(LogEventInfo logEvent)
|
|||
{
|
||||
try
|
||||
{
|
||||
using var connection = new SQLiteConnection(_connectionStringFactory.LogDbConnectionString).OpenAndReturn();
|
||||
var log = new Log();
|
||||
log.Time = logEvent.TimeStamp;
|
||||
log.Message = CleanseLogMessage.Cleanse(logEvent.FormattedMessage);
|
||||
|
|
@ -84,16 +85,17 @@ protected override void Write(LogEventInfo logEvent)
|
|||
|
||||
log.Level = logEvent.Level.Name;
|
||||
|
||||
var sqlCommand = new SQLiteCommand(INSERT_COMMAND, connection);
|
||||
var connectionString = _connectionStringFactory.LogDbConnectionString;
|
||||
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level });
|
||||
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
//TODO: Probably need more robust way to differentiate what's being used
|
||||
if (connectionString.Contains(".db"))
|
||||
{
|
||||
WriteSqliteLog(log, connectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
WritePostgresLog(log, connectionString);
|
||||
}
|
||||
}
|
||||
catch (SQLiteException ex)
|
||||
{
|
||||
|
|
@ -102,6 +104,48 @@ protected override void Write(LogEventInfo logEvent)
|
|||
}
|
||||
}
|
||||
|
||||
private void WritePostgresLog(Log log, string connectionString)
|
||||
{
|
||||
using (var connection =
|
||||
new NpgsqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (var sqlCommand = connection.CreateCommand())
|
||||
{
|
||||
sqlCommand.CommandText = INSERT_COMMAND;
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Exception", DbType.String) { Value = log.Exception == null ? DBNull.Value : log.Exception });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("ExceptionType", DbType.String) { Value = log.ExceptionType == null ? DBNull.Value : log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new NpgsqlParameter("Level", DbType.String) { Value = log.Level });
|
||||
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSqliteLog(Log log, string connectionString)
|
||||
{
|
||||
using (var connection =
|
||||
SQLiteFactory.Instance.CreateConnection())
|
||||
{
|
||||
connection.ConnectionString = connectionString;
|
||||
connection.Open();
|
||||
using (var sqlCommand = connection.CreateCommand())
|
||||
{
|
||||
sqlCommand.CommandText = INSERT_COMMAND;
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Message", DbType.String) { Value = log.Message });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Time", DbType.DateTime) { Value = log.Time.ToUniversalTime() });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Logger", DbType.String) { Value = log.Logger });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Exception", DbType.String) { Value = log.Exception });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("ExceptionType", DbType.String) { Value = log.ExceptionType });
|
||||
sqlCommand.Parameters.Add(new SQLiteParameter("Level", DbType.String) { Value = log.Level });
|
||||
sqlCommand.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ApplicationShutdownRequested message)
|
||||
{
|
||||
if (LogManager.Configuration != null && LogManager.Configuration.LoggingRules.Contains(Rule))
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@
|
|||
"CutoffHelpText": "Once this quality is reached Readarr will no longer download books",
|
||||
"CutoffUnmet": "Cutoff Unmet",
|
||||
"DataAllBooks": "Monitor all books",
|
||||
"Database": "Database",
|
||||
"DataExistingBooks": "Monitor books that have files or have not released yet",
|
||||
"DataFirstBook": "Monitor the first book. All other books will be ignored",
|
||||
"DataFuturebooks": "Monitor books that have not released yet",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public MediaFileRepository(IMainDatabase database, IEventAggregator eventAggrega
|
|||
|
||||
// always join with all the other good stuff
|
||||
// needed more often than not so better to load it all now
|
||||
protected override SqlBuilder Builder() => new SqlBuilder()
|
||||
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||
.LeftJoin<BookFile, Edition>((b, e) => b.EditionId == e.Id)
|
||||
.LeftJoin<Edition, Book>((e, b) => e.BookId == b.Id)
|
||||
.LeftJoin<Book, Author>((book, author) => book.AuthorMetadataId == author.AuthorMetadataId)
|
||||
|
|
@ -86,7 +86,7 @@ public List<BookFile> GetFilesByEdition(int editionId)
|
|||
|
||||
public List<BookFile> GetUnmappedFiles()
|
||||
{
|
||||
return _database.Query<BookFile>(new SqlBuilder().Select(typeof(BookFile))
|
||||
return _database.Query<BookFile>(new SqlBuilder(_database.DatabaseType).Select(typeof(BookFile))
|
||||
.Where<BookFile>(t => t.EditionId == 0)).ToList();
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ public List<BookFile> GetFilesWithBasePath(string path)
|
|||
{
|
||||
// ensure path ends with a single trailing path separator to avoid matching partial paths
|
||||
var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
return _database.Query<BookFile>(new SqlBuilder().Where<BookFile>(x => x.Path.StartsWith(safePath))).ToList();
|
||||
return _database.Query<BookFile>(new SqlBuilder(_database.DatabaseType).Where<BookFile>(x => x.Path.StartsWith(safePath))).ToList();
|
||||
}
|
||||
|
||||
public BookFile GetFileWithPath(string path)
|
||||
|
|
@ -118,7 +118,7 @@ public BookFile GetFileWithPath(string path)
|
|||
public List<BookFile> GetFileWithPath(List<string> paths)
|
||||
{
|
||||
// use more limited join for speed
|
||||
var builder = new SqlBuilder()
|
||||
var builder = new SqlBuilder(_database.DatabaseType)
|
||||
.LeftJoin<BookFile, Edition>((f, t) => f.EditionId == t.Id);
|
||||
|
||||
var all = _database.QueryJoined<BookFile, Edition>(builder, (file, book) => MapTrack(file, book)).ToList();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public void Trim()
|
|||
|
||||
public void OrphanStarted()
|
||||
{
|
||||
var sql = @"UPDATE Commands SET Status = @Orphaned, EndedAt = @Ended WHERE Status = @Started";
|
||||
var sql = @"UPDATE ""Commands"" SET ""Status"" = @Orphaned, ""EndedAt"" = @Ended WHERE ""Status"" = @Started";
|
||||
var args = new
|
||||
{
|
||||
Orphaned = (int)CommandStatus.Orphaned,
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="FluentMigrator.Runner" />
|
||||
<PackageReference Include="FluentMigrator.Runner.SQLite" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Postgres" />
|
||||
<PackageReference Include="FluentValidation" />
|
||||
<PackageReference Include="MailKit" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="NLog" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
<PackageReference Include="RestSharp" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="TagLibSharp-Lidarr" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Analytics;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Update
|
||||
{
|
||||
|
|
@ -20,13 +21,15 @@ public class UpdatePackageProvider : IUpdatePackageProvider
|
|||
private readonly IHttpRequestBuilderFactory _requestBuilder;
|
||||
private readonly IPlatformInfo _platformInfo;
|
||||
private readonly IAnalyticsService _analyticsService;
|
||||
private readonly IMainDatabase _mainDatabase;
|
||||
|
||||
public UpdatePackageProvider(IHttpClient httpClient, IReadarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo)
|
||||
public UpdatePackageProvider(IHttpClient httpClient, IReadarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo, IMainDatabase mainDatabase)
|
||||
{
|
||||
_platformInfo = platformInfo;
|
||||
_analyticsService = analyticsService;
|
||||
_requestBuilder = requestBuilder.Services;
|
||||
_httpClient = httpClient;
|
||||
_mainDatabase = mainDatabase;
|
||||
}
|
||||
|
||||
public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
|
||||
|
|
@ -38,6 +41,7 @@ public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
|
|||
.AddQueryParam("arch", RuntimeInformation.OSArchitecture)
|
||||
.AddQueryParam("runtime", PlatformInfo.Platform.ToString().ToLowerInvariant())
|
||||
.AddQueryParam("runtimeVer", _platformInfo.Version)
|
||||
.AddQueryParam("dbType", _mainDatabase.DatabaseType)
|
||||
.SetSegment("branch", branch);
|
||||
|
||||
if (_analyticsService.IsEnabled)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@
|
|||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Composition.Extensions;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
|
|
@ -43,6 +45,7 @@ public void SetUp()
|
|||
// set up a dummy broadcaster and lifetime to allow tests to resolve
|
||||
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
|
||||
container.RegisterInstance<IBroadcastSignalRMessage>(new Mock<IBroadcastSignalRMessage>().Object);
|
||||
container.RegisterInstance<IOptions<PostgresOptions>>(new Mock<IOptions<PostgresOptions>>().Object);
|
||||
|
||||
_container = container.GetServiceProvider();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
using System.Text;
|
||||
using DryIoc;
|
||||
using DryIoc.Microsoft.DependencyInjection;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -26,6 +25,8 @@
|
|||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Host;
|
||||
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
|
||||
|
||||
namespace NzbDrone.Host
|
||||
{
|
||||
|
|
@ -141,6 +142,10 @@ public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContex
|
|||
.AddStartupContext(context)
|
||||
.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartingEvent());
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.Configure<PostgresOptions>(config.GetSection("Readarr:Postgres"));
|
||||
})
|
||||
.ConfigureWebHost(builder =>
|
||||
{
|
||||
builder.UseConfiguration(config);
|
||||
|
|
@ -213,6 +218,7 @@ private static IConfiguration GetConfiguration(StartupContext context)
|
|||
return new ConfigurationBuilder()
|
||||
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
|
||||
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
using NzbDrone.Core.Indexers.Newznab;
|
||||
using NzbDrone.Test.Common;
|
||||
using NzbDrone.Test.Common.Datastore;
|
||||
using Readarr.Http.ClientSchema;
|
||||
|
||||
namespace NzbDrone.Integration.Test
|
||||
|
|
@ -19,6 +25,8 @@ public abstract class IntegrationTest : IntegrationTestBase
|
|||
|
||||
protected int Port { get; private set; }
|
||||
|
||||
protected PostgresOptions PostgresOptions { get; set; } = new ();
|
||||
|
||||
protected override string RootUrl => $"http://localhost:{Port}/";
|
||||
|
||||
protected override string ApiKey => _runner.ApiKey;
|
||||
|
|
@ -27,7 +35,14 @@ protected override void StartTestTarget()
|
|||
{
|
||||
Port = Interlocked.Increment(ref StaticPort);
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), Port);
|
||||
PostgresOptions = PostgresDatabase.GetTestOptions();
|
||||
|
||||
if (PostgresOptions?.Host != null)
|
||||
{
|
||||
CreatePostgresDb(PostgresOptions);
|
||||
}
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), PostgresOptions, Port);
|
||||
_runner.Kill();
|
||||
|
||||
_runner.Start();
|
||||
|
|
@ -59,6 +74,22 @@ protected override void InitializeTestTarget()
|
|||
protected override void StopTestTarget()
|
||||
{
|
||||
_runner.Kill();
|
||||
if (PostgresOptions?.Host != null)
|
||||
{
|
||||
DropPostgresDb(PostgresOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreatePostgresDb(PostgresOptions options)
|
||||
{
|
||||
PostgresDatabase.Create(options, MigrationType.Main);
|
||||
PostgresDatabase.Create(options, MigrationType.Log);
|
||||
}
|
||||
|
||||
private static void DropPostgresDb(PostgresOptions options)
|
||||
{
|
||||
PostgresDatabase.Drop(options, MigrationType.Main);
|
||||
PostgresDatabase.Drop(options, MigrationType.Log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs
Normal file
69
src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using Npgsql;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Test.Common.Datastore
|
||||
{
|
||||
public static class PostgresDatabase
|
||||
{
|
||||
public static PostgresOptions GetTestOptions()
|
||||
{
|
||||
var options = PostgresOptions.GetOptions();
|
||||
|
||||
var uid = TestBase.GetUID();
|
||||
options.MainDb = uid + "_main";
|
||||
options.LogDb = uid + "_log";
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static void Create(PostgresOptions options, MigrationType migrationType)
|
||||
{
|
||||
var db = GetDatabaseName(options, migrationType);
|
||||
var connectionString = GetConnectionString(options);
|
||||
using var conn = new NpgsqlConnection(connectionString);
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $"CREATE DATABASE \"{db}\" WITH OWNER = {options.User} ENCODING = 'UTF8' CONNECTION LIMIT = -1;";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public static void Drop(PostgresOptions options, MigrationType migrationType)
|
||||
{
|
||||
var db = GetDatabaseName(options, migrationType);
|
||||
var connectionString = GetConnectionString(options);
|
||||
using var conn = new NpgsqlConnection(connectionString);
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $"DROP DATABASE \"{db}\" WITH (FORCE);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static string GetConnectionString(PostgresOptions options)
|
||||
{
|
||||
var builder = new NpgsqlConnectionStringBuilder()
|
||||
{
|
||||
Host = options.Host,
|
||||
Port = options.Port,
|
||||
Username = options.User,
|
||||
Password = options.Password,
|
||||
Enlist = false
|
||||
};
|
||||
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
|
||||
private static string GetDatabaseName(PostgresOptions options, MigrationType migrationType)
|
||||
{
|
||||
return migrationType switch
|
||||
{
|
||||
MigrationType.Main => options.MainDb,
|
||||
MigrationType.Log => options.LogDb,
|
||||
_ => throw new NotImplementedException("Unknown migration type")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs
Normal file
14
src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Test.Common.Datastore
|
||||
{
|
||||
public static class SqliteDatabase
|
||||
{
|
||||
public static string GetCachedDb(MigrationType type)
|
||||
{
|
||||
return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
|
@ -9,7 +10,9 @@
|
|||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Processes;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using RestSharp;
|
||||
|
||||
namespace NzbDrone.Test.Common
|
||||
|
|
@ -23,13 +26,15 @@ public class NzbDroneRunner
|
|||
|
||||
public string AppData { get; private set; }
|
||||
public string ApiKey { get; private set; }
|
||||
public PostgresOptions PostgresOptions { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
|
||||
public NzbDroneRunner(Logger logger, int port = 8787)
|
||||
public NzbDroneRunner(Logger logger, PostgresOptions postgresOptions, int port = 8787)
|
||||
{
|
||||
_processProvider = new ProcessProvider(logger);
|
||||
_restClient = new RestClient($"http://localhost:{port}/api/v1");
|
||||
|
||||
PostgresOptions = postgresOptions;
|
||||
Port = port;
|
||||
}
|
||||
|
||||
|
|
@ -142,10 +147,24 @@ public void KillAll()
|
|||
|
||||
private void Start(string outputNzbdroneConsoleExe)
|
||||
{
|
||||
StringDictionary envVars = new ();
|
||||
if (PostgresOptions?.Host != null)
|
||||
{
|
||||
envVars.Add("Readarr__Postgres__Host", PostgresOptions.Host);
|
||||
envVars.Add("Readarr__Postgres__Port", PostgresOptions.Port.ToString());
|
||||
envVars.Add("Readarr__Postgres__User", PostgresOptions.User);
|
||||
envVars.Add("Readarr__Postgres__Password", PostgresOptions.Password);
|
||||
envVars.Add("Readarr__Postgres__MainDb", PostgresOptions.MainDb);
|
||||
envVars.Add("Readarr__Postgres__LogDb", PostgresOptions.LogDb);
|
||||
envVars.Add("Readarr__Postgres__CacheDb", PostgresOptions.CacheDb);
|
||||
|
||||
TestContext.Progress.WriteLine("Using env vars:\n{0}", envVars.ToJson());
|
||||
}
|
||||
|
||||
TestContext.Progress.WriteLine("Starting instance from {0} on port {1}", outputNzbdroneConsoleExe, Port);
|
||||
|
||||
var args = "-nobrowser -nosingleinstancecheck -data=\"" + AppData + "\"";
|
||||
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, null, OnOutputDataReceived, OnOutputDataReceived);
|
||||
_nzbDroneProcess = _processProvider.Start(outputNzbdroneConsoleExe, args, envVars, OnOutputDataReceived, OnOutputDataReceived);
|
||||
}
|
||||
|
||||
private void OnOutputDataReceived(string data)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ public object GetStatus()
|
|||
Mode = _runtimeInfo.Mode,
|
||||
Branch = _configFileProvider.Branch,
|
||||
Authentication = _configFileProvider.AuthenticationMethod,
|
||||
SqliteVersion = _database.Version,
|
||||
DatabaseType = _database.DatabaseType,
|
||||
DatabaseVersion = _database.Version,
|
||||
MigrationVersion = _database.Migration,
|
||||
UrlBase = _configFileProvider.UrlBase,
|
||||
RuntimeVersion = _platformInfo.Version,
|
||||
|
|
|
|||
11
src/postgres.runsettings
Normal file
11
src/postgres.runsettings
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<RunConfiguration>
|
||||
<EnvironmentVariables>
|
||||
<Readarr__Postgres__Host>192.168.100.5</Readarr__Postgres__Host>
|
||||
<Readarr__Postgres__Port>5432</Readarr__Postgres__Port>
|
||||
<Readarr__Postgres__User>abc</Readarr__Postgres__User>
|
||||
<Readarr__Postgres__Password>abc</Readarr__Postgres__Password>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
</RunSettings>
|
||||
Loading…
Reference in a new issue