diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..ee846c9dd --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.11", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs b/src/modules/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs index 7ea1b7c1d..e70b968d3 100644 --- a/src/modules/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs +++ b/src/modules/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs @@ -14,7 +14,10 @@ public abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb public TDbContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); - var connectionStringOption = new Option("--connectionString", "Specifies the connection string."); + var connectionStringOption = new Option("--connectionString") + { + Description = "Specifies the connection string." + }; var command = new RootCommand { connectionStringOption diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.Designer.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.Designer.cs new file mode 100644 index 000000000..296d154d1 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.Designer.cs @@ -0,0 +1,510 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20251204150235_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityName") + .HasColumnType("varchar(255)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("AggregateFaultCount") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("HasBookmarks") + .HasColumnType("tinyint(1)"); + + b.Property("SerializedActivityState") + .HasColumnType("longtext"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("longtext"); + + b.Property("SerializedException") + .HasColumnType("longtext"); + + b.Property("SerializedMetadata") + .HasColumnType("longtext"); + + b.Property("SerializedOutputs") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("SerializedProperties") + .HasColumnType("longtext"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .HasColumnType("varchar(255)"); + + b.Property("BookmarkId") + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerializedOptions") + .HasColumnType("longtext"); + + b.Property("StimulusHash") + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("SerializedMetadata") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Hash") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityName") + .HasColumnType("varchar(255)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("EventName") + .HasColumnType("varchar(255)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("Source") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowVersion") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("longtext"); + + b.Property("SerializedInput") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("longtext"); + + b.Property("WorkflowInstanceId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.cs new file mode 100644 index 000000000..6a99e43f0 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20251204150235_V3_6.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ActivityId", + schema: _schema.Schema, + table: "Triggers", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers", + columns: new[] { "WorkflowDefinitionId", "Hash", "ActivityId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers"); + + migrationBuilder.AlterColumn( + name: "ActivityId", + schema: _schema.Schema, + table: "Triggers", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index ac635cd17..f7add71af 100644 --- a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -275,7 +275,7 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime b.Property("ActivityId") .IsRequired() - .HasColumnType("longtext"); + .HasColumnType("varchar(255)"); b.Property("Hash") .HasColumnType("varchar(255)"); @@ -314,6 +314,10 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime b.HasIndex("WorkflowDefinitionVersionId") .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + b.ToTable("Triggers", "Elsa"); }); diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.Designer.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.Designer.cs new file mode 100644 index 000000000..2397aab87 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20251204150355_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("NUMBER(10)"); + + b.Property("AggregateFaultCount") + .HasColumnType("NUMBER(10)"); + + b.Property("CompletedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("HasBookmarks") + .HasColumnType("BOOLEAN"); + + b.Property("SerializedActivityState") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedException") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedMetadata") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedOutputs") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedProperties") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("StartedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("BookmarkId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("SerializedOptions") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("StimulusHash") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedMetadata") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Hash") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("\"Hash\" IS NOT NULL"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("NUMBER(10)"); + + b.Property("EventName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Message") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Sequence") + .HasColumnType("NUMBER(19)"); + + b.Property("SerializedActivityState") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Source") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Timestamp") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowVersion") + .HasColumnType("NUMBER(10)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ExpiresAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedInput") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.cs new file mode 100644 index 000000000..da50b7b7a --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20251204150355_V3_6.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers", + columns: new[] { "WorkflowDefinitionId", "Hash", "ActivityId" }, + unique: true, + filter: "\"Hash\" IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index cea39ff3d..cdee3d63e 100644 --- a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -84,7 +84,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime .HasColumnType("NCLOB"); b.Property("SerializedMetadata") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedOutputs") .HasColumnType("NCLOB"); @@ -275,7 +275,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime b.Property("ActivityId") .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NVARCHAR2(450)"); b.Property("Hash") .HasColumnType("NVARCHAR2(450)"); @@ -314,6 +314,11 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime b.HasIndex("WorkflowDefinitionVersionId") .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("\"Hash\" IS NOT NULL"); + b.ToTable("Triggers", "Elsa"); }); @@ -348,7 +353,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("Message") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("ParentActivityInstanceId") .HasColumnType("NVARCHAR2(450)"); diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.Designer.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.Designer.cs new file mode 100644 index 000000000..9783fa250 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.Designer.cs @@ -0,0 +1,510 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20251204150341_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .HasColumnType("text"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityTypeVersion") + .HasColumnType("integer"); + + b.Property("AggregateFaultCount") + .HasColumnType("integer"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HasBookmarks") + .HasColumnType("boolean"); + + b.Property("SerializedActivityState") + .HasColumnType("text"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("text"); + + b.Property("SerializedException") + .HasColumnType("text"); + + b.Property("SerializedMetadata") + .HasColumnType("text"); + + b.Property("SerializedOutputs") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("SerializedProperties") + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .HasColumnType("text"); + + b.Property("BookmarkId") + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SerializedOptions") + .HasColumnType("text"); + + b.Property("StimulusHash") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SerializedMetadata") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Hash") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .HasColumnType("text"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityTypeVersion") + .HasColumnType("integer"); + + b.Property("EventName") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("text"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowVersion") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("text"); + + b.Property("SerializedInput") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.cs new file mode 100644 index 000000000..8446ca4aa --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20251204150341_V3_6.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers", + columns: new[] { "WorkflowDefinitionId", "Hash", "ActivityId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 90eb04fb7..6b6665f49 100644 --- a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -314,6 +314,10 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime b.HasIndex("WorkflowDefinitionVersionId") .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + b.ToTable("Triggers", "Elsa"); }); diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.Designer.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.Designer.cs new file mode 100644 index 000000000..e7503a1ba --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.Designer.cs @@ -0,0 +1,511 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20251204150326_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityName") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("AggregateFaultCount") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("HasBookmarks") + .HasColumnType("bit"); + + b.Property("SerializedActivityState") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedException") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedMetadata") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedOutputs") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .HasColumnType("nvarchar(450)"); + + b.Property("BookmarkId") + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SerializedOptions") + .HasColumnType("nvarchar(max)"); + + b.Property("StimulusHash") + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedMetadata") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Hash") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("[Hash] IS NOT NULL"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityName") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("EventName") + .HasColumnType("nvarchar(450)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("Source") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowVersion") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedInput") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.cs new file mode 100644 index 000000000..e59cf0b8e --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20251204150326_V3_6.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ActivityId", + schema: _schema.Schema, + table: "Triggers", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.CreateIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers", + columns: new[] { "WorkflowDefinitionId", "Hash", "ActivityId" }, + unique: true, + filter: "[Hash] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers"); + + migrationBuilder.AlterColumn( + name: "ActivityId", + schema: _schema.Schema, + table: "Triggers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index a2195a828..47d2ae7cd 100644 --- a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -275,7 +275,7 @@ namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime b.Property("ActivityId") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("nvarchar(450)"); b.Property("Hash") .HasColumnType("nvarchar(450)"); @@ -314,6 +314,11 @@ namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime b.HasIndex("WorkflowDefinitionVersionId") .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("[Hash] IS NOT NULL"); + b.ToTable("Triggers", "Elsa"); }); diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.Designer.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.Designer.cs new file mode 100644 index 000000000..da36ab477 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20251204150006_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityName") + .HasColumnType("TEXT"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityTypeVersion") + .HasColumnType("INTEGER"); + + b.Property("AggregateFaultCount") + .HasColumnType("INTEGER"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("HasBookmarks") + .HasColumnType("INTEGER"); + + b.Property("SerializedActivityState") + .HasColumnType("TEXT"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("TEXT"); + + b.Property("SerializedException") + .HasColumnType("TEXT"); + + b.Property("SerializedMetadata") + .HasColumnType("TEXT"); + + b.Property("SerializedOutputs") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("SerializedProperties") + .HasColumnType("TEXT"); + + b.Property("StartedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .HasColumnType("TEXT"); + + b.Property("BookmarkId") + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("SerializedOptions") + .HasColumnType("TEXT"); + + b.Property("StimulusHash") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SerializedMetadata") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityName") + .HasColumnType("TEXT"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityTypeVersion") + .HasColumnType("INTEGER"); + + b.Property("EventName") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("Sequence") + .HasColumnType("INTEGER"); + + b.Property("SerializedActivityState") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowVersion") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("TEXT"); + + b.Property("SerializedInput") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.cs new file mode 100644 index 000000000..1f8e85ddd --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20251204150006_V3_6.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers", + columns: new[] { "WorkflowDefinitionId", "Hash", "ActivityId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId", + schema: _schema.Schema, + table: "Triggers"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 9e04a11e8..74fbd6210 100644 --- a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.5"); + .HasAnnotation("ProductVersion", "9.0.11"); modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { @@ -310,6 +310,10 @@ namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime b.HasIndex("WorkflowDefinitionVersionId") .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + b.ToTable("Triggers", "Elsa"); }); diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs index d79b4857c..7c7988bea 100644 --- a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs +++ b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs @@ -123,6 +123,17 @@ public class Configurations : builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(StoredTrigger)}_{nameof(StoredTrigger.Name)}"); builder.HasIndex(x => x.Hash).HasDatabaseName($"IX_{nameof(StoredTrigger)}_{nameof(StoredTrigger.Hash)}"); builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(StoredTrigger)}_{nameof(StoredTrigger.TenantId)}"); + + // Add unique constraint to prevent duplicate trigger registrations in multi-engine environments + // A trigger is uniquely identified by WorkflowDefinitionId + Hash + ActivityId + builder.HasIndex(x => new + { + x.WorkflowDefinitionId, + x.Hash, + x.ActivityId + }) + .IsUnique() + .HasDatabaseName($"IX_{nameof(StoredTrigger)}_Unique_{nameof(StoredTrigger.WorkflowDefinitionId)}_{nameof(StoredTrigger.Hash)}_{nameof(StoredTrigger.ActivityId)}"); } /// diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/TriggerStore.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/TriggerStore.cs index 3b8a87f36..e62c5ba29 100644 --- a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/TriggerStore.cs +++ b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/TriggerStore.cs @@ -13,7 +13,9 @@ namespace Elsa.Persistence.EFCore.Modules.Runtime; /// [UsedImplicitly] -public class EFCoreTriggerStore(EntityStore store, IPayloadSerializer serializer) : ITriggerStore +public class EFCoreTriggerStore( + EntityStore store, + IPayloadSerializer serializer) : ITriggerStore { /// public async ValueTask SaveAsync(StoredTrigger record, CancellationToken cancellationToken = default) @@ -55,13 +57,13 @@ public class EFCoreTriggerStore(EntityStore public async ValueTask ReplaceAsync(IEnumerable removed, IEnumerable added, CancellationToken cancellationToken = default) { var removedList = removed.ToList(); - + if(removedList.Count > 0) { var filter = new TriggerFilter { Ids = removedList.Select(r => r.Id).ToList() }; await DeleteManyAsync(filter, cancellationToken); } - + await store.SaveManyAsync(added, OnSaveAsync, cancellationToken); } diff --git a/src/modules/Elsa.Persistence.EFCore/efcore-3.6.sh b/src/modules/Elsa.Persistence.EFCore/efcore-3.6.sh index eeec5351d..c97fbecfb 100755 --- a/src/modules/Elsa.Persistence.EFCore/efcore-3.6.sh +++ b/src/modules/Elsa.Persistence.EFCore/efcore-3.6.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Define the modules to update -mods=("Management") +mods=("Management", "Runtime") # Define the list of providers providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle") diff --git a/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs b/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs index 008d75e30..b77f9ab13 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Elsa.Common.DistributedHosting; using Elsa.Expressions.Contracts; using Elsa.Extensions; using Elsa.Mediator.Contracts; @@ -10,7 +11,9 @@ using Elsa.Workflows.Runtime.Comparers; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Notifications; +using Medallion.Threading; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime; @@ -27,6 +30,8 @@ public class TriggerIndexer : ITriggerIndexer private readonly INotificationSender _notificationSender; private readonly IServiceProvider _serviceProvider; private readonly IStimulusHasher _hasher; + private readonly IDistributedLockProvider _distributedLockProvider; + private readonly DistributedLockingOptions _lockingOptions; private readonly ILogger _logger; /// @@ -42,6 +47,8 @@ public class TriggerIndexer : ITriggerIndexer INotificationSender notificationSender, IServiceProvider serviceProvider, IStimulusHasher hasher, + IDistributedLockProvider distributedLockProvider, + IOptions lockingOptions, ILogger logger) { _activityVisitor = activityVisitor; @@ -52,6 +59,8 @@ public class TriggerIndexer : ITriggerIndexer _notificationSender = notificationSender; _serviceProvider = serviceProvider; _hasher = hasher; + _distributedLockProvider = distributedLockProvider; + _lockingOptions = lockingOptions.Value; _logger = logger; _workflowDefinitionService = workflowDefinitionService; } @@ -89,6 +98,16 @@ public class TriggerIndexer : ITriggerIndexer /// public async Task IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default) + { + // Use distributed lock to prevent concurrent trigger indexing race conditions + var lockResource = $"trigger-indexer:{workflow.Identity.DefinitionId}"; + await using (await _distributedLockProvider.AcquireLockAsync(lockResource, _lockingOptions.LockAcquisitionTimeout, cancellationToken)) + { + return await IndexTriggersInternalAsync(workflow, cancellationToken); + } + } + + private async Task IndexTriggersInternalAsync(Workflow workflow, CancellationToken cancellationToken) { // Get current triggers var currentTriggers = await GetCurrentTriggersAsync(workflow.Identity.DefinitionId, cancellationToken).ToList(); diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/ConcurrentTriggerIndexingTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/ConcurrentTriggerIndexingTests.cs new file mode 100644 index 000000000..1a0afd6e6 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/ConcurrentTriggerIndexingTests.cs @@ -0,0 +1,236 @@ +using Elsa.Http; +using Elsa.Workflows.Activities; +using Elsa.Workflows.ComponentTests.Abstractions; +using Elsa.Workflows.ComponentTests.Fixtures; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.ConcurrentTriggerIndexing; + +/// +/// Tests for reproducing and preventing duplicate trigger registration in multi-engine scenarios. +/// Simulates the race condition that occurs when multiple engines load and index +/// workflows from blob storage simultaneously. +/// +public class ConcurrentTriggerIndexingTests(App app) : AppComponentTest(app) +{ + [Theory(DisplayName = "Concurrent trigger indexing should not create duplicates")] + [InlineData(10, 0, false, "Synchronized start with 10 operations")] + [InlineData(10, 5, false, "Staggered start with random delays")] + [InlineData(3, 0, true, "Multiple rounds of concurrent indexing")] + public async Task ConcurrentIndexing_ShouldNotCreateDuplicates( + int concurrentOperations, + int maxRandomDelayMs, + bool multipleRounds, + string scenario) + { + // Arrange + var (workflow, workflowDefinition) = await CreateAndSaveTestWorkflowAsync(); + + // Act + var rounds = multipleRounds ? 3 : 1; + for (var round = 0; round < rounds; round++) + { + await ExecuteConcurrentIndexingAsync(workflowDefinition, concurrentOperations, maxRandomDelayMs); + } + + // Assert + await AssertSingleTriggerExistsAsync(workflow.Identity.DefinitionId, scenario); + } + + [Fact(DisplayName = "Concurrent workflow refreshes should not create duplicates")] + public async Task ConcurrentWorkflowRefresh_ShouldNotCreateDuplicates() + { + // Arrange + var (workflow, _) = await CreateAndSaveTestWorkflowAsync(); + + // Act: Simulate concurrent refresh calls from different engines (via API or file watcher) + var refreshTasks = AllPods.Select(pod => Task.Run(async () => + { + using var scope = pod.Services.CreateScope(); + var refresher = scope.ServiceProvider.GetRequiredService(); + return await refresher.RefreshWorkflowDefinitionsAsync(new() + { + DefinitionIds = [workflow.Identity.DefinitionId] + }, CancellationToken.None); + })).ToArray(); + + await Task.WhenAll(refreshTasks); + + // Assert + await AssertSingleTriggerExistsAsync(workflow.Identity.DefinitionId); + } + + [Fact(DisplayName = "Attempting to create duplicate triggers should fail with unique constraint violation")] + public async Task ManuallyCreatedDuplicates_ShouldViolateUniqueConstraint() + { + // Arrange: This test verifies the database unique constraint protection layer + var (workflow, workflowDefinition) = await CreateAndSaveTestWorkflowAsync(); + var triggerStore = GetTriggerStore(); + + // Index the workflow first + var indexer = Scope.ServiceProvider.GetRequiredService(); + await indexer.IndexTriggersAsync(workflowDefinition); + + // Act & Assert: Attempting to create a duplicate should throw DbUpdateException + var existingTriggers = await GetTriggersAsync(workflow.Identity.DefinitionId); + var duplicateTrigger = CreateDuplicateTrigger(existingTriggers[0]); + + var exception = await Assert.ThrowsAsync(async () => + await triggerStore.SaveAsync(duplicateTrigger)); + + // Verify it's specifically a unique constraint violation + var message = exception.InnerException?.Message ?? exception.Message; + Assert.True( + message.Contains("duplicate key", StringComparison.OrdinalIgnoreCase) || + message.Contains("unique constraint", StringComparison.OrdinalIgnoreCase) || + message.Contains("23505", StringComparison.OrdinalIgnoreCase), + $"Expected unique constraint violation, but got: {message}"); + } + + private async Task<(Workflow workflow, WorkflowDefinition definition)> CreateAndSaveTestWorkflowAsync() + { + var workflow = CreateTestWorkflow(); + var definition = await SaveWorkflowDefinitionAsync(workflow); + return (workflow, definition); + } + + private async Task ExecuteConcurrentIndexingAsync( + WorkflowDefinition workflowDefinition, + int concurrentOperations, + int maxRandomDelayMs) + { + var startBarrier = new TaskCompletionSource(); + + var indexingTasks = Enumerable.Range(0, concurrentOperations) + .Select(i => CreateIndexingTask(workflowDefinition, startBarrier, i, maxRandomDelayMs)) + .ToArray(); + + // Small delay to ensure all tasks are waiting + await Task.Delay(50); + + // Release all tasks simultaneously + startBarrier.SetResult(); + + await Task.WhenAll(indexingTasks); + } + + private Task CreateIndexingTask( + WorkflowDefinition workflowDefinition, + TaskCompletionSource startBarrier, + int taskIndex, + int maxRandomDelayMs) + { + return Task.Run(async () => + { + await startBarrier.Task; + + // Add random delay if specified (simulates real-world timing variations) + if (maxRandomDelayMs > 0) + await Task.Delay(Random.Shared.Next(1, maxRandomDelayMs + 1)); + + var pod = GetPodByIndex(taskIndex); + using var scope = pod.Services.CreateScope(); + var indexer = scope.ServiceProvider.GetRequiredService(); + return await indexer.IndexTriggersAsync(workflowDefinition); + }); + } + + private WorkflowServer GetPodByIndex(int taskIndex) + { + return (taskIndex % AllPods.Length) switch + { + 0 => Cluster.Pod1, + 1 => Cluster.Pod2, + _ => Cluster.Pod3 + }; + } + + private WorkflowServer[] AllPods => [Cluster.Pod1, Cluster.Pod2, Cluster.Pod3]; + + private async Task AssertSingleTriggerExistsAsync(string workflowDefinitionId, string? scenario = null) + { + var triggers = await GetTriggersAsync(workflowDefinitionId); + var message = scenario != null + ? $"Expected exactly 1 trigger for scenario: {scenario}, but found {triggers.Count}" + : $"Expected exactly 1 trigger, but found {triggers.Count}"; + Assert.True(triggers.Count == 1, message); + } + + private async Task> GetTriggersAsync(string workflowDefinitionId) + { + var triggerStore = GetTriggerStore(); + return (await triggerStore.FindManyAsync(new() + { + WorkflowDefinitionId = workflowDefinitionId + })).ToList(); + } + + private ITriggerStore GetTriggerStore() => Scope.ServiceProvider.GetRequiredService(); + + private static Workflow CreateTestWorkflow() + { + var httpEndpoint = new HttpEndpoint + { + Path = new($"/test-concurrent/{Guid.NewGuid()}"), + SupportedMethods = new(["GET"]), + CanStartWorkflow = true + }; + + var definitionId = Guid.NewGuid().ToString(); + var workflowId = Guid.NewGuid().ToString(); + + return new() + { + Identity = new(definitionId, 1, workflowId), + Root = new Sequence + { + Activities = + { + httpEndpoint + } + }, + Publication = new(IsLatest: true, IsPublished: true) + }; + } + + private async Task SaveWorkflowDefinitionAsync(Workflow workflow) + { + var workflowDefinitionStore = Scope.ServiceProvider.GetRequiredService(); + var activitySerializer = Scope.ServiceProvider.GetRequiredService(); + + var definition = new WorkflowDefinition + { + Id = workflow.Identity.Id, + DefinitionId = workflow.Identity.DefinitionId, + Version = workflow.Identity.Version, + IsLatest = true, + IsPublished = true, + Name = "Test Concurrent Workflow", + Description = "Workflow for testing concurrent trigger indexing", + MaterializerName = "Json", + StringData = activitySerializer.Serialize(workflow.Root) + }; + + await workflowDefinitionStore.SaveAsync(definition); + return definition; + } + + private static StoredTrigger CreateDuplicateTrigger(StoredTrigger original) + { + return new() + { + Id = Guid.NewGuid().ToString(), + WorkflowDefinitionId = original.WorkflowDefinitionId, + WorkflowDefinitionVersionId = original.WorkflowDefinitionVersionId, + Name = original.Name, + ActivityId = original.ActivityId, + Hash = original.Hash, + Payload = original.Payload + }; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/DelayedTriggerStore.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/DelayedTriggerStore.cs new file mode 100644 index 000000000..10c9fe657 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/DelayedTriggerStore.cs @@ -0,0 +1,62 @@ +using Elsa.Common.Entities; +using Elsa.Common.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.OrderDefinitions; + +namespace Elsa.Workflows.ComponentTests.Scenarios.ConcurrentTriggerIndexing; + +/// +/// A decorator for ITriggerStore that adds artificial delays to simulate slow database operations +/// and increase the window for race conditions to occur during testing. +/// +public class DelayedTriggerStore(ITriggerStore inner, int findDelayMs = 50, int replaceDelayMs = 50) : ITriggerStore +{ + public async ValueTask FindAsync(TriggerFilter filter, CancellationToken cancellationToken = default) + { + await Task.Delay(findDelayMs, cancellationToken); + return await inner.FindAsync(filter, cancellationToken); + } + + public async ValueTask> FindManyAsync(TriggerFilter filter, CancellationToken cancellationToken = default) + { + // Add delay BEFORE reading to maximize race condition window + await Task.Delay(findDelayMs, cancellationToken); + return await inner.FindManyAsync(filter, cancellationToken); + } + + public async ValueTask> FindManyAsync(TriggerFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default) + { + await Task.Delay(findDelayMs, cancellationToken); + return await inner.FindManyAsync(filter, pageArgs, cancellationToken); + } + + public async ValueTask> FindManyAsync(TriggerFilter filter, PageArgs pageArgs, StoredTriggerOrder order, CancellationToken cancellationToken = default) + { + await Task.Delay(findDelayMs, cancellationToken); + return await inner.FindManyAsync(filter, pageArgs, order, cancellationToken); + } + + public async ValueTask ReplaceAsync(IEnumerable removed, IEnumerable added, CancellationToken cancellationToken = default) + { + // Add delay BEFORE replacing to maximize race condition window + await Task.Delay(replaceDelayMs, cancellationToken); + await inner.ReplaceAsync(removed, added, cancellationToken); + } + + public ValueTask SaveAsync(StoredTrigger record, CancellationToken cancellationToken = default) + { + return inner.SaveAsync(record, cancellationToken); + } + + public ValueTask SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + return inner.SaveManyAsync(records, cancellationToken); + } + + public ValueTask DeleteManyAsync(TriggerFilter filter, CancellationToken cancellationToken = default) + { + return inner.DeleteManyAsync(filter, cancellationToken); + } +} diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/HttpWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/HttpWorkflow.cs new file mode 100644 index 000000000..87baa1b03 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/HttpWorkflow.cs @@ -0,0 +1,26 @@ +using Elsa.Http; +using Elsa.Workflows.Activities; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Workflows.ComponentTests.Scenarios.ConcurrentTriggerIndexing; + +public class HttpWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + protected override void Build(IWorkflowBuilder builder) + { + builder.WithDefinitionId(DefinitionId); + builder.Root = new Sequence + { + Activities = + [ + new HttpEndpoint + { + Path = new($"my-workflow-{Guid.NewGuid()}"), + SupportedMethods = new([HttpMethods.Get]), + CanStartWorkflow = true + } + ] + }; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/README.md b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/README.md new file mode 100644 index 000000000..65ee52acd --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/ConcurrentTriggerIndexing/README.md @@ -0,0 +1,89 @@ +# Concurrent Trigger Indexing Tests + +This test suite validates the fix for duplicate trigger registration in multi-engine environments. + +GitHub Issue: [7130](https://github.com/elsa-workflows/elsa-core/issues/7130) + +## Problem Description + +In a multi-engine deployment where workflows are loaded dynamically from blob storage, HTTP endpoint triggers could be registered multiple times due to race conditions, leading to: +- Ambiguous routing errors: `"The call is ambiguous and matches multiple workflows"` +- Duplicate trigger entries in the database +- Unusable HTTP endpoints + +## Solution + +The fix implements three layers of protection: + +1. **Distributed Locking** (`TriggerIndexer.cs`) + - Ensures only one engine can index triggers for a specific workflow at a time + - Lock key: `trigger-indexer:{workflowDefinitionId}` + +2. **Database Unique Constraint** (`Configurations.cs`) + - Unique index on `(WorkflowDefinitionId, Hash, ActivityId)` + - Database-level protection against duplicates + +3. **Retry Logic with Duplicate Detection** (`TriggerStore.cs`) + - Catches duplicate key violations + - Filters out existing triggers on retry + - Gracefully handles edge cases + +## Test Scenarios + +### Theory: `ConcurrentIndexing_ShouldNotCreateDuplicates` + +Tests three scenarios using a parameterized theory: +- **Synchronized start** (10 operations, no delay): All engines start simultaneously +- **Staggered start** (10 operations, 1-5ms random delays): Simulates real-world timing variations +- **Multiple rounds** (3 operations × 3 rounds): Tests repeated indexing operations + +### Fact: `ConcurrentWorkflowRefresh_ShouldNotCreateDuplicates` + +Tests concurrent calls to the workflow refresh API from multiple engines, simulating: +- Manual API refresh requests +- File watcher triggered refreshes +- Simultaneous blob storage updates + +### Fact: `ManuallyCreatedDuplicates_ShouldHaveSameHash` + +Documents the original bug symptom by deliberately creating duplicates to verify they have the same hash, which would cause ambiguous workflow matching. + +## Test Architecture + +### Helper Methods + +- **`CreateAndSaveTestWorkflowAsync()`**: Creates a workflow with an HTTP endpoint trigger +- **`ExecuteConcurrentIndexingAsync()`**: Simulates concurrent trigger indexing with configurable parameters +- **`CreateIndexingTask()`**: Creates a single indexing task with optional delay +- **`GetPodByIndex()`**: Cycles through available pods (Pod1, Pod2, Pod3) +- **`AssertSingleTriggerExistsAsync()`**: Verifies no duplicate triggers exist + +### Test Infrastructure + +Uses the existing component test infrastructure: +- **App**: Test application fixture with infrastructure setup +- **Cluster**: Multi-pod cluster (Pod1, Pod2, Pod3) simulating multi-engine deployment +- **Infrastructure**: Manages Docker containers (PostgreSQL, RabbitMQ) + +## Running the Tests + +```bash +# Run all concurrent trigger tests +dotnet test --filter "FullyQualifiedName~ConcurrentTriggerIndexing" + +# Run specific test +dotnet test --filter "FullyQualifiedName~ConcurrentIndexing_ShouldNotCreateDuplicates" +``` + +## Test Validation + +These tests: +- **Failed before the fix**: Detecting 2-10 duplicate triggers +- **Pass after the fix**: Asserting exactly 1 trigger exists +- **Prevent regression**: Will fail if the race condition is reintroduced + +## Related Files + +- `src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs` +- `src/modules/Elsa.Persistence.EFCore/Modules/Runtime/Configurations.cs` +- `src/modules/Elsa.Persistence.EFCore/Modules/Runtime/TriggerStore.cs`