Fixed: All migrations are now transactional and will rollback if failed
This commit is contained in:
parent
2be35dfc37
commit
b9623957fd
|
@ -1,142 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using FizzWare.NBuilder;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
|
||||||
using NzbDrone.Core.Tv;
|
|
||||||
using System.Linq;
|
|
||||||
using NzbDrone.Test.Common;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class AlterFixture : DbTest
|
|
||||||
{
|
|
||||||
private SqLiteMigrationHelper _subject;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void SetUp()
|
|
||||||
{
|
|
||||||
_subject = Mocker.Resolve<SqLiteMigrationHelper>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_parse_existing_columns()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("Series");
|
|
||||||
|
|
||||||
columns.Should().NotBeEmpty();
|
|
||||||
|
|
||||||
columns.Values.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Name));
|
|
||||||
columns.Values.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Schema));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_create_table_from_column_list()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("Series");
|
|
||||||
columns.Remove("Title");
|
|
||||||
|
|
||||||
_subject.CreateTable("Series_New", columns.Values, new List<SQLiteIndex>());
|
|
||||||
|
|
||||||
var newColumns = _subject.GetColumns("Series_New");
|
|
||||||
|
|
||||||
newColumns.Values.Should().HaveSameCount(columns.Values);
|
|
||||||
newColumns.Should().NotContainKey("Title");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_able_to_transfer_empty_tables()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("Series");
|
|
||||||
var indexes = _subject.GetIndexes("Series");
|
|
||||||
columns.Remove("Title");
|
|
||||||
|
|
||||||
_subject.CreateTable("Series_New", columns.Values, indexes);
|
|
||||||
|
|
||||||
|
|
||||||
_subject.CopyData("Series", "Series_New", columns.Values);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_transfer_table_with_data()
|
|
||||||
{
|
|
||||||
var originalEpisodes = Builder<Episode>.CreateListOfSize(10).BuildListOfNew();
|
|
||||||
|
|
||||||
Mocker.Resolve<EpisodeRepository>().InsertMany(originalEpisodes);
|
|
||||||
|
|
||||||
var columns = _subject.GetColumns("Episodes");
|
|
||||||
var indexes = _subject.GetIndexes("Episodes");
|
|
||||||
|
|
||||||
columns.Remove("Title");
|
|
||||||
|
|
||||||
_subject.CreateTable("Episodes_New", columns.Values, indexes);
|
|
||||||
|
|
||||||
_subject.CopyData("Episodes", "Episodes_New", columns.Values);
|
|
||||||
|
|
||||||
_subject.GetRowCount("Episodes_New").Should().Be(originalEpisodes.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_read_existing_indexes()
|
|
||||||
{
|
|
||||||
var indexes = _subject.GetIndexes("QualityDefinitions");
|
|
||||||
|
|
||||||
indexes.Should().NotBeEmpty();
|
|
||||||
|
|
||||||
indexes.Should().OnlyContain(c => c != null);
|
|
||||||
indexes.Should().OnlyContain(c => !string.IsNullOrWhiteSpace(c.Column));
|
|
||||||
indexes.Should().OnlyContain(c => c.Table == "QualityDefinitions");
|
|
||||||
indexes.Should().OnlyContain(c => c.Unique);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_add_indexes_when_creating_new_table()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("QualityDefinitions");
|
|
||||||
var indexes = _subject.GetIndexes("QualityDefinitions");
|
|
||||||
|
|
||||||
_subject.CreateTable("QualityDefinitionsB", columns.Values, indexes);
|
|
||||||
|
|
||||||
var newIndexes = _subject.GetIndexes("QualityDefinitionsB");
|
|
||||||
|
|
||||||
newIndexes.Should().HaveSameCount(indexes);
|
|
||||||
newIndexes.Select(c=>c.Column).Should().BeEquivalentTo(indexes.Select(c=>c.Column));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_be_able_to_create_table_with_new_indexes()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("Series");
|
|
||||||
columns.Remove("Title");
|
|
||||||
|
|
||||||
_subject.CreateTable("Series_New", columns.Values, new List<SQLiteIndex>{new SQLiteIndex{Column = "AirTime", Table = "Series_New", Unique = true}});
|
|
||||||
|
|
||||||
var newColumns = _subject.GetColumns("Series_New");
|
|
||||||
var newIndexes = _subject.GetIndexes("Series_New");
|
|
||||||
|
|
||||||
newColumns.Values.Should().HaveSameCount(columns.Values);
|
|
||||||
newIndexes.Should().Contain(i=>i.Column == "AirTime");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void should_create_indexes_with_the_same_uniqueness()
|
|
||||||
{
|
|
||||||
var columns = _subject.GetColumns("Series");
|
|
||||||
var indexes = _subject.GetIndexes("Series");
|
|
||||||
|
|
||||||
var tempIndexes = indexes.JsonClone();
|
|
||||||
|
|
||||||
tempIndexes[0].Unique = false;
|
|
||||||
tempIndexes[1].Unique = true;
|
|
||||||
|
|
||||||
_subject.CreateTable("Series_New", columns.Values, tempIndexes);
|
|
||||||
var newIndexes = _subject.GetIndexes("Series_New");
|
|
||||||
|
|
||||||
newIndexes.Should().HaveSameCount(tempIndexes);
|
|
||||||
newIndexes.ShouldAllBeEquivalentTo(tempIndexes, options => options.Excluding(o => o.IndexName).Excluding(o => o.Table));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using System.Linq;
|
|
||||||
using FizzWare.NBuilder;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
|
||||||
using NzbDrone.Core.Tv;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Datastore.SQLiteMigrationHelperTests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class DuplicateFixture : DbTest
|
|
||||||
{
|
|
||||||
private SqLiteMigrationHelper _subject;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void SetUp()
|
|
||||||
{
|
|
||||||
_subject = Mocker.Resolve<SqLiteMigrationHelper>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void get_duplicates()
|
|
||||||
{
|
|
||||||
var series = Builder<Series>.CreateListOfSize(10)
|
|
||||||
.Random(3)
|
|
||||||
.With(c => c.ProfileId = 100)
|
|
||||||
.BuildListOfNew();
|
|
||||||
|
|
||||||
Db.InsertMany(series);
|
|
||||||
|
|
||||||
var duplicates = _subject.GetDuplicates<int>("series", "ProfileId").ToList();
|
|
||||||
|
|
||||||
|
|
||||||
duplicates.Should().HaveCount(1);
|
|
||||||
duplicates.First().Should().HaveCount(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.SqliteSchemaDumperTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SqliteSchemaDumperFixture
|
||||||
|
{
|
||||||
|
public SqliteSchemaDumper Subject { get; private set; }
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
Subject = new SqliteSchemaDumper(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(@"CREATE TABLE TestTable (MyId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE TABLE ""TestTable"" (""MyId"" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE TABLE [TestTable] ([MyId] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE TABLE `TestTable` (`MyId` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE TABLE ""Test """"Table"" (""My""""Id"" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test \"Table", "My\"Id")]
|
||||||
|
[TestCase(@"CREATE TABLE [Test Table] ([My Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test Table", "My Id")]
|
||||||
|
[TestCase(@" CREATE TABLE `Test ``Table` ( `My`` Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) ", "Test `Table", "My` Id")]
|
||||||
|
public void should_parse_table_language_flavors(String sql, String tableName, String columnName)
|
||||||
|
{
|
||||||
|
var result = Subject.ReadTableSchema(sql);
|
||||||
|
|
||||||
|
result.Name.Should().Be(tableName);
|
||||||
|
result.Columns.Count.Should().Be(1);
|
||||||
|
result.Columns.First().Name.Should().Be(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(@"CREATE INDEX TestIndex ON TestTable (MyId)", "TestIndex", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE INDEX ""TestIndex"" ON ""TestTable"" (""MyId"" ASC)", "TestIndex", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE INDEX [TestIndex] ON ""TestTable"" ([MyId] DESC)", "TestIndex", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE INDEX `TestIndex` ON `TestTable` (`MyId` COLLATE abc ASC)", "TestIndex", "TestTable", "MyId")]
|
||||||
|
[TestCase(@"CREATE INDEX ""Test """"Index"" ON ""TestTable"" (""My""""Id"" ASC)", "Test \"Index", "TestTable", "My\"Id")]
|
||||||
|
[TestCase(@"CREATE INDEX [Test Index] ON [TestTable] ([My Id]) ", "Test Index", "TestTable", "My Id")]
|
||||||
|
[TestCase(@" CREATE INDEX `Test ``Index` ON ""TestTable"" ( `My`` Id` ASC) ", "Test `Index", "TestTable", "My` Id")]
|
||||||
|
public void should_parse_index_language_flavors(String sql, String indexName, String tableName, String columnName)
|
||||||
|
{
|
||||||
|
var result = Subject.ReadIndexSchema(sql);
|
||||||
|
|
||||||
|
result.Name.Should().Be(indexName);
|
||||||
|
result.TableName.Should().Be(tableName);
|
||||||
|
result.Columns.Count.Should().Be(1);
|
||||||
|
result.Columns.First().Name.Should().Be(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(@"CREATE TABLE TestTable (MyId)")]
|
||||||
|
[TestCase(@"CREATE TABLE TestTable (MyId NOT NULL PRIMARY KEY AUTOINCREMENT)")]
|
||||||
|
[TestCase("CREATE TABLE TestTable\r\n(\t`MyId`\t NOT NULL PRIMARY KEY AUTOINCREMENT\n)")]
|
||||||
|
public void should_parse_column_attributes(String sql)
|
||||||
|
{
|
||||||
|
var result = Subject.ReadTableSchema(sql);
|
||||||
|
|
||||||
|
result.Name.Should().Be("TestTable");
|
||||||
|
result.Columns.Count.Should().Be(1);
|
||||||
|
result.Columns.First().Name.Should().Be("MyId");
|
||||||
|
result.Columns.First().Type.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_ignore_unknown_symbols()
|
||||||
|
{
|
||||||
|
var result = Subject.ReadTableSchema("CREATE TABLE TestTable (MyId INTEGER DEFAULT 10 CHECK (Some weir +1e3 expression), CONSTRAINT NULL, MyCol INTEGER)");
|
||||||
|
|
||||||
|
result.Name.Should().Be("TestTable");
|
||||||
|
result.Columns.Count.Should().Be(2);
|
||||||
|
result.Columns.First().Name.Should().Be("MyId");
|
||||||
|
result.Columns.First().Type.Should().Be(DbType.Int64);
|
||||||
|
result.Columns.Last().Name.Should().Be("MyCol");
|
||||||
|
result.Columns.Last().Type.Should().Be(DbType.Int64);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,8 +92,6 @@ namespace NzbDrone.Core.Test.Framework
|
||||||
|
|
||||||
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
|
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
|
||||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||||
Mocker.SetConstant<ISqLiteMigrationHelper>(Mocker.Resolve<SqLiteMigrationHelper>());
|
|
||||||
Mocker.SetConstant<ISQLiteAlter>(Mocker.Resolve<SQLiteAlter>());
|
|
||||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||||
|
|
||||||
MapRepository.Instance.EnableTraceLogging = true;
|
MapRepository.Instance.EnableTraceLogging = true;
|
||||||
|
|
|
@ -117,8 +117,7 @@
|
||||||
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
|
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
|
||||||
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
|
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
|
||||||
<Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" />
|
<Compile Include="Datastore\ReflectionStrategyFixture\Benchmarks.cs" />
|
||||||
<Compile Include="Datastore\SQLiteMigrationHelperTests\AlterFixture.cs" />
|
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
|
||||||
<Compile Include="Datastore\SQLiteMigrationHelperTests\DuplicateFixture.cs" />
|
|
||||||
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
||||||
|
|
|
@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
Execute.Sql("DROP INDEX IX_Series_TvRageId;");
|
Delete.Index().OnTable("Series").OnColumn("TvRageId");
|
||||||
Execute.Sql("DROP INDEX IX_Series_ImdbId;");
|
Delete.Index().OnTable("Series").OnColumn("ImdbId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Series", new[] { "BacklogSetting" });
|
Delete.Column("BacklogSetting").FromTable("Series");
|
||||||
SqLiteAlter.DropColumns("NamingConfig", new[] { "UseSceneName" });
|
Delete.Column("UseSceneName").FromTable("NamingConfig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("NamingConfig", new[] { "SeasonFolderFormat" });
|
Delete.Column("SeasonFolderFormat").FromTable("NamingConfig");
|
||||||
|
|
||||||
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 1 WHERE RenameEpisodes = -1");
|
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 1 WHERE RenameEpisodes = -1");
|
||||||
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 0 WHERE RenameEpisodes = -2");
|
Execute.Sql("UPDATE NamingConfig SET RenameEpisodes = 0 WHERE RenameEpisodes = -2");
|
||||||
|
|
|
@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Episodes", new[] { "Ignored" });
|
Delete.Column("Ignored").FromTable("Seasons");
|
||||||
SqLiteAlter.DropColumns("Seasons", new[] { "Ignored" });
|
Delete.Column("Ignored").FromTable("Episodes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Series", new[] { "CustomStartDate" });
|
Delete.Column("CustomStartDate").FromTable("Series");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Episodes", new []{ "AirDate" });
|
Delete.Column("AirDate").FromTable("Episodes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Data;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
|
@ -9,52 +12,88 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
using (var transaction = MigrationHelper.BeginTransaction())
|
Execute.WithConnection(RemoveDuplicates);
|
||||||
{
|
}
|
||||||
RemoveDuplicateSeries<int>("TvdbId");
|
|
||||||
RemoveDuplicateSeries<string>("TitleSlug");
|
|
||||||
|
|
||||||
var duplicatedEpisodes = MigrationHelper.GetDuplicates<int>("Episodes", "TvDbEpisodeId");
|
private void RemoveDuplicates(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
RemoveDuplicateSeries<int>(conn, tran, "TvdbId");
|
||||||
|
RemoveDuplicateSeries<string>(conn, tran, "TitleSlug");
|
||||||
|
|
||||||
|
var duplicatedEpisodes = GetDuplicates<int>(conn, tran, "Episodes", "TvDbEpisodeId");
|
||||||
|
|
||||||
foreach (var duplicate in duplicatedEpisodes)
|
foreach (var duplicate in duplicatedEpisodes)
|
||||||
{
|
{
|
||||||
foreach (var episodeId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
foreach (var episodeId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
||||||
{
|
{
|
||||||
RemoveEpisodeRows(episodeId);
|
RemoveEpisodeRows(conn, tran, episodeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
private IEnumerable<IGrouping<T, KeyValuePair<int, T>>> GetDuplicates<T>(IDbConnection conn, IDbTransaction tran, string tableName, string columnName)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveDuplicateSeries<T>(string field)
|
|
||||||
{
|
{
|
||||||
var duplicatedSeries = MigrationHelper.GetDuplicates<T>("Series", field);
|
var getDuplicates = conn.CreateCommand();
|
||||||
|
getDuplicates.Transaction = tran;
|
||||||
|
getDuplicates.CommandText = string.Format("select id, {0} from {1}", columnName, tableName);
|
||||||
|
|
||||||
|
var result = new List<KeyValuePair<int, T>>();
|
||||||
|
|
||||||
|
using (var reader = getDuplicates.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
result.Add(new KeyValuePair<int, T>(reader.GetInt32(0), (T)Convert.ChangeType(reader[1], typeof(T))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.GroupBy(c => c.Value).Where(g => g.Count() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDuplicateSeries<T>(IDbConnection conn, IDbTransaction tran, string field)
|
||||||
|
{
|
||||||
|
var duplicatedSeries = GetDuplicates<T>(conn, tran, "Series", field);
|
||||||
|
|
||||||
foreach (var duplicate in duplicatedSeries)
|
foreach (var duplicate in duplicatedSeries)
|
||||||
{
|
{
|
||||||
foreach (var seriesId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
foreach (var seriesId in duplicate.OrderBy(c => c.Key).Skip(1).Select(c => c.Key))
|
||||||
{
|
{
|
||||||
RemoveSeriesRows(seriesId);
|
RemoveSeriesRows(conn, tran, seriesId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSeriesRows(int seriesId)
|
private void RemoveSeriesRows(IDbConnection conn, IDbTransaction tran, int seriesId)
|
||||||
{
|
{
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Series WHERE Id = {0}", seriesId.ToString());
|
var deleteCmd = conn.CreateCommand();
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Episodes WHERE SeriesId = {0}", seriesId.ToString());
|
deleteCmd.Transaction = tran;
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Seasons WHERE SeriesId = {0}", seriesId.ToString());
|
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM History WHERE SeriesId = {0}", seriesId.ToString());
|
deleteCmd.CommandText = String.Format("DELETE FROM Series WHERE Id = {0}", seriesId.ToString());
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM EpisodeFiles WHERE SeriesId = {0}", seriesId.ToString());
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM Episodes WHERE SeriesId = {0}", seriesId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM Seasons WHERE SeriesId = {0}", seriesId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM History WHERE SeriesId = {0}", seriesId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM EpisodeFiles WHERE SeriesId = {0}", seriesId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveEpisodeRows(int episodeId)
|
private void RemoveEpisodeRows(IDbConnection conn, IDbTransaction tran, int episodeId)
|
||||||
{
|
{
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM Episodes WHERE Id = {0}", episodeId.ToString());
|
var deleteCmd = conn.CreateCommand();
|
||||||
MigrationHelper.ExecuteNonQuery("DELETE FROM History WHERE EpisodeId = {0}", episodeId.ToString());
|
deleteCmd.Transaction = tran;
|
||||||
}
|
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM Episodes WHERE Id = {0}", episodeId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
deleteCmd.CommandText = String.Format("DELETE FROM History WHERE EpisodeId = {0}", episodeId.ToString());
|
||||||
|
deleteCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,14 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.AddIndexes("Series",
|
// During an earlier version of drone, the indexes weren't recreated during alter table.
|
||||||
new SQLiteIndex { Column = "TvdbId", Table = "Series", Unique = true },
|
Execute.Sql("DROP INDEX IF EXISTS \"IX_Series_TvdbId\"");
|
||||||
new SQLiteIndex { Column = "TitleSlug", Table = "Series", Unique = true });
|
Execute.Sql("DROP INDEX IF EXISTS \"IX_Series_TitleSlug\"");
|
||||||
|
Execute.Sql("DROP INDEX IF EXISTS \"IX_Episodes_TvDbEpisodeId\"");
|
||||||
|
|
||||||
SqLiteAlter.AddIndexes("Episodes",
|
Create.Index().OnTable("Series").OnColumn("TvdbId").Unique();
|
||||||
new SQLiteIndex { Column = "TvDbEpisodeId", Table = "Episodes", Unique = true });
|
Create.Index().OnTable("Series").OnColumn("TitleSlug").Unique();
|
||||||
|
Create.Index().OnTable("Episodes").OnColumn("TvDbEpisodeId").Unique();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Episodes", new[] { "TvDbEpisodeId" });
|
Delete.Column("TvDbEpisodeId").FromTable("Episodes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,13 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("NamingConfig",
|
Delete.Column("Separator")
|
||||||
new[]
|
.Column("NumberStyle")
|
||||||
{
|
.Column("IncludeSeriesTitle")
|
||||||
"Separator",
|
.Column("IncludeEpisodeTitle")
|
||||||
"NumberStyle",
|
.Column("IncludeQuality")
|
||||||
"IncludeSeriesTitle",
|
.Column("ReplaceSpaces")
|
||||||
"IncludeEpisodeTitle",
|
.FromTable("NamingConfig");
|
||||||
"IncludeQuality",
|
|
||||||
"ReplaceSpaces"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using FluentMigrator;
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
@ -8,7 +9,9 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.Nullify("Series", new[] { "ImdbId", "TitleSlug" });
|
Alter.Table("Series")
|
||||||
|
.AlterColumn("ImdbId").AsString().Nullable()
|
||||||
|
.AlterColumn("TitleSlug").AsString().Nullable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("QualityProfiles", new[] { "Allowed" });
|
Delete.Column("Allowed").FromTable("QualityProfiles");
|
||||||
|
|
||||||
Alter.Column("Items").OnTable("QualityProfiles").AsString().NotNullable();
|
Alter.Column("Items").OnTable("QualityProfiles").AsString().NotNullable();
|
||||||
|
|
||||||
Create.TableForModel("QualityDefinitions")
|
Create.TableForModel("QualityDefinitions")
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Series", new[] { "QualityProfileId" });
|
Delete.Column("QualityProfileId").FromTable("Series");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("EpisodeFiles", new [] { "Path" });
|
Delete.Column("Path").FromTable("EpisodeFiles");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ namespace NzbDrone.Core.Datastore.Migration
|
||||||
{
|
{
|
||||||
protected override void MainDbUpgrade()
|
protected override void MainDbUpgrade()
|
||||||
{
|
{
|
||||||
SqLiteAlter.DropColumns("Indexers", new[] { "Enable" });
|
Delete.Column("Enable").FromTable("Indexers");
|
||||||
SqLiteAlter.DropColumns("DownloadClients", new[] { "Protocol" });
|
Delete.Column("Protocol").FromTable("DownloadClients");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,5 @@
|
||||||
public class MigrationContext
|
public class MigrationContext
|
||||||
{
|
{
|
||||||
public MigrationType MigrationType { get; set; }
|
public MigrationType MigrationType { get; set; }
|
||||||
public ISQLiteAlter SQLiteAlter { get; set; }
|
|
||||||
public ISqLiteMigrationHelper MigrationHelper { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,14 +13,10 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
{
|
{
|
||||||
private readonly IAnnouncer _announcer;
|
private readonly IAnnouncer _announcer;
|
||||||
private readonly ISQLiteAlter _sqLiteAlter;
|
|
||||||
private readonly ISqLiteMigrationHelper _migrationHelper;
|
|
||||||
|
|
||||||
public MigrationController(IAnnouncer announcer, ISQLiteAlter sqLiteAlter, ISqLiteMigrationHelper migrationHelper)
|
public MigrationController(IAnnouncer announcer)
|
||||||
{
|
{
|
||||||
_announcer = announcer;
|
_announcer = announcer;
|
||||||
_sqLiteAlter = sqLiteAlter;
|
|
||||||
_migrationHelper = migrationHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MigrateToLatest(string connectionString, MigrationType migrationType)
|
public void MigrateToLatest(string connectionString, MigrationType migrationType)
|
||||||
|
@ -34,14 +30,12 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
Namespace = "NzbDrone.Core.Datastore.Migration",
|
Namespace = "NzbDrone.Core.Datastore.Migration",
|
||||||
ApplicationContext = new MigrationContext
|
ApplicationContext = new MigrationContext
|
||||||
{
|
{
|
||||||
MigrationType = migrationType,
|
MigrationType = migrationType
|
||||||
SQLiteAlter = _sqLiteAlter,
|
|
||||||
MigrationHelper = _migrationHelper,
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
|
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
|
||||||
var factory = new SqliteProcessorFactory();
|
var factory = new NzbDroneSqliteProcessorFactory();
|
||||||
var processor = factory.Create(connectionString, _announcer, options);
|
var processor = factory.Create(connectionString, _announcer, options);
|
||||||
var runner = new MigrationRunner(assembly, migrationContext, processor);
|
var runner = new MigrationRunner(assembly, migrationContext, processor);
|
||||||
runner.MigrateUp(true);
|
runner.MigrateUp(true);
|
||||||
|
|
|
@ -25,9 +25,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
{
|
{
|
||||||
var context = (MigrationContext)ApplicationContext;
|
var context = (MigrationContext)ApplicationContext;
|
||||||
|
|
||||||
SqLiteAlter = context.SQLiteAlter;
|
|
||||||
MigrationHelper = context.MigrationHelper;
|
|
||||||
|
|
||||||
switch (context.MigrationType)
|
switch (context.MigrationType)
|
||||||
{
|
{
|
||||||
case MigrationType.Main:
|
case MigrationType.Main:
|
||||||
|
@ -43,9 +40,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ISQLiteAlter SqLiteAlter { get; private set; }
|
|
||||||
protected ISqLiteMigrationHelper MigrationHelper { get; private set; }
|
|
||||||
|
|
||||||
public override void Down()
|
public override void Down()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentMigrator;
|
||||||
|
using FluentMigrator.Exceptions;
|
||||||
|
using FluentMigrator.Expressions;
|
||||||
|
using FluentMigrator.Model;
|
||||||
|
using FluentMigrator.Runner;
|
||||||
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
|
{
|
||||||
|
public class NzbDroneSqliteProcessor : SqliteProcessor
|
||||||
|
{
|
||||||
|
public NzbDroneSqliteProcessor(IDbConnection connection, IMigrationGenerator generator, IAnnouncer announcer, IMigrationProcessorOptions options, FluentMigrator.Runner.Processors.IDbFactory factory)
|
||||||
|
: base(connection, generator, announcer, options, factory)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool SupportsTransactions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(AlterColumnExpression expression)
|
||||||
|
{
|
||||||
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
|
|
||||||
|
var columnDefinitions = tableDefinition.Columns.ToList();
|
||||||
|
var columnIndex = columnDefinitions.FindIndex(c => c.Name == expression.Column.Name);
|
||||||
|
|
||||||
|
if (columnIndex == -1)
|
||||||
|
{
|
||||||
|
throw new ApplicationException(String.Format("Column {0} does not exist on table {1}.", expression.Column.Name, expression.TableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
columnDefinitions[columnIndex] = expression.Column;
|
||||||
|
|
||||||
|
tableDefinition.Columns = columnDefinitions;
|
||||||
|
|
||||||
|
ProcessAlterTable(tableDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(DeleteColumnExpression expression)
|
||||||
|
{
|
||||||
|
var tableDefinition = GetTableSchema(expression.TableName);
|
||||||
|
|
||||||
|
var columnDefinitions = tableDefinition.Columns.ToList();
|
||||||
|
var indexDefinitions = tableDefinition.Indexes.ToList();
|
||||||
|
|
||||||
|
var columnsToRemove = expression.ColumnNames.ToList();
|
||||||
|
|
||||||
|
columnDefinitions.RemoveAll(c => columnsToRemove.Remove(c.Name));
|
||||||
|
indexDefinitions.RemoveAll(i => i.Columns.Any(c => expression.ColumnNames.Contains(c.Name)));
|
||||||
|
|
||||||
|
tableDefinition.Columns = columnDefinitions;
|
||||||
|
tableDefinition.Indexes = indexDefinitions;
|
||||||
|
|
||||||
|
if (columnsToRemove.Any())
|
||||||
|
{
|
||||||
|
throw new ApplicationException(String.Format("Column {0} does not exist on table {1}.", columnsToRemove.First(), expression.TableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessAlterTable(tableDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual TableDefinition GetTableSchema(String tableName)
|
||||||
|
{
|
||||||
|
var schemaDumper = new SqliteSchemaDumper(this, Announcer);
|
||||||
|
var schema = schemaDumper.ReadDbSchema();
|
||||||
|
|
||||||
|
return schema.Single(v => v.Name == tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void ProcessAlterTable(TableDefinition tableDefinition)
|
||||||
|
{
|
||||||
|
var tableName = tableDefinition.Name;
|
||||||
|
var tempTableName = tableName + "_temp";
|
||||||
|
|
||||||
|
var uid = 0;
|
||||||
|
while (TableExists(null, tempTableName))
|
||||||
|
{
|
||||||
|
tempTableName = tableName + "_temp" + uid++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What is the cleanest way to do this? Add function to Generator?
|
||||||
|
var quoter = new SqliteQuoter();
|
||||||
|
var columnsToTransfer = String.Join(", ", tableDefinition.Columns.Select(c => quoter.QuoteColumnName(c.Name)));
|
||||||
|
|
||||||
|
Process(new CreateTableExpression() { TableName = tempTableName, Columns = tableDefinition.Columns.ToList() });
|
||||||
|
|
||||||
|
Process(String.Format("INSERT INTO {0} SELECT {1} FROM {2}", quoter.QuoteTableName(tempTableName), columnsToTransfer, quoter.QuoteTableName(tableName)));
|
||||||
|
|
||||||
|
Process(new DeleteTableExpression() { TableName = tableName });
|
||||||
|
|
||||||
|
Process(new RenameTableExpression() { OldName = tempTableName, NewName = tableName });
|
||||||
|
|
||||||
|
foreach (var index in tableDefinition.Indexes)
|
||||||
|
{
|
||||||
|
Process(new CreateIndexExpression() { Index = index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using FluentMigrator;
|
||||||
|
using FluentMigrator.Runner;
|
||||||
|
using FluentMigrator.Runner.Generators.SQLite;
|
||||||
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
|
{
|
||||||
|
public class NzbDroneSqliteProcessorFactory : SqliteProcessorFactory
|
||||||
|
{
|
||||||
|
public override IMigrationProcessor Create(String connectionString, IAnnouncer announcer, IMigrationProcessorOptions options)
|
||||||
|
{
|
||||||
|
var factory = new SqliteDbFactory();
|
||||||
|
var connection = factory.CreateConnection(connectionString);
|
||||||
|
var generator = new SqliteGenerator() { compatabilityMode = CompatabilityMode.STRICT };
|
||||||
|
return new NzbDroneSqliteProcessor(connection, generator, announcer, options, factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
|
||||||
{
|
|
||||||
public class SQLiteColumn
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Schema { get; set; }
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("[{0}] {1}", Name, Schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
|
||||||
{
|
|
||||||
public class SQLiteIndex : IEquatable<SQLiteIndex>
|
|
||||||
{
|
|
||||||
public string Column { get; set; }
|
|
||||||
public string Table { get; set; }
|
|
||||||
public bool Unique { get; set; }
|
|
||||||
|
|
||||||
public bool Equals(SQLiteIndex other)
|
|
||||||
{
|
|
||||||
return IndexName == other.IndexName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return IndexName.GetHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("[{0}] Unique: {1}", Column, Unique);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string IndexName
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return string.Format("IX_{0}_{1}", Table, Column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreateSql(string tableName)
|
|
||||||
{
|
|
||||||
if (Unique)
|
|
||||||
{
|
|
||||||
return String.Format(@"CREATE UNIQUE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Format(@"CREATE INDEX ""{2}"" ON ""{0}"" (""{1}"" ASC)", tableName, Column, IndexName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,226 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.SQLite;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using NLog;
|
|
||||||
using NzbDrone.Common.Exceptions;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
|
||||||
{
|
|
||||||
public interface ISqLiteMigrationHelper
|
|
||||||
{
|
|
||||||
Dictionary<String, SQLiteColumn> GetColumns(string tableName);
|
|
||||||
void CreateTable(string tableName, IEnumerable<SQLiteColumn> values, IEnumerable<SQLiteIndex> indexes);
|
|
||||||
void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteColumn> columns);
|
|
||||||
void DropTable(string tableName);
|
|
||||||
void RenameTable(string tableName, string newName);
|
|
||||||
IEnumerable<IGrouping<T, KeyValuePair<int, T>>> GetDuplicates<T>(string tableName, string columnName);
|
|
||||||
SQLiteTransaction BeginTransaction();
|
|
||||||
List<SQLiteIndex> GetIndexes(string tableName);
|
|
||||||
int ExecuteScalar(string command, params string[] args);
|
|
||||||
void ExecuteNonQuery(string command, params string[] args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SqLiteMigrationHelper : ISqLiteMigrationHelper
|
|
||||||
{
|
|
||||||
private readonly SQLiteConnection _connection;
|
|
||||||
|
|
||||||
private static readonly Regex SchemaRegex = new Regex(@"[`'\""\[](?<name>\w+)[`'\""\]]\s(?<schema>[\w-\s]+)",
|
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
|
||||||
|
|
||||||
private static readonly Regex IndexRegex = new Regex(@"\((?:""|')(?<col>.*)(?:""|')\s(?<direction>ASC|DESC)\)$",
|
|
||||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
|
||||||
|
|
||||||
public SqLiteMigrationHelper(IConnectionStringFactory connectionStringFactory, Logger logger)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_connection = new SQLiteConnection(connectionStringFactory.MainDbConnectionString);
|
|
||||||
_connection.Open();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.ErrorException("Couldn't open database " + connectionStringFactory.MainDbConnectionString, e);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetOriginalSql(string tableName)
|
|
||||||
{
|
|
||||||
var command =
|
|
||||||
new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='table' AND name ='{0}'",
|
|
||||||
tableName));
|
|
||||||
|
|
||||||
command.Connection = _connection;
|
|
||||||
|
|
||||||
var sql = (string)command.ExecuteScalar();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sql))
|
|
||||||
{
|
|
||||||
throw new TableNotFoundException(tableName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sql;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<String, SQLiteColumn> GetColumns(string tableName)
|
|
||||||
{
|
|
||||||
var originalSql = GetOriginalSql(tableName);
|
|
||||||
|
|
||||||
var matches = SchemaRegex.Matches(originalSql);
|
|
||||||
|
|
||||||
return matches.Cast<Match>().ToDictionary(
|
|
||||||
match => match.Groups["name"].Value.Trim(),
|
|
||||||
match => new SQLiteColumn
|
|
||||||
{
|
|
||||||
Name = match.Groups["name"].Value.Trim(),
|
|
||||||
Schema = match.Groups["schema"].Value.Trim()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static IEnumerable<T> ReadArray<T>(SQLiteDataReader reader)
|
|
||||||
{
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
yield return (T)Convert.ChangeType(reader[0], typeof(T));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<SQLiteIndex> GetIndexes(string tableName)
|
|
||||||
{
|
|
||||||
var command = new SQLiteCommand(string.Format("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name ='{0}'", tableName));
|
|
||||||
command.Connection = _connection;
|
|
||||||
|
|
||||||
var reader = command.ExecuteReader();
|
|
||||||
var sqls = ReadArray<string>(reader).ToList();
|
|
||||||
var indexes = new List<SQLiteIndex>();
|
|
||||||
|
|
||||||
foreach (var indexSql in sqls)
|
|
||||||
{
|
|
||||||
var newIndex = new SQLiteIndex();
|
|
||||||
var matches = IndexRegex.Match(indexSql);
|
|
||||||
|
|
||||||
if (!matches.Success) continue;;
|
|
||||||
|
|
||||||
newIndex.Column = matches.Groups["col"].Value;
|
|
||||||
newIndex.Unique = indexSql.Contains("UNIQUE");
|
|
||||||
newIndex.Table = tableName;
|
|
||||||
|
|
||||||
indexes.Add(newIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateTable(string tableName, IEnumerable<SQLiteColumn> values, IEnumerable<SQLiteIndex> indexes)
|
|
||||||
{
|
|
||||||
var columns = String.Join(",", values.Select(c => c.ToString()));
|
|
||||||
|
|
||||||
ExecuteNonQuery("CREATE TABLE [{0}] ({1})", tableName, columns);
|
|
||||||
|
|
||||||
foreach (var index in indexes)
|
|
||||||
{
|
|
||||||
ExecuteNonQuery("DROP INDEX IF EXISTS {0}", index.IndexName);
|
|
||||||
ExecuteNonQuery(index.CreateSql(tableName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CopyData(string sourceTable, string destinationTable, IEnumerable<SQLiteColumn> columns)
|
|
||||||
{
|
|
||||||
var originalCount = GetRowCount(sourceTable);
|
|
||||||
|
|
||||||
var columnsToTransfer = String.Join(",", columns.Select(c => c.Name));
|
|
||||||
|
|
||||||
var transferCommand = BuildCommand("INSERT INTO {0} SELECT {1} FROM {2};", destinationTable, columnsToTransfer, sourceTable);
|
|
||||||
|
|
||||||
transferCommand.ExecuteNonQuery();
|
|
||||||
|
|
||||||
var transferredRows = GetRowCount(destinationTable);
|
|
||||||
|
|
||||||
|
|
||||||
if (transferredRows != originalCount)
|
|
||||||
{
|
|
||||||
throw new ApplicationException(string.Format("Expected {0} rows to be copied from [{1}] to [{2}]. But only copied {3}", originalCount, sourceTable, destinationTable, transferredRows));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DropTable(string tableName)
|
|
||||||
{
|
|
||||||
var dropCommand = BuildCommand("DROP TABLE {0};", tableName);
|
|
||||||
dropCommand.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RenameTable(string tableName, string newName)
|
|
||||||
{
|
|
||||||
var renameCommand = BuildCommand("ALTER TABLE {0} RENAME TO {1};", tableName, newName);
|
|
||||||
renameCommand.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IGrouping<T, KeyValuePair<int, T>>> GetDuplicates<T>(string tableName, string columnName)
|
|
||||||
{
|
|
||||||
var getDuplicates = BuildCommand("select id, {0} from {1}", columnName, tableName);
|
|
||||||
|
|
||||||
var result = new List<KeyValuePair<int, T>>();
|
|
||||||
|
|
||||||
using (var reader = getDuplicates.ExecuteReader())
|
|
||||||
{
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
result.Add(new KeyValuePair<int, T>(reader.GetInt32(0), (T)Convert.ChangeType(reader[1], typeof(T))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.GroupBy(c => c.Value).Where(g => g.Count() > 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetRowCount(string tableName)
|
|
||||||
{
|
|
||||||
var countCommand = BuildCommand("SELECT COUNT(*) FROM {0};", tableName);
|
|
||||||
return Convert.ToInt32(countCommand.ExecuteScalar());
|
|
||||||
}
|
|
||||||
|
|
||||||
public SQLiteTransaction BeginTransaction()
|
|
||||||
{
|
|
||||||
return _connection.BeginTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SQLiteCommand BuildCommand(string format, params string[] args)
|
|
||||||
{
|
|
||||||
var command = new SQLiteCommand(string.Format(format, args));
|
|
||||||
command.Connection = _connection;
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExecuteNonQuery(string command, params string[] args)
|
|
||||||
{
|
|
||||||
var sqLiteCommand = new SQLiteCommand(string.Format(command, args))
|
|
||||||
{
|
|
||||||
Connection = _connection
|
|
||||||
};
|
|
||||||
|
|
||||||
sqLiteCommand.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ExecuteScalar(string command, params string[] args)
|
|
||||||
{
|
|
||||||
var sqLiteCommand = new SQLiteCommand(string.Format(command, args))
|
|
||||||
{
|
|
||||||
Connection = _connection
|
|
||||||
};
|
|
||||||
|
|
||||||
return (int)sqLiteCommand.ExecuteScalar();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TableNotFoundException : NzbDroneException
|
|
||||||
{
|
|
||||||
public TableNotFoundException(string tableName)
|
|
||||||
: base("Table [{0}] not found", tableName)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
|
||||||
{
|
|
||||||
public interface ISQLiteAlter
|
|
||||||
{
|
|
||||||
void DropColumns(string tableName, IEnumerable<string> columns);
|
|
||||||
void AddIndexes(string tableName, params SQLiteIndex[] indexes);
|
|
||||||
void Nullify(string tableName, IEnumerable<string> columns);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SQLiteAlter : ISQLiteAlter
|
|
||||||
{
|
|
||||||
private readonly ISqLiteMigrationHelper _sqLiteMigrationHelper;
|
|
||||||
|
|
||||||
public SQLiteAlter(ISqLiteMigrationHelper sqLiteMigrationHelper)
|
|
||||||
{
|
|
||||||
_sqLiteMigrationHelper = sqLiteMigrationHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DropColumns(string tableName, IEnumerable<string> columns)
|
|
||||||
{
|
|
||||||
using (var transaction = _sqLiteMigrationHelper.BeginTransaction())
|
|
||||||
{
|
|
||||||
var originalColumns = _sqLiteMigrationHelper.GetColumns(tableName);
|
|
||||||
var originalIndexes = _sqLiteMigrationHelper.GetIndexes(tableName);
|
|
||||||
|
|
||||||
var newColumns = originalColumns.Where(c => !columns.Contains(c.Key)).Select(c => c.Value).ToList();
|
|
||||||
var newIndexes = originalIndexes.Where(c => !columns.Contains(c.Column));
|
|
||||||
|
|
||||||
CreateTable(tableName, newColumns, newIndexes);
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddIndexes(string tableName, params SQLiteIndex[] indexes)
|
|
||||||
{
|
|
||||||
using (var transaction = _sqLiteMigrationHelper.BeginTransaction())
|
|
||||||
{
|
|
||||||
var columns = _sqLiteMigrationHelper.GetColumns(tableName).Select(c => c.Value).ToList();
|
|
||||||
var originalIndexes = _sqLiteMigrationHelper.GetIndexes(tableName);
|
|
||||||
|
|
||||||
var newIndexes = originalIndexes.Union(indexes);
|
|
||||||
|
|
||||||
CreateTable(tableName, columns, newIndexes);
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Nullify(string tableName, IEnumerable<string> columns)
|
|
||||||
{
|
|
||||||
using (var transaction = _sqLiteMigrationHelper.BeginTransaction())
|
|
||||||
{
|
|
||||||
var originalColumns = _sqLiteMigrationHelper.GetColumns(tableName);
|
|
||||||
var indexes = _sqLiteMigrationHelper.GetIndexes(tableName);
|
|
||||||
|
|
||||||
var newColumns = originalColumns.Select(c =>
|
|
||||||
{
|
|
||||||
if (!columns.Contains(c.Key))
|
|
||||||
{
|
|
||||||
return c.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!c.Value.Schema.Contains("NOT NULL") && c.Value.Schema.Contains("NULL"))
|
|
||||||
{
|
|
||||||
return c.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.Value.Schema.Contains("NOT NULL"))
|
|
||||||
{
|
|
||||||
c.Value.Schema = c.Value.Schema.Replace("NOT NULL", "NULL");
|
|
||||||
return c.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Value.Schema += " NULL";
|
|
||||||
|
|
||||||
return c.Value;
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
CreateTable(tableName, newColumns, indexes);
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateTable(string tableName, List<SQLiteColumn> newColumns, IEnumerable<SQLiteIndex> newIndexes)
|
|
||||||
{
|
|
||||||
var tempTableName = tableName + "_temp";
|
|
||||||
|
|
||||||
_sqLiteMigrationHelper.CreateTable(tempTableName, newColumns, newIndexes);
|
|
||||||
|
|
||||||
_sqLiteMigrationHelper.CopyData(tableName, tempTableName, newColumns);
|
|
||||||
|
|
||||||
_sqLiteMigrationHelper.DropTable(tableName);
|
|
||||||
|
|
||||||
_sqLiteMigrationHelper.RenameTable(tempTableName, tableName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentMigrator.Builders.Execute;
|
||||||
|
using FluentMigrator.Model;
|
||||||
|
using FluentMigrator.Runner;
|
||||||
|
using System;
|
||||||
|
using FluentMigrator.Runner.Processors.SQLite;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
|
{
|
||||||
|
// Modeled after the FluentMigrator SchemaDumper class.
|
||||||
|
// The original implementation had bad support for escaped identifiers, amongst other things.
|
||||||
|
public class SqliteSchemaDumper
|
||||||
|
{
|
||||||
|
public SqliteSchemaDumper(SqliteProcessor processor, IAnnouncer announcer)
|
||||||
|
{
|
||||||
|
Announcer = announcer;
|
||||||
|
Processor = processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IAnnouncer Announcer { get; set; }
|
||||||
|
public SqliteProcessor Processor { get; set; }
|
||||||
|
|
||||||
|
protected internal virtual TableDefinition ReadTableSchema(String sqlSchema)
|
||||||
|
{
|
||||||
|
var reader = new SqliteSyntaxReader(sqlSchema);
|
||||||
|
|
||||||
|
var result = ParseCreateTableStatement(reader);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internal virtual IndexDefinition ReadIndexSchema(String sqlSchema)
|
||||||
|
{
|
||||||
|
var reader = new SqliteSyntaxReader(sqlSchema);
|
||||||
|
|
||||||
|
var result = ParseCreateIndexStatement(reader);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual TableDefinition ParseCreateTableStatement(SqliteSyntaxReader reader)
|
||||||
|
{
|
||||||
|
var table = new TableDefinition();
|
||||||
|
|
||||||
|
while (reader.Read() != SqliteSyntaxReader.TokenType.StringToken || reader.ValueToUpper != "TABLE") ;
|
||||||
|
|
||||||
|
if (reader.Read() == SqliteSyntaxReader.TokenType.StringToken && reader.ValueToUpper == "IF")
|
||||||
|
{
|
||||||
|
reader.Read(); // NOT
|
||||||
|
reader.Read(); // EXISTS
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.Rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Name = ParseIdentifier(reader);
|
||||||
|
|
||||||
|
// Find Column List
|
||||||
|
reader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);
|
||||||
|
|
||||||
|
// Split the list.
|
||||||
|
var list = reader.ReadList();
|
||||||
|
|
||||||
|
foreach (var columnReader in list)
|
||||||
|
{
|
||||||
|
columnReader.SkipWhitespace();
|
||||||
|
|
||||||
|
if (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
|
||||||
|
{
|
||||||
|
if (columnReader.ValueToUpper == "CONSTRAINT" ||
|
||||||
|
columnReader.ValueToUpper == "PRIMARY" || columnReader.ValueToUpper == "UNIQUE" ||
|
||||||
|
columnReader.ValueToUpper == "CHECK" || columnReader.ValueToUpper == "FOREIGN")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columnReader.Rollback();
|
||||||
|
|
||||||
|
var column = ParseColumnDefinition(columnReader);
|
||||||
|
column.TableName = table.Name;
|
||||||
|
table.Columns.Add(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual ColumnDefinition ParseColumnDefinition(SqliteSyntaxReader reader)
|
||||||
|
{
|
||||||
|
var column = new ColumnDefinition();
|
||||||
|
|
||||||
|
column.Name = ParseIdentifier(reader);
|
||||||
|
|
||||||
|
reader.TrimBuffer();
|
||||||
|
|
||||||
|
reader.Read();
|
||||||
|
if (reader.Type != SqliteSyntaxReader.TokenType.End)
|
||||||
|
{
|
||||||
|
column.Type = GetDbType(reader.Value);
|
||||||
|
|
||||||
|
var upper = reader.Buffer.ToUpperInvariant();
|
||||||
|
column.IsPrimaryKey = upper.Contains("PRIMARY KEY");
|
||||||
|
column.IsIdentity = upper.Contains("AUTOINCREMENT");
|
||||||
|
column.IsNullable = !upper.Contains("NOT NULL") && !upper.Contains("PRIMARY KEY");
|
||||||
|
column.IsUnique = upper.Contains("UNIQUE") || upper.Contains("PRIMARY KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IndexDefinition ParseCreateIndexStatement(SqliteSyntaxReader reader)
|
||||||
|
{
|
||||||
|
var index = new IndexDefinition();
|
||||||
|
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
reader.Read();
|
||||||
|
index.IsUnique = reader.ValueToUpper == "UNIQUE";
|
||||||
|
|
||||||
|
while (reader.ValueToUpper != "INDEX") reader.Read();
|
||||||
|
|
||||||
|
if (reader.Read() == SqliteSyntaxReader.TokenType.StringToken && reader.ValueToUpper == "IF")
|
||||||
|
{
|
||||||
|
reader.Read(); // NOT
|
||||||
|
reader.Read(); // EXISTS
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.Rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
index.Name = ParseIdentifier(reader);
|
||||||
|
|
||||||
|
reader.Read(); // ON
|
||||||
|
|
||||||
|
index.TableName = ParseIdentifier(reader);
|
||||||
|
|
||||||
|
// Find Column List
|
||||||
|
reader.SkipTillToken(SqliteSyntaxReader.TokenType.ListStart);
|
||||||
|
|
||||||
|
// Split the list.
|
||||||
|
var list = reader.ReadList();
|
||||||
|
|
||||||
|
foreach (var columnReader in list)
|
||||||
|
{
|
||||||
|
var column = new IndexColumnDefinition();
|
||||||
|
column.Name = ParseIdentifier(columnReader);
|
||||||
|
|
||||||
|
while (columnReader.Read() == SqliteSyntaxReader.TokenType.StringToken)
|
||||||
|
{
|
||||||
|
if (columnReader.ValueToUpper == "COLLATE")
|
||||||
|
{
|
||||||
|
columnReader.Read(); // Skip Collation name
|
||||||
|
}
|
||||||
|
else if (columnReader.ValueToUpper == "DESC")
|
||||||
|
{
|
||||||
|
column.Direction = Direction.Descending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index.Columns.Add(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual String ParseIdentifier(SqliteSyntaxReader reader)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (reader.Type != SqliteSyntaxReader.TokenType.Identifier &&
|
||||||
|
reader.Type != SqliteSyntaxReader.TokenType.StringToken)
|
||||||
|
{
|
||||||
|
throw reader.CreateSyntaxException("Expected Identifier but found {0}", reader.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ISchemaDumper Members
|
||||||
|
|
||||||
|
public virtual IList<TableDefinition> ReadDbSchema()
|
||||||
|
{
|
||||||
|
IList<TableDefinition> tables = ReadTables();
|
||||||
|
foreach (var table in tables)
|
||||||
|
{
|
||||||
|
table.Indexes = ReadIndexes(table.SchemaName, table.Name);
|
||||||
|
//table.ForeignKeys = ReadForeignKeys(table.SchemaName, table.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected virtual DataSet Read(string template, params object[] args)
|
||||||
|
{
|
||||||
|
return Processor.Read(template, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IList<TableDefinition> ReadTables()
|
||||||
|
{
|
||||||
|
const string sqlCommand = @"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;";
|
||||||
|
var dtTable = Read(sqlCommand).Tables[0];
|
||||||
|
|
||||||
|
var tableDefinitionList = new List<TableDefinition>();
|
||||||
|
|
||||||
|
foreach (DataRow dr in dtTable.Rows)
|
||||||
|
{
|
||||||
|
var sql = dr["sql"].ToString();
|
||||||
|
var table = ReadTableSchema(sql);
|
||||||
|
|
||||||
|
tableDefinitionList.Add(table);
|
||||||
|
}
|
||||||
|
return tableDefinitionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get DbType from string type definition
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeNum"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DbType? GetDbType(string typeNum)
|
||||||
|
{
|
||||||
|
switch (typeNum.ToUpper())
|
||||||
|
{
|
||||||
|
case "BLOB":
|
||||||
|
return DbType.Binary;
|
||||||
|
case "INTEGER":
|
||||||
|
return DbType.Int64;
|
||||||
|
case "NUMERIC":
|
||||||
|
return DbType.Double;
|
||||||
|
case "TEXT":
|
||||||
|
return DbType.String;
|
||||||
|
case "DATETIME":
|
||||||
|
return DbType.DateTime;
|
||||||
|
case "UNIQUEIDENTIFIER":
|
||||||
|
return DbType.Guid;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IList<IndexDefinition> ReadIndexes(string schemaName, string tableName)
|
||||||
|
{
|
||||||
|
var sqlCommand = string.Format(@"SELECT type, name, sql FROM sqlite_master WHERE tbl_name = '{0}' AND type = 'index' AND name NOT LIKE 'sqlite_auto%';", tableName);
|
||||||
|
DataTable table = Read(sqlCommand).Tables[0];
|
||||||
|
|
||||||
|
IList<IndexDefinition> indexes = new List<IndexDefinition>();
|
||||||
|
|
||||||
|
foreach (DataRow dr in table.Rows)
|
||||||
|
{
|
||||||
|
var sql = dr["sql"].ToString();
|
||||||
|
var index = ReadIndexSchema(sql);
|
||||||
|
indexes.Add(index);
|
||||||
|
}
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
|
{
|
||||||
|
|
||||||
|
public class SqliteSyntaxReader
|
||||||
|
{
|
||||||
|
public String Buffer { get; private set; }
|
||||||
|
public Int32 Index { get; private set; }
|
||||||
|
|
||||||
|
private Int32 _previousIndex;
|
||||||
|
|
||||||
|
public TokenType Type { get; private set; }
|
||||||
|
public String Value { get; private set; }
|
||||||
|
|
||||||
|
public String ValueToUpper
|
||||||
|
{
|
||||||
|
get { return Value.ToUpperInvariant(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean IsEndOfFile
|
||||||
|
{
|
||||||
|
get { return Index >= Buffer.Length; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TokenType
|
||||||
|
{
|
||||||
|
Start,
|
||||||
|
Whitespace,
|
||||||
|
End,
|
||||||
|
ListStart,
|
||||||
|
ListSeparator,
|
||||||
|
ListEnd,
|
||||||
|
Identifier,
|
||||||
|
StringToken,
|
||||||
|
StringLiteral,
|
||||||
|
UnknownSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
public SqliteSyntaxReader(String sql)
|
||||||
|
{
|
||||||
|
Buffer = sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TrimBuffer()
|
||||||
|
{
|
||||||
|
Buffer = Buffer.Substring(Index);
|
||||||
|
Index = 0;
|
||||||
|
_previousIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SkipWhitespace()
|
||||||
|
{
|
||||||
|
while (!IsEndOfFile && char.IsWhiteSpace(Buffer[Index])) Index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SkipTillToken(TokenType tokenType)
|
||||||
|
{
|
||||||
|
if (IsEndOfFile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (Read() != tokenType)
|
||||||
|
{
|
||||||
|
if (Type == TokenType.ListStart)
|
||||||
|
SkipTillToken(TokenType.ListEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rollback()
|
||||||
|
{
|
||||||
|
Index = _previousIndex;
|
||||||
|
Type = TokenType.Whitespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenType Read()
|
||||||
|
{
|
||||||
|
if (!IsEndOfFile && char.IsWhiteSpace(Buffer[Index]))
|
||||||
|
{
|
||||||
|
Type = TokenType.Whitespace;
|
||||||
|
SkipWhitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
_previousIndex = Index;
|
||||||
|
|
||||||
|
if (IsEndOfFile)
|
||||||
|
{
|
||||||
|
Type = TokenType.End;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == '(')
|
||||||
|
{
|
||||||
|
Type = TokenType.ListStart;
|
||||||
|
Value = null;
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == ',')
|
||||||
|
{
|
||||||
|
Type = TokenType.ListSeparator;
|
||||||
|
Value = null;
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == ')')
|
||||||
|
{
|
||||||
|
Type = TokenType.ListEnd;
|
||||||
|
Value = null;
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == '\'')
|
||||||
|
{
|
||||||
|
Type = TokenType.StringLiteral;
|
||||||
|
Value = ReadEscapedString('\'');
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == '\"')
|
||||||
|
{
|
||||||
|
Type = TokenType.Identifier;
|
||||||
|
Value = ReadEscapedString('\"');
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == '`')
|
||||||
|
{
|
||||||
|
Type = TokenType.Identifier;
|
||||||
|
Value = ReadEscapedString('`');
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer[Index] == '[')
|
||||||
|
{
|
||||||
|
Type = TokenType.Identifier;
|
||||||
|
Value = ReadTerminatedString(']');
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Type == TokenType.UnknownSymbol)
|
||||||
|
{
|
||||||
|
Value = Buffer[Index].ToString();
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetter(Buffer[Index]))
|
||||||
|
{
|
||||||
|
var start = Index;
|
||||||
|
var end = start + 1;
|
||||||
|
while (end < Buffer.Length && (char.IsLetter(Buffer[end]) || Buffer[end] == '_')) end++;
|
||||||
|
if (end >= Buffer.Length || Buffer[end] == ',' || Buffer[end] == ')' || char.IsWhiteSpace(Buffer[end]))
|
||||||
|
{
|
||||||
|
Index = end;
|
||||||
|
}
|
||||||
|
else if (Type == TokenType.UnknownSymbol)
|
||||||
|
{
|
||||||
|
Value = Buffer[Index].ToString();
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw CreateSyntaxException("Unexpected sequence.");
|
||||||
|
}
|
||||||
|
Type = TokenType.StringToken;
|
||||||
|
Value = Buffer.Substring(start, end - start);
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type = TokenType.UnknownSymbol;
|
||||||
|
Value = Buffer[Index].ToString();
|
||||||
|
Index++;
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SqliteSyntaxReader> ReadList()
|
||||||
|
{
|
||||||
|
if (Type != TokenType.ListStart)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<SqliteSyntaxReader>();
|
||||||
|
|
||||||
|
var start = Index;
|
||||||
|
while (Read() != TokenType.ListEnd)
|
||||||
|
{
|
||||||
|
if (Type == TokenType.End)
|
||||||
|
{
|
||||||
|
throw CreateSyntaxException("Expected ListEnd first");
|
||||||
|
}
|
||||||
|
if (Type == TokenType.ListStart)
|
||||||
|
{
|
||||||
|
SkipTillToken(TokenType.ListEnd);
|
||||||
|
}
|
||||||
|
else if (Type == TokenType.ListSeparator)
|
||||||
|
{
|
||||||
|
result.Add(new SqliteSyntaxReader(Buffer.Substring(start, Index - start - 1)));
|
||||||
|
start = Index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Index >= start + 1)
|
||||||
|
{
|
||||||
|
result.Add(new SqliteSyntaxReader(Buffer.Substring(start, Index - start - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String ReadTerminatedString(Char terminator)
|
||||||
|
{
|
||||||
|
var start = Index + 1;
|
||||||
|
var end = Buffer.IndexOf(terminator, Index);
|
||||||
|
|
||||||
|
if (end == -1) throw new SyntaxErrorException();
|
||||||
|
|
||||||
|
Index = end + 1;
|
||||||
|
return Buffer.Substring(start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String ReadEscapedString(Char escape)
|
||||||
|
{
|
||||||
|
var identifier = new StringBuilder();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var start = Index + 1;
|
||||||
|
var end = Buffer.IndexOf(escape, start);
|
||||||
|
|
||||||
|
if (end == -1) throw new SyntaxErrorException();
|
||||||
|
|
||||||
|
Index = end + 1;
|
||||||
|
identifier.Append(Buffer.Substring(start, end - start));
|
||||||
|
|
||||||
|
if (Buffer[Index] != escape) break;
|
||||||
|
|
||||||
|
identifier.Append(escape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return identifier.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyntaxErrorException CreateSyntaxException(String message, params object[] args)
|
||||||
|
{
|
||||||
|
return new SyntaxErrorException(String.Format("{0}. Syntax Error near: {1}", String.Format(message, args), Buffer.Substring(_previousIndex)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -230,10 +230,10 @@
|
||||||
<Compile Include="Datastore\Migration\Framework\MigrationOptions.cs" />
|
<Compile Include="Datastore\Migration\Framework\MigrationOptions.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\MigrationType.cs" />
|
<Compile Include="Datastore\Migration\Framework\MigrationType.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\NzbDroneMigrationBase.cs" />
|
<Compile Include="Datastore\Migration\Framework\NzbDroneMigrationBase.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\SQLiteAlter.cs" />
|
<Compile Include="Datastore\Migration\Framework\NzbDroneSqliteProcessor.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\SQLiteColumn.cs" />
|
<Compile Include="Datastore\Migration\Framework\NzbDroneSqliteProcessorFactory.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\SQLiteIndex.cs" />
|
<Compile Include="Datastore\Migration\Framework\SqliteSchemaDumper.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\SQLiteMigrationHelper.cs" />
|
<Compile Include="Datastore\Migration\Framework\SqliteSyntaxReader.cs" />
|
||||||
<Compile Include="Datastore\ModelBase.cs" />
|
<Compile Include="Datastore\ModelBase.cs" />
|
||||||
<Compile Include="Datastore\ModelNotFoundException.cs" />
|
<Compile Include="Datastore\ModelNotFoundException.cs" />
|
||||||
<Compile Include="Datastore\PagingSpec.cs" />
|
<Compile Include="Datastore\PagingSpec.cs" />
|
||||||
|
|
Loading…
Reference in New Issue