diff --git a/NzbDrone.Core/Datastore/Connection.cs b/NzbDrone.Core/Datastore/Connection.cs index e8eedf736..1c45779db 100644 --- a/NzbDrone.Core/Datastore/Connection.cs +++ b/NzbDrone.Core/Datastore/Connection.cs @@ -1,4 +1,6 @@ using System; +using System.Data; +using System.Data.Common; using System.Data.SQLite; using System.IO; using MvcMiniProfiler.Data; @@ -58,9 +60,12 @@ public static IRepository CreateSimpleRepository(string connectionString) public static IDatabase GetPetaPocoDb(string connectionString) { var profileConnection = ProfiledDbConnection.Get(new SQLiteConnection(connectionString)); - PetaPoco.Database.Mapper = new CustomeMapper(); - var db = new PetaPoco.Database(profileConnection); - db.OpenSharedConnection(); + + Database.Mapper = new CustomeMapper(); + var db = new Database(profileConnection); + + if (profileConnection.State != ConnectionState.Open) + profileConnection.Open(); return db; } diff --git a/NzbDrone.Core/Datastore/PetaPoco/PetaPoco.cs b/NzbDrone.Core/Datastore/PetaPoco/PetaPoco.cs index 8abc47cc4..415faaa9c 100644 --- a/NzbDrone.Core/Datastore/PetaPoco/PetaPoco.cs +++ b/NzbDrone.Core/Datastore/PetaPoco/PetaPoco.cs @@ -1,5 +1,5 @@ /* PetaPoco v4.0.2 - A Tiny ORMish thing for your POCO's. - * Copyright © 2011 Topten Software. All Rights Reserved. + * Copyright © 2011 Topten Software. All Rights Reserved. * * Apache License 2.0 - http://www.toptensoftware.com/petapoco/license * @@ -24,7 +24,6 @@ using System.Linq.Expressions; -// ReSharper disable namespace PetaPoco { // Poco's marked [Explicit] require all column properties to be marked @@ -182,6 +181,8 @@ public interface IDatabaseQuery List Fetch(long page, long itemsPerPage, Sql sql); Page Page(long page, long itemsPerPage, string sql, params object[] args); Page Page(long page, long itemsPerPage, Sql sql); + List SkipTake(long skip, long take, string sql, params object[] args); + List SkipTake(long skip, long take, Sql sql); List Fetch(Func cb, string sql, params object[] args); List Fetch(Func cb, string sql, params object[] args); List Fetch(Func cb, string sql, params object[] args); @@ -221,13 +222,14 @@ public interface IDatabaseQuery T FirstOrDefault(Sql sql); bool Exists(object primaryKey); int OneTimeCommandTimeout { get; set; } + bool Exists(string sql, params object[] args); } public interface IDatabase : IDatabaseQuery { void Dispose(); IDbConnection Connection { get; } - Transaction GetTransaction(); + ITransaction GetTransaction(); void BeginTransaction(); void AbortTransaction(); void CompleteTransaction(); @@ -240,6 +242,7 @@ public interface IDatabase : IDatabaseQuery int Update(object poco, object primaryKeyValue); int Update(string sql, params object[] args); int Update(Sql sql); + void UpdateMany(IEnumerable pocoList); int Delete(string tableName, string primaryKeyName, object poco); int Delete(string tableName, string primaryKeyName, object poco, object primaryKeyValue); int Delete(object poco); @@ -248,6 +251,8 @@ public interface IDatabase : IDatabaseQuery int Delete(object pocoOrPrimaryKey); void Save(string tableName, string primaryKeyName, object poco); void Save(object poco); + void InsertMany(IEnumerable pocoList); + void SaveMany(IEnumerable pocoList); } // Database class ... this is where most of the action happens @@ -353,19 +358,17 @@ public void OpenSharedConnection() { _sharedConnection = _factory.CreateConnection(); _sharedConnection.ConnectionString = _connectionString; + _sharedConnection.Open(); if (KeepConnectionAlive) _sharedConnectionDepth++; // Make sure you call Dispose } - - if (_sharedConnection.State != ConnectionState.Open) - _sharedConnection.Open(); - _sharedConnectionDepth++; - } - // Close a previously opened connection + /// + /// Close a previously opened connection + /// public void CloseSharedConnection() { if (_sharedConnectionDepth > 0) @@ -386,7 +389,7 @@ public IDbConnection Connection } // Helper to create a transaction scope - public Transaction GetTransaction() + public ITransaction GetTransaction() { return new Transaction(this); } @@ -750,7 +753,7 @@ public List Fetch() static Regex rxColumns = new Regex(@"\A\s*SELECT\s+((?:\((?>\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|.)*?)(?\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:ASC|DESC))?(?:\s*,\s*(?:\((?>\((?)|\)(?<-depth>)|.?)*(?(depth)(?!))\)|[\w\(\)\.])+(?:\s+(?:ASC|DESC))?)*", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled); - public static bool SplitSqlForPaging(string sql, out string sqlCount, out string sqlSelectRemoved, out string sqlOrderBy) + public static bool SplitSqlForPaging(string sql, out string sqlCount, out string sqlSelectRemoved, out string sqlOrderBy) { sqlSelectRemoved = null; sqlCount = null; @@ -766,26 +769,32 @@ public static bool SplitSqlForPaging(string sql, out string sqlCount, out string sqlCount = sql.Substring(0, g.Index) + "COUNT(*) " + sql.Substring(g.Index + g.Length); sqlSelectRemoved = sql.Substring(g.Index); - // Look for an "ORDER BY " clause + // Look for an "ORDER BY " clause or primarykey from pocodata + var data = PocoData.ForType(typeof(T)); + m = rxOrderBy.Match(sqlCount); - if (!m.Success) + if (!m.Success + && (string.IsNullOrEmpty(data.TableInfo.PrimaryKey) || + (!data.TableInfo.PrimaryKey.Split(',').All(x => data.Columns.Values.Any(y => y.ColumnName.Equals(x, StringComparison.OrdinalIgnoreCase)))))) + { return false; + } g = m.Groups[0]; - sqlOrderBy = g.ToString(); + sqlOrderBy = m.Success ? g.ToString() : "ORDER BY " + data.TableInfo.PrimaryKey; sqlCount = sqlCount.Substring(0, g.Index) + sqlCount.Substring(g.Index + g.Length); return true; } - public void BuildPageQueries(long page, long itemsPerPage, string sql, ref object[] args, out string sqlCount, out string sqlPage) + private void BuildPageQueries(long skip, long take, string sql, ref object[] args, out string sqlCount, out string sqlPage) { // Add auto select clause sql = AddSelectClause(sql); // Split the SQL into the bits we need string sqlSelectRemoved, sqlOrderBy; - if (!SplitSqlForPaging(sql, out sqlCount, out sqlSelectRemoved, out sqlOrderBy)) + if (!SplitSqlForPaging(sql, out sqlCount, out sqlSelectRemoved, out sqlOrderBy)) throw new Exception("Unable to parse SQL statement for paged query"); if (_dbType == DBType.Oracle && sqlSelectRemoved.StartsWith("*")) throw new Exception("Query must alias '*' when performing a paged query.\neg. select t.* from table t order by t.id"); @@ -793,20 +802,21 @@ public void BuildPageQueries(long page, long itemsPerPage, string sql, ref ob // Build the SQL for the actual final result if (_dbType == DBType.SqlServer || _dbType == DBType.Oracle) { + var fromIndex = sqlSelectRemoved.IndexOf("from", StringComparison.OrdinalIgnoreCase); sqlSelectRemoved = rxOrderBy.Replace(sqlSelectRemoved, ""); - sqlPage = string.Format("SELECT * FROM (SELECT ROW_NUMBER() OVER ({0}) peta_rn, {1}) peta_paged WHERE peta_rn>@{2} AND peta_rn<=@{3}", - sqlOrderBy, sqlSelectRemoved, args.Length, args.Length + 1); - args = args.Concat(new object[] { (page - 1) * itemsPerPage, page * itemsPerPage }).ToArray(); + sqlPage = string.Format("SELECT * FROM (SELECT {2}, ROW_NUMBER() OVER ({0}) peta_rn {1}) peta_paged WHERE peta_rn>@{3} AND peta_rn<=@{4}", + sqlOrderBy, sqlSelectRemoved.Substring(fromIndex), sqlSelectRemoved.Substring(0, fromIndex - 1), args.Length, args.Length + 1); + args = args.Concat(new object[] { skip, skip + take }).ToArray(); } else if (_dbType == DBType.SqlServerCE) { sqlPage = string.Format("{0}\nOFFSET @{1} ROWS FETCH NEXT @{2} ROWS ONLY", sql, args.Length, args.Length + 1); - args = args.Concat(new object[] { (page - 1) * itemsPerPage, itemsPerPage }).ToArray(); + args = args.Concat(new object[] { skip, take }).ToArray(); } else { sqlPage = string.Format("{0}\nLIMIT @{1} OFFSET @{2}", sql, args.Length, args.Length + 1); - args = args.Concat(new object[] { itemsPerPage, (page - 1) * itemsPerPage }).ToArray(); + args = args.Concat(new object[] { take, skip }).ToArray(); } } @@ -815,7 +825,11 @@ public void BuildPageQueries(long page, long itemsPerPage, string sql, ref ob public Page Page(long page, long itemsPerPage, string sql, params object[] args) { string sqlCount, sqlPage; - BuildPageQueries(page, itemsPerPage, sql, ref args, out sqlCount, out sqlPage); + + long skip = (page - 1) * itemsPerPage; + long take = itemsPerPage; + + BuildPageQueries(skip, take, sql, ref args, out sqlCount, out sqlPage); // Save the one-time command time out and use it for both queries int saveTimeout = OneTimeCommandTimeout; @@ -843,6 +857,21 @@ public Page Page(long page, long itemsPerPage, Sql sql) return Page(page, itemsPerPage, sql.SQL, sql.Arguments); } + public List SkipTake(long skip, long take, string sql, params object[] args) + { + string sqlCount, sqlPage; + + BuildPageQueries(skip, take, sql, ref args, out sqlCount, out sqlPage); + + var result = Fetch(sqlPage, args); + return result; + } + + public List SkipTake(long skip, long take, Sql sql) + { + return SkipTake(skip, take, sql.SQL, sql.Arguments); + } + // Return an enumerable collection of pocos public IEnumerable Query(string sql, params object[] args) { @@ -1213,6 +1242,38 @@ public bool Exists(object primaryKey) var primaryKeyValuePairs = GetPrimaryKeyValues(PocoData.ForType(typeof(T)).TableInfo.PrimaryKey, primaryKey); return FirstOrDefault(string.Format("WHERE {0}", BuildPrimaryKeySql(primaryKeyValuePairs, ref index)), primaryKeyValuePairs.Select(x => x.Value).ToArray()) != null; } + + + public bool Exists(string sql, params object[] args) + { + var poco = PocoData.ForType(typeof(T)).TableInfo; + + string existsTemplate; + + switch (_dbType) + { + case DBType.SQLite: + case DBType.MySql: + { + existsTemplate = "SELECT EXISTS (SELECT 1 FROM {0} WHERE {1})"; + break; + } + + case DBType.SqlServer: + { + existsTemplate = "IF EXISTS (SELECT 1 FROM {0} WHERE {1}) SELECT 1 ELSE SELECT 0"; + break; + } + default: + { + existsTemplate = "SELECT COUNT(*) FROM {0} WHERE {1}"; + break; + } + } + + + return ExecuteScalar(string.Format(existsTemplate, poco.TableName, sql), args) != 0; + } public T Single(object primaryKey) { var index = 0; @@ -1297,7 +1358,6 @@ public object Insert(string tableName, string primaryKeyName, bool autoIncrement { using (var cmd = CreateCommand(_sharedConnection, "")) { - var pd = PocoData.ForObject(poco, primaryKeyName); var names = new List(); var values = new List(); @@ -1466,6 +1526,19 @@ public object Insert(object poco) return Insert(pd.TableInfo.TableName, pd.TableInfo.PrimaryKey, pd.TableInfo.AutoIncrement, poco); } + public void InsertMany(IEnumerable pocoList) + { + using (var tran = GetTransaction()) + { + foreach (var poco in pocoList) + { + Insert(poco); + } + + tran.Complete(); + } + } + // Update a record with values from a poco. primary key value can be either supplied or read from the poco public int Update(string tableName, string primaryKeyName, object poco, object primaryKeyValue) { @@ -1617,6 +1690,20 @@ public int Update(Sql sql) return Execute(new Sql(string.Format("UPDATE {0}", EscapeTableName(pd.TableInfo.TableName))).Append(sql)); } + public void UpdateMany(IEnumerable pocoList) + { + using (var tran = GetTransaction()) + { + foreach (var poco in pocoList) + { + Update(poco); + } + + tran.Complete(); + } + } + + public int Delete(string tableName, string primaryKeyName, object poco) { return Delete(tableName, primaryKeyName, poco, null); @@ -1747,6 +1834,19 @@ public void Save(object poco) Save(pd.TableInfo.TableName, pd.TableInfo.PrimaryKey, poco); } + public void SaveMany(IEnumerable pocoList) + { + using (var tran = GetTransaction()) + { + foreach (var poco in pocoList) + { + Save(poco); + } + + tran.Complete(); + } + } + public int CommandTimeout { get; set; } public int OneTimeCommandTimeout { get; set; } @@ -2269,7 +2369,12 @@ private static Func GetConverter(bool forceDateTimesToUtc, PocoC } // Transaction object helps maintain transaction depth counts - public class Transaction : IDisposable + public interface ITransaction : IDisposable + { + void Complete(); + } + + public class Transaction : ITransaction { public Transaction(Database db) {