Files
Umbraco-CMS/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs
Laura Neto b722c0d72d Abstract submit and poll operations (#19688)
* Started implementing new LongRunningOperationService and adjusting tasks to use this service

This service will manage operations that require status to be synced between servers (load balanced setup).

* Missing migration to add new lock. Other simplifications.

* Add job to cleanup the LongRunningOperations entries

* Add new DatabaseCacheRebuilder.RebuildAsync method

This is both async and returns an attempt, which will fail if a rebuild operation is already running.

* Missing LongRunningOperation database table creation on clean install

* Store expire date in the long running operation. Better handling of non-background operations.

Storing an expiration date allows setting different expiration times depending on the type of operation, and whether it is running in the background or not.

* Added integration tests for LongRunningOperationRepository

* Added unit tests for LongRunningOperationService

* Add type as a parameter to more repository calls. Distinguish between expiration and deletion in `LongRunningOperationRepository.CleanOperations`.

* Fix failing unit test

* Fixed `PerformPublishBranchAsync` result not being deserialized correctly

* Remove unnecessary DatabaseCacheRebuildResult value

* Add status to `LongRunningOperationService.GetResult` attempt to inform on why a result could not be retrieved

* General improvements

* Missing rename

* Improve the handling of long running operations that are not in background and stale operations

* Fix failing unit tests

* Fixed small mismatch between interface and implementation

* Use a fire and forget task instead of the background queue

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Make sure exceptions are caught when running in the background

* Alignment with other repositories (async + pagination)

* Additional fixes

* Add Async suffix to service methods

* Missing adjustment

* Moved hardcoded settings to IOptions

* Fix issue in SQL Server where 0 is not accepted as requested number of rows

* Fix issue in SQL Server where query provided to count cannot contain orderby

* Additional SQL Server fixes

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-07-22 15:26:04 +02:00

131 lines
4.2 KiB
C#

using System.Linq.Expressions;
using NPoco;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
using Umbraco.Cms.Infrastructure.Runtime;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence;
internal static class UmbracoDatabaseExtensions
{
public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database)
{
if (database is not UmbracoDatabase asDatabase)
{
throw new Exception("oops: database.");
}
return asDatabase;
}
/// <summary>
/// Gets a dictionary of key/values directly from the database, no scope, nothing.
/// </summary>
/// <remarks>Used by <see cref="RuntimeState" /> to determine the runtime state.</remarks>
public static IReadOnlyDictionary<string, string?>? GetFromKeyValueTable(
this IUmbracoDatabase? database,
string keyPrefix)
{
if (database is null)
{
return null;
}
// create the wildcard where clause
ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax;
var whereParam = sqlSyntax.GetStringColumnWildcardComparison(
sqlSyntax.GetQuotedColumnName("key"),
0,
TextColumnType.NVarchar);
Sql<ISqlContext>? sql = database.SqlContext.Sql()
.Select<KeyValueDto>()
.From<KeyValueDto>()
.Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder());
return database.Fetch<KeyValueDto>(sql)
.ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>
/// Returns true if the database contains the specified table
/// </summary>
/// <param name="database"></param>
/// <param name="tableName"></param>
/// <returns></returns>
public static bool HasTable(this IUmbracoDatabase database, string tableName)
{
try
{
return database.SqlContext.SqlSyntax.GetTablesInSchema(database)
.Any(table => table.InvariantEquals(tableName));
}
catch (Exception)
{
return false; // will occur if the database cannot connect
}
}
/// <summary>
/// Returns true if the database contains no tables
/// </summary>
/// <param name="database"></param>
/// <returns></returns>
public static bool IsDatabaseEmpty(this IUmbracoDatabase database)
=> database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false;
public static long Count(this IUmbracoDatabase database, Sql sql)
{
// We need to copy the sql into a new object, to avoid this method from changing the sql.
var query = new Sql().Select("COUNT(*)").From().Append("(").Append(new Sql(sql.SQL, sql.Arguments)).Append(") as count_query");
return database.ExecuteScalar<long>(query);
}
public static async Task<long> CountAsync(this IUmbracoDatabase database, Sql sql)
{
// We need to copy the sql into a new object, to avoid this method from changing the sql.
Sql query = new Sql().Select("COUNT(*)").From().Append("(").Append(new Sql(sql.SQL, sql.Arguments)).Append(") as count_query");
return await database.ExecuteScalarAsync<long>(query);
}
public static async Task<PagedModel<TResult>> PagedAsync<TDto, TResult>(
this IUmbracoDatabase database,
Sql<ISqlContext> sql,
int skip,
int take,
Action<Sql<ISqlContext>> sortingAction,
Func<TDto, TResult> mapper)
{
ArgumentOutOfRangeException.ThrowIfLessThan(skip, 0, nameof(skip));
ArgumentOutOfRangeException.ThrowIfLessThan(take, 0, nameof(take));
var count = await database.CountAsync(sql);
if (take == 0 || skip >= count)
{
return new PagedModel<TResult>
{
Total = count,
Items = [],
};
}
sortingAction(sql);
List<TDto> results = await database.SkipTakeAsync<TDto>(
skip,
take,
sql);
return new PagedModel<TResult>
{
Total = count,
Items = results.Select(mapper),
};
}
}