using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using System.Linq;
using System.Text.RegularExpressions;
using NPoco;
using StackExchange.Profiling.Data;
using Umbraco.Core.Persistence.FaultHandling;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence
{
///
/// Provides extension methods to NPoco Database class.
///
public static partial class NPocoDatabaseExtensions
{
// NOTE
//
// proper way to do it with TSQL and SQLCE
// IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...)
// BEGIN
// UPDATE table SET ... WHERE ...
// END
// ELSE
// BEGIN
// INSERT INTO table (...) VALUES (...)
// END
//
// works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK
//
// proper way to do it with MySQL
// IF EXISTS (SELECT ... FROM table WHERE ... FOR UPDATE)
// BEGIN
// UPDATE table SET ... WHERE ...
// END
// ELSE
// BEGIN
// INSERT INTO table (...) VALUES (...)
// END
//
// MySQL locks the constraint ONLY if it exists, so INSERT may fail...
// in theory, happens in READ COMMITTED but not REPEATABLE READ
// http://www.percona.com/blog/2012/08/28/differences-between-read-committed-and-repeatable-read-transaction-isolation-levels/
// but according to
// http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
// it won't work for exact index value (only ranges) so really...
//
// MySQL should do
// INSERT INTO table (...) VALUES (...) ON DUPLICATE KEY UPDATE ...
//
// also the lock is released when the transaction is committed
// not sure if that can have unexpected consequences on our code?
//
// so... for the time being, let's do with that somewhat crazy solution below...
// todo: use the proper database syntax, not this kludge
///
/// Safely inserts a record, or updates if it exists, based on a unique constraint.
///
///
///
/// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object
/// passed in will contain the updated value.
///
/// We cannot rely on database-specific options such as MySql ON DUPLICATE KEY UPDATE or MSSQL MERGE WHEN MATCHED because SQLCE
/// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting
/// isolation levels globally. We want to keep it simple for the time being and manage it manually.
/// We handle it by trying to update, then insert, etc. until something works, or we get bored.
/// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value
/// once T1 and T2 have completed. Whereas here, it could contain T1's value.
///
internal static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco)
where T : class
{
return db.InsertOrUpdate(poco, null, null);
}
///
/// Safely inserts a record, or updates if it exists, based on a unique constraint.
///
///
///
///
/// If the entity has a composite key they you need to specify the update command explicitly
/// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object
/// passed in will contain the updated value.
///
/// We cannot rely on database-specific options such as MySql ON DUPLICATE KEY UPDATE or MSSQL MERGE WHEN MATCHED because SQLCE
/// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting
/// isolation levels globally. We want to keep it simple for the time being and manage it manually.
/// We handle it by trying to update, then insert, etc. until something works, or we get bored.
/// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value
/// once T1 and T2 have completed. Whereas here, it could contain T1's value.
///
internal static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db,
T poco,
string updateCommand,
object updateArgs)
where T : class
{
if (poco == null)
throw new ArgumentNullException(nameof(poco));
// fixme - NPoco has a Save method that works with the primary key
// in any case, no point trying to update if there's no primary key!
// try to update
var rowCount = updateCommand.IsNullOrWhiteSpace()
? db.Update(poco)
: db.Update(updateCommand, updateArgs);
if (rowCount > 0)
return RecordPersistenceType.Update;
// failed: does not exist, need to insert
// RC1 race cond here: another thread may insert a record with the same constraint
var i = 0;
while (i++ < 4)
{
try
{
// try to insert
db.Insert(poco);
return RecordPersistenceType.Insert;
}
catch (SqlException) // assuming all db engines will throw that exception
{
// failed: exists (due to race cond RC1)
// RC2 race cond here: another thread may remove the record
// try to update
rowCount = updateCommand.IsNullOrWhiteSpace()
? db.Update(poco)
: db.Update(updateCommand, updateArgs);
if (rowCount > 0)
return RecordPersistenceType.Update;
// failed: does not exist (due to race cond RC2), need to insert
// loop
}
}
// this can go on forever... have to break at some point and report an error.
throw new DataException("Record could not be inserted or updated.");
}
///
/// This will escape single @ symbols for npoco values so it doesn't think it's a parameter
///
///
///
public static string EscapeAtSymbols(string value)
{
if (value.Contains("@") == false) return value;
//this fancy regex will only match a single @ not a double, etc...
var regex = new Regex("(?
/// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff
///
///
///
///
private static TConnection GetTypedConnection(IDbConnection connection)
where TConnection : class, IDbConnection
{
var profiled = connection as ProfiledDbConnection;
return profiled == null ? connection as TConnection : profiled.InnerConnection as TConnection;
}
///
/// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff
///
///
///
///
private static TTransaction GetTypedTransaction(IDbTransaction transaction)
where TTransaction : class, IDbTransaction
{
var profiled = transaction as ProfiledDbTransaction;
return profiled == null ? transaction as TTransaction : profiled.WrappedTransaction as TTransaction;
}
///
/// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff
///
///
///
///
private static TCommand GetTypedCommand(IDbCommand command)
where TCommand : class, IDbCommand
{
var faultHandling = command as FaultHandlingDbCommand;
if (faultHandling != null) command = faultHandling.Inner;
var profiled = command as ProfiledDbCommand;
if (profiled != null) command = profiled.InternalCommand;
return command as TCommand;
}
public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName)
{
var sql = new Sql(string.Format(
sqlSyntax.TruncateTable,
sqlSyntax.GetQuotedTableName(tableName)));
db.Execute(sql);
}
public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database)
{
var transaction = database.Transaction;
return transaction?.IsolationLevel ?? IsolationLevel.Unspecified;
}
public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory)
{
return source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize);
}
}
}