* fix(efcore-oracle): migrate LOB columns in V3_6 without in-place datatype alteration Both Oracle V3_6 migrations were generated as in-place `ALTER TABLE ... MODIFY` statements that change a column's datatype to or from a LOB type: NCLOB to JSON for `WorkflowDefinitions.StringData`, and NVARCHAR2(450) to NCLOB for `ActivityNodeId` on `WorkflowExecutionLogRecords` and `ActivityExecutionRecords`. Oracle refuses both (ORA-22858 / ORA-22859), so neither migration could ever apply and the reported ORA-22858 was unavoidable. Convert the columns the way the ORA-22858 message prescribes instead: add a temporary column of the target type, copy the values across, drop the original and rename the temporary one. Because Oracle commits DDL implicitly, a run that fails partway leaves its earlier statements applied - the reporter's already committed `OriginalSource` column is exactly that - so every step is guarded against the state a previous attempt can have left behind. The conversion block derives what still needs doing from `ALL_TAB_COLUMNS`, skips a conversion that already completed (including one applied by hand), and raises rather than copy out of and drop a column whose datatype it does not recognize. Add an offline regression test that generates the Oracle Management and Runtime V3_6 scripts through `IMigrator.GenerateScript` without a connection and asserts that no in-place datatype `MODIFY` is emitted for either column, that the add/copy/drop/rename sequence appears in order, and that the re-run guards are present. All 19 cases fail against the previous migrations. Refs #8011 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(efcore-oracle): tighten migration helper visibility and index guard, share the script-generation test harness Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(efcore-oracle): brace the foreach bodies in the V3_6 migration tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(efcore-oracle): refuse to truncate node IDs on downgrade and validate same-named indexes The Runtime V3_6 downgrade converted ActivityNodeId from NCLOB back to NVARCHAR2(450) by copying DBMS_LOB.SUBSTR(..., 450, 1), silently truncating any value the upgraded schema had allowed to grow past 450 characters. EnsureLobLengthAtMost now checks for oversized values while the column is still a LOB and raises before any data is copied. CreateIndexIfMissing also treated any index with a matching name as already done. It now validates the existing index's table, uniqueness and ordered column list against ALL_INDEXES/ALL_IND_COLUMNS so a same-named index left behind by schema drift or manual recovery is not mistaken for the one the migration means to create. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(efcore-oracle): correct the dynamic SQL rationale on the LOB length guard Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(efcore-oracle): preflight both tables before downgrading and escape schema names in migration SQL Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(efcore-oracle): copy every row NULL-preservingly so retries reproduce the current source The filtered copy `WHERE "<column>" IS NOT NULL` skipped rows whose nullable source had since become NULL. If a prior conversion committed the copy and then failed before dropping the source column, an operator clearing a value before retrying would find the predicate skip that row, and the stale converted value would be renamed into place. Replace the filtered UPDATE with an unconditional, NULL- preserving CASE expression so a retry always reproduces the current source exactly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
39 lines
1.9 KiB
C#
39 lines
1.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Elsa.Persistence.EFCore.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Generates a migration script offline, through <see cref="IMigrator.GenerateScript"/>, so tests can assert on the
|
|
/// SQL a migration produces without opening a database connection.
|
|
/// </summary>
|
|
internal static class MigrationScriptGenerator
|
|
{
|
|
/// <summary>
|
|
/// Builds a <typeparamref name="TDbContext"/> configured by <paramref name="configure"/> and generates the
|
|
/// migration script between <paramref name="fromMigration"/> and <paramref name="toMigration"/>.
|
|
/// </summary>
|
|
/// <param name="configure">Configures the provider (and, through it, the migrations assembly and schema) on the options builder.</param>
|
|
/// <param name="fromMigration">The migration to generate the script from, or <c>null</c> for the initial database state.</param>
|
|
/// <param name="toMigration">The migration to generate the script up to.</param>
|
|
/// <param name="options">The SQL generation options, for example whether the script is idempotent.</param>
|
|
public static string Generate<TDbContext>(
|
|
Action<DbContextOptionsBuilder<TDbContext>> configure,
|
|
string? fromMigration,
|
|
string toMigration,
|
|
MigrationsSqlGenerationOptions options)
|
|
where TDbContext : DbContext
|
|
{
|
|
var optionsBuilder = new DbContextOptionsBuilder<TDbContext>();
|
|
configure(optionsBuilder);
|
|
|
|
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
|
|
|
using var dbContext = (TDbContext)Activator.CreateInstance(typeof(TDbContext), optionsBuilder.Options, serviceProvider)!;
|
|
|
|
return dbContext.GetService<IMigrator>().GenerateScript(fromMigration: fromMigration, toMigration: toMigration, options: options);
|
|
}
|
|
}
|