2016-04-12 15:11:07 +02:00
using System ;
using System.Collections.Generic ;
using System.Data ;
using System.Text.RegularExpressions ;
2022-01-18 15:23:53 +00:00
using Microsoft.Data.SqlClient ;
2016-04-12 15:11:07 +02:00
using NPoco ;
2016-11-04 18:40:42 +01:00
using StackExchange.Profiling.Data ;
2021-02-12 13:36:50 +01:00
using Umbraco.Cms.Infrastructure.Persistence ;
using Umbraco.Cms.Infrastructure.Persistence.FaultHandling ;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax ;
2016-04-12 15:11:07 +02:00
2021-02-12 13:36:50 +01:00
namespace Umbraco.Extensions
2016-04-12 15:11:07 +02:00
{
/// <summary>
/// Provides extension methods to NPoco Database class.
/// </summary>
2017-05-12 14:49:44 +02:00
public static partial class NPocoDatabaseExtensions
2016-04-12 15:11:07 +02:00
{
2020-04-17 00:47:26 +10:00
/// <summary>
/// Iterates over the result of a paged data set with a db reader
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="database"></param>
/// <param name="pageSize">
/// The number of rows to load per page
/// </param>
/// <param name="sql"></param>
2021-06-24 09:43:57 -06:00
/// <param name="sqlCount">Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used</param>
2020-04-17 00:47:26 +10:00
/// <returns></returns>
/// <remarks>
/// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to
/// iterate over each row with a reader using Query vs Fetch.
/// </remarks>
2022-02-24 09:24:56 +01:00
public static IEnumerable < T > QueryPaged < T > ( this IDatabase database , long pageSize , Sql sql , Sql ? sqlCount )
2020-04-17 00:47:26 +10:00
{
var sqlString = sql . SQL ;
var sqlArgs = sql . Arguments ;
int? itemCount = null ;
long pageIndex = 0 ;
do
{
// Get the paged queries
2021-06-24 09:43:57 -06:00
database . BuildPageQueries < T > ( pageIndex * pageSize , pageSize , sqlString , ref sqlArgs , out var generatedSqlCount , out var sqlPage ) ;
2020-04-17 00:47:26 +10:00
// get the item count once
if ( itemCount = = null )
{
2021-06-24 09:43:57 -06:00
itemCount = database . ExecuteScalar < int > ( sqlCount ? . SQL ? ? generatedSqlCount , sqlCount ? . Arguments ? ? sqlArgs ) ;
2020-04-17 00:47:26 +10:00
}
pageIndex + + ;
// iterate over rows without allocating all items to memory (Query vs Fetch)
foreach ( var row in database . Query < T > ( sqlPage , sqlArgs ) )
{
yield return row ;
}
} while ( ( pageIndex * pageSize ) < itemCount ) ;
}
2021-06-24 09:43:57 -06:00
/// <summary>
/// Iterates over the result of a paged data set with a db reader
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="database"></param>
/// <param name="pageSize">
/// The number of rows to load per page
/// </param>
/// <param name="sql"></param>
/// <returns></returns>
/// <remarks>
/// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to
/// iterate over each row with a reader using Query vs Fetch.
/// </remarks>
public static IEnumerable < T > QueryPaged < T > ( this IDatabase database , long pageSize , Sql sql ) = > database . QueryPaged < T > ( pageSize , sql , null ) ;
2016-04-12 15:11:07 +02:00
// 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
//
2019-01-26 09:42:14 -05:00
// TODO: use the proper database syntax, not this kludge
2016-04-12 15:11:07 +02:00
/// <summary>
/// Safely inserts a record, or updates if it exists, based on a unique constraint.
/// </summary>
/// <param name="db"></param>
/// <param name="poco"></param>
/// <returns>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.</returns>
/// <remarks>
2019-01-17 12:07:31 +01:00
/// <para>We cannot rely on database-specific options because SQLCE
2016-04-12 15:11:07 +02:00
/// 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.</para>
/// <para>We handle it by trying to update, then insert, etc. until something works, or we get bored.</para>
/// <para>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.</para>
/// </remarks>
2019-12-12 08:11:23 +01:00
public static RecordPersistenceType InsertOrUpdate < T > ( this IUmbracoDatabase db , T poco )
2016-04-12 15:11:07 +02:00
where T : class
{
return db . InsertOrUpdate ( poco , null , null ) ;
}
/// <summary>
/// Safely inserts a record, or updates if it exists, based on a unique constraint.
/// </summary>
/// <param name="db"></param>
/// <param name="poco"></param>
/// <param name="updateArgs"></param>
/// <param name="updateCommand">If the entity has a composite key they you need to specify the update command explicitly</param>
/// <returns>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.</returns>
/// <remarks>
2019-01-17 12:07:31 +01:00
/// <para>We cannot rely on database-specific options because SQLCE
2016-04-12 15:11:07 +02:00
/// 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.</para>
/// <para>We handle it by trying to update, then insert, etc. until something works, or we get bored.</para>
/// <para>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.</para>
/// </remarks>
2019-12-12 08:11:23 +01:00
public static RecordPersistenceType InsertOrUpdate < T > ( this IUmbracoDatabase db ,
2016-04-12 15:11:07 +02:00
T poco ,
2022-02-24 09:24:56 +01:00
string? updateCommand ,
object? updateArgs )
2016-04-12 15:11:07 +02:00
where T : class
{
if ( poco = = null )
2016-06-08 09:59:41 +02:00
throw new ArgumentNullException ( nameof ( poco ) ) ;
2016-04-12 15:11:07 +02:00
2019-01-26 09:42:14 -05:00
// TODO: NPoco has a Save method that works with the primary key
2017-05-12 14:49:44 +02:00
// in any case, no point trying to update if there's no primary key!
2016-04-12 15:11:07 +02:00
// try to update
2022-02-24 09:24:56 +01:00
var rowCount = updateCommand . IsNullOrWhiteSpace ( ) | | updateArgs is null
2016-04-12 15:11:07 +02:00
? db . Update ( poco )
2022-02-24 09:24:56 +01:00
: db . Update < T > ( updateCommand ! , updateArgs ) ;
2016-04-12 15:11:07 +02:00
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 ;
}
2016-06-08 09:59:41 +02:00
catch ( SqlException ) // assuming all db engines will throw that exception
2016-04-12 15:11:07 +02:00
{
// failed: exists (due to race cond RC1)
// RC2 race cond here: another thread may remove the record
// try to update
2022-02-24 09:24:56 +01:00
rowCount = updateCommand . IsNullOrWhiteSpace ( ) | | updateArgs is null
2016-04-12 15:11:07 +02:00
? db . Update ( poco )
2022-02-24 09:24:56 +01:00
: db . Update < T > ( updateCommand ! , updateArgs ) ;
2016-04-12 15:11:07 +02:00
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." ) ;
}
/// <summary>
/// This will escape single @ symbols for npoco values so it doesn't think it's a parameter
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string EscapeAtSymbols ( string value )
{
2016-06-08 09:59:41 +02:00
if ( value . Contains ( "@" ) = = false ) return value ;
2016-04-12 15:11:07 +02:00
2016-06-08 09:59:41 +02:00
//this fancy regex will only match a single @ not a double, etc...
var regex = new Regex ( "(?<!@)@(?!@)" ) ;
return regex . Replace ( value , "@@" ) ;
2016-04-12 15:11:07 +02:00
}
2016-11-04 18:40:42 +01:00
/// <summary>
/// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff
/// </summary>
/// <typeparam name="TConnection"></typeparam>
/// <param name="connection"></param>
/// <returns></returns>
2019-12-12 12:55:17 +01:00
public static TConnection GetTypedConnection < TConnection > ( IDbConnection connection )
2016-11-04 18:40:42 +01:00
where TConnection : class , IDbConnection
{
2019-02-21 11:17:17 +01:00
var c = connection ;
for ( ; ; )
{
switch ( c )
{
case TConnection ofType :
return ofType ;
case RetryDbConnection retry :
c = retry . Inner ;
break ;
case ProfiledDbConnection profiled :
c = profiled . WrappedConnection ;
break ;
default :
throw new NotSupportedException ( connection . GetType ( ) . FullName ) ;
}
}
2016-11-04 18:40:42 +01:00
}
/// <summary>
/// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff
/// </summary>
/// <typeparam name="TTransaction"></typeparam>
/// <param name="transaction"></param>
/// <returns></returns>
2022-02-24 09:24:56 +01:00
public static TTransaction GetTypedTransaction < TTransaction > ( IDbTransaction ? transaction )
2016-11-04 18:40:42 +01:00
where TTransaction : class , IDbTransaction
{
2019-02-21 11:17:17 +01:00
var t = transaction ;
for ( ; ; )
{
switch ( t )
{
case TTransaction ofType :
return ofType ;
case ProfiledDbTransaction profiled :
t = profiled . WrappedTransaction ;
break ;
default :
2022-02-24 09:24:56 +01:00
throw new NotSupportedException ( transaction ? . GetType ( ) . FullName ) ;
2019-02-21 11:17:17 +01:00
}
}
2016-11-04 18:40:42 +01:00
}
/// <summary>
/// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <param name="command"></param>
/// <returns></returns>
2019-12-12 08:11:23 +01:00
public static TCommand GetTypedCommand < TCommand > ( IDbCommand command )
2016-11-04 18:40:42 +01:00
where TCommand : class , IDbCommand
{
2019-02-21 11:17:17 +01:00
var c = command ;
for ( ; ; )
{
switch ( c )
{
case TCommand ofType :
return ofType ;
case FaultHandlingDbCommand faultHandling :
c = faultHandling . Inner ;
break ;
case ProfiledDbCommand profiled :
c = profiled . InternalCommand ;
break ;
default :
throw new NotSupportedException ( command . GetType ( ) . FullName ) ;
}
}
2016-11-04 18:40:42 +01:00
}
2016-04-12 19:55:50 +02:00
public static void TruncateTable ( this IDatabase db , ISqlSyntaxProvider sqlSyntax , string tableName )
2016-04-12 15:11:07 +02:00
{
var sql = new Sql ( string . Format (
sqlSyntax . TruncateTable ,
sqlSyntax . GetQuotedTableName ( tableName ) ) ) ;
db . Execute ( sql ) ;
}
2016-04-12 19:55:50 +02:00
public static IsolationLevel GetCurrentTransactionIsolationLevel ( this IDatabase database )
2016-04-12 15:11:07 +02:00
{
var transaction = database . Transaction ;
2016-06-08 09:59:41 +02:00
return transaction ? . IsolationLevel ? ? IsolationLevel . Unspecified ;
2016-04-12 15:11:07 +02:00
}
2016-04-12 19:55:50 +02:00
2017-09-22 18:48:58 +02:00
public static IEnumerable < TResult > FetchByGroups < TResult , TSource > ( this IDatabase db , IEnumerable < TSource > source , int groupSize , Func < IEnumerable < TSource > , Sql < ISqlContext > > sqlFactory )
2016-04-12 19:55:50 +02:00
{
return source . SelectByGroups ( x = > db . Fetch < TResult > ( sqlFactory ( x ) ) , groupSize ) ;
}
2016-04-12 15:11:07 +02:00
}
2017-07-20 11:21:28 +02:00
}