2016-04-12 15:11:07 +02:00
|
|
|
using System.Data;
|
2022-06-02 08:18:31 +02:00
|
|
|
using System.Data.Common;
|
2016-04-12 15:11:07 +02:00
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
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
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
namespace Umbraco.Extensions;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Provides extension methods to NPoco Database class.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static partial class NPocoDatabaseExtensions
|
2016-04-12 15:11:07 +02:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
2022-06-02 08:18:31 +02:00
|
|
|
/// Iterates over the result of a paged data set with a db reader
|
2016-04-12 15:11:07 +02:00
|
|
|
/// </summary>
|
2022-06-02 08:18:31 +02:00
|
|
|
/// <typeparam name="T"></typeparam>
|
|
|
|
|
/// <param name="database"></param>
|
|
|
|
|
/// <param name="pageSize">
|
|
|
|
|
/// The number of rows to load per page
|
|
|
|
|
/// </param>
|
|
|
|
|
/// <param name="sql"></param>
|
|
|
|
|
/// <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>
|
|
|
|
|
/// <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, Sql? sqlCount)
|
2016-04-12 15:11:07 +02:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
var sqlString = sql.SQL;
|
|
|
|
|
var sqlArgs = sql.Arguments;
|
|
|
|
|
|
|
|
|
|
int? itemCount = null;
|
|
|
|
|
long pageIndex = 0;
|
|
|
|
|
do
|
2020-04-17 00:47:26 +10:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
// Get the paged queries
|
|
|
|
|
database.BuildPageQueries<T>(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage);
|
2020-04-17 00:47:26 +10:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// get the item count once
|
|
|
|
|
if (itemCount == null)
|
2020-04-17 00:47:26 +10:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
itemCount = database.ExecuteScalar<int>(
|
|
|
|
|
sqlCount?.SQL ?? generatedSqlCount,
|
|
|
|
|
sqlCount?.Arguments ?? sqlArgs);
|
|
|
|
|
}
|
2020-04-17 00:47:26 +10:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
pageIndex++;
|
2020-04-17 00:47:26 +10:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// iterate over rows without allocating all items to memory (Query vs Fetch)
|
|
|
|
|
foreach (T row in database.Query<T>(sqlPage, sqlArgs))
|
|
|
|
|
{
|
|
|
|
|
yield return row;
|
|
|
|
|
}
|
2020-04-17 00:47:26 +10:00
|
|
|
}
|
2022-06-02 08:18:31 +02:00
|
|
|
while (pageIndex * pageSize < itemCount);
|
|
|
|
|
}
|
2020-04-17 00:47:26 +10:00
|
|
|
|
2022-06-02 08:18:31 +02: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);
|
2021-06-24 09:43:57 -06:00
|
|
|
|
2022-06-02 08:18:31 +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
|
|
|
|
|
//
|
|
|
|
|
// TODO: use the proper database syntax, not this kludge
|
2016-04-12 15:11:07 +02:00
|
|
|
|
2022-06-02 08:18:31 +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>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// We cannot rely on database-specific options 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.
|
|
|
|
|
/// </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>
|
2024-09-10 00:49:18 +09:00
|
|
|
[Obsolete("Use InsertOrUpdateAsync instead")]
|
2022-06-02 08:18:31 +02:00
|
|
|
public static RecordPersistenceType InsertOrUpdate<T>(this IUmbracoDatabase db, T poco)
|
|
|
|
|
where T : class =>
|
|
|
|
|
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>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// We cannot rely on database-specific options 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.
|
|
|
|
|
/// </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>
|
2024-09-10 00:49:18 +09:00
|
|
|
public static async Task<RecordPersistenceType> InsertOrUpdateAsync<T>(
|
2022-06-02 08:18:31 +02:00
|
|
|
this IUmbracoDatabase db,
|
|
|
|
|
T poco,
|
|
|
|
|
string? updateCommand,
|
|
|
|
|
object? updateArgs)
|
|
|
|
|
where T : class
|
|
|
|
|
{
|
|
|
|
|
if (poco == null)
|
2016-04-12 15:11:07 +02:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
throw new ArgumentNullException(nameof(poco));
|
2016-04-12 15:11:07 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// TODO: 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() || updateArgs is null
|
2024-09-10 00:49:18 +09:00
|
|
|
? await db.UpdateAsync(poco)
|
2022-06-02 08:18:31 +02:00
|
|
|
: db.Update<T>(updateCommand!, updateArgs);
|
|
|
|
|
if (rowCount > 0)
|
2016-04-12 15:11:07 +02:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
return RecordPersistenceType.Update;
|
|
|
|
|
}
|
2016-04-12 15:11:07 +02:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// 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
|
2024-09-10 00:49:18 +09:00
|
|
|
await db.InsertAsync(poco);
|
2022-06-02 08:18:31 +02:00
|
|
|
return RecordPersistenceType.Insert;
|
|
|
|
|
}
|
2022-09-27 14:22:34 +02:00
|
|
|
catch (DbException)
|
2022-06-02 08:18:31 +02:00
|
|
|
{
|
|
|
|
|
// assuming all db engines will throw SQLException exception
|
|
|
|
|
// failed: exists (due to race cond RC1)
|
|
|
|
|
// RC2 race cond here: another thread may remove the record
|
2017-05-12 14:49:44 +02:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// try to update
|
|
|
|
|
rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null
|
2024-09-10 00:49:18 +09:00
|
|
|
? await db.UpdateAsync(poco)
|
2022-02-24 09:24:56 +01:00
|
|
|
: db.Update<T>(updateCommand!, updateArgs);
|
2022-06-02 08:18:31 +02:00
|
|
|
if (rowCount > 0)
|
2016-04-12 15:11:07 +02:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
return RecordPersistenceType.Update;
|
2016-04-12 15:11:07 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// failed: does not exist (due to race cond RC2), need to insert
|
|
|
|
|
// loop
|
2016-04-12 15:11:07 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// 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.");
|
|
|
|
|
}
|
2016-04-12 15:11:07 +02:00
|
|
|
|
2024-09-10 00:49:18 +09:00
|
|
|
public static RecordPersistenceType InsertOrUpdate<T>(
|
|
|
|
|
this IUmbracoDatabase db,
|
|
|
|
|
T poco,
|
|
|
|
|
string? updateCommand,
|
|
|
|
|
object? updateArgs)
|
|
|
|
|
where T : class =>
|
|
|
|
|
db.InsertOrUpdateAsync(poco, updateCommand, updateArgs).GetAwaiter().GetResult();
|
|
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
/// <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)
|
|
|
|
|
{
|
2025-09-28 23:40:50 +02:00
|
|
|
if (value.Contains('@') == false)
|
2022-06-02 08:18:31 +02:00
|
|
|
{
|
|
|
|
|
return value;
|
2016-04-12 15:11:07 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
// this fancy regex will only match a single @ not a double, etc...
|
2025-09-28 23:40:50 +02:00
|
|
|
return AtRegex().Replace(value, "@@");
|
2022-06-02 08:18:31 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 23:40:50 +02:00
|
|
|
[GeneratedRegex("(?<!@)@(?!@)")]
|
|
|
|
|
private static partial Regex AtRegex();
|
|
|
|
|
|
2022-06-02 08:18:31 +02: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>
|
2025-09-19 10:59:03 +02:00
|
|
|
public static TConnection GetTypedConnection<TConnection>(IDbConnection? connection)
|
2022-06-02 08:18:31 +02:00
|
|
|
where TConnection : class, IDbConnection
|
|
|
|
|
{
|
|
|
|
|
IDbConnection? c = connection;
|
|
|
|
|
for (; ;)
|
2016-11-04 18:40:42 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
switch (c)
|
2019-02-21 11:17:17 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
case TConnection ofType:
|
|
|
|
|
return ofType;
|
|
|
|
|
case RetryDbConnection retry:
|
|
|
|
|
c = retry.Inner;
|
|
|
|
|
break;
|
|
|
|
|
case ProfiledDbConnection profiled:
|
|
|
|
|
c = profiled.WrappedConnection;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
2025-09-19 10:59:03 +02:00
|
|
|
throw new NotSupportedException(connection?.GetType().FullName);
|
2019-02-21 11:17:17 +01:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
}
|
2022-06-02 08:18:31 +02:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
|
2022-06-02 08:18:31 +02: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>
|
|
|
|
|
public static TTransaction GetTypedTransaction<TTransaction>(IDbTransaction? transaction)
|
|
|
|
|
where TTransaction : class, IDbTransaction
|
|
|
|
|
{
|
|
|
|
|
IDbTransaction? t = transaction;
|
|
|
|
|
for (; ;)
|
2016-11-04 18:40:42 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
switch (t)
|
2019-02-21 11:17:17 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
case TTransaction ofType:
|
|
|
|
|
return ofType;
|
|
|
|
|
case ProfiledDbTransaction profiled:
|
|
|
|
|
t = profiled.WrappedTransaction;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new NotSupportedException(transaction?.GetType().FullName);
|
2019-02-21 11:17:17 +01:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
}
|
2022-06-02 08:18:31 +02:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
|
2022-06-02 08:18:31 +02: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>
|
|
|
|
|
public static TCommand GetTypedCommand<TCommand>(IDbCommand command)
|
|
|
|
|
where TCommand : class, IDbCommand
|
|
|
|
|
{
|
|
|
|
|
IDbCommand? c = command;
|
|
|
|
|
for (; ;)
|
2016-11-04 18:40:42 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
switch (c)
|
2019-02-21 11:17:17 +01:00
|
|
|
{
|
2022-06-02 08:18:31 +02:00
|
|
|
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);
|
2019-02-21 11:17:17 +01:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
}
|
2022-06-02 08:18:31 +02:00
|
|
|
}
|
2016-11-04 18:40:42 +01:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2016-04-12 19:55:50 +02:00
|
|
|
|
2022-06-02 08:18:31 +02:00
|
|
|
public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database)
|
|
|
|
|
{
|
|
|
|
|
DbTransaction? transaction = database.Transaction;
|
|
|
|
|
return transaction?.IsolationLevel ?? IsolationLevel.Unspecified;
|
2016-04-12 15:11:07 +02:00
|
|
|
}
|
2022-06-02 08:18:31 +02:00
|
|
|
|
|
|
|
|
public static IEnumerable<TResult> FetchByGroups<TResult, TSource>(this IDatabase db, IEnumerable<TSource> source, int groupSize, Func<IEnumerable<TSource>, Sql<ISqlContext>> sqlFactory) =>
|
|
|
|
|
source.SelectByGroups(x => db.Fetch<TResult>(sqlFactory(x)), groupSize);
|
2017-07-20 11:21:28 +02:00
|
|
|
}
|