diff --git a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.Designer.cs b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.Designer.cs similarity index 88% rename from samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.Designer.cs rename to samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.Designer.cs index 63a5ccb..d4965ac 100644 --- a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.Designer.cs +++ b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Atomizer.EFCore.Example.Data.MySql.Migrations { [DbContext(typeof(ExampleMySqlContext))] - [Migration("20260504195004_Initial")] + [Migration("20260505152241_Initial")] partial class Initial { /// @@ -60,6 +60,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", (string)null); }); @@ -133,6 +136,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", (string)null); }); @@ -186,10 +204,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("tinyint(1)"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("varchar(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -207,6 +221,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("datetime(6)"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("longtext"); @@ -242,7 +260,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", (string)null); }); diff --git a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.cs b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.cs similarity index 87% rename from samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.cs rename to samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.cs index 4dc5ffd..8a4cb7a 100644 --- a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260504195004_Initial.cs +++ b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/20260505152241_Initial.cs @@ -178,12 +178,54 @@ protected override void Up(MigrationBuilder migrationBuilder) ) .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateIndex( + name: "IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId", + table: "AtomizerActiveServers", + columns: new[] { "LastHeartbeatAt", "InstanceId" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerJobErrors_JobId", table: "AtomizerJobErrors", column: "JobId" ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_IdempotencyKey", + table: "AtomizerJobs", + column: "IdempotencyKey" + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "PartitionKey", "SequenceNumber" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "Attempts", "PartitionKey" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "ScheduledAt", "Id" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_Status_LeaseToken", + table: "AtomizerJobs", + columns: new[] { "Status", "LeaseToken" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerSchedules_Enabled_NextRunAt_Id", + table: "AtomizerSchedules", + columns: new[] { "Enabled", "NextRunAt", "Id" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerSchedules_JobKey", table: "AtomizerSchedules", diff --git a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/ExampleMySqlContextModelSnapshot.cs b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/ExampleMySqlContextModelSnapshot.cs index 21aa76e..12b87ee 100644 --- a/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/ExampleMySqlContextModelSnapshot.cs +++ b/samples/Atomizer.EFCore.Example/Data/MySql/Migrations/ExampleMySqlContextModelSnapshot.cs @@ -57,6 +57,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", (string)null); }); @@ -130,6 +133,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", (string)null); }); @@ -183,10 +201,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("tinyint(1)"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("varchar(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -204,6 +218,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("datetime(6)"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("longtext"); @@ -239,7 +257,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", (string)null); }); diff --git a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.Designer.cs b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.Designer.cs similarity index 88% rename from samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.Designer.cs rename to samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.Designer.cs index a8c62e7..124f5f1 100644 --- a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.Designer.cs +++ b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Atomizer.EFCore.Example.Data.Postgres.Migrations { [DbContext(typeof(ExamplePostgresContext))] - [Migration("20260504194951_Initial")] + [Migration("20260505152231_Initial")] partial class Initial { /// @@ -60,6 +60,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -133,6 +136,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -186,10 +204,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("boolean"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -207,6 +221,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("timestamp with time zone"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("text"); @@ -242,7 +260,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.cs b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.cs similarity index 83% rename from samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.cs rename to samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.cs index 0390093..1fad2fc 100644 --- a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260504194951_Initial.cs +++ b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/20260505152231_Initial.cs @@ -165,6 +165,13 @@ protected override void Up(MigrationBuilder migrationBuilder) } ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId", + schema: "Atomizer", + table: "AtomizerActiveServers", + columns: new[] { "LastHeartbeatAt", "InstanceId" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerJobErrors_JobId", schema: "Atomizer", @@ -172,6 +179,48 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "JobId" ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_IdempotencyKey", + schema: "Atomizer", + table: "AtomizerJobs", + column: "IdempotencyKey" + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "PartitionKey", "SequenceNumber" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "Attempts", "PartitionKey" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "ScheduledAt", "Id" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_Status_LeaseToken", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "Status", "LeaseToken" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerSchedules_Enabled_NextRunAt_Id", + schema: "Atomizer", + table: "AtomizerSchedules", + columns: new[] { "Enabled", "NextRunAt", "Id" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerSchedules_JobKey", schema: "Atomizer", diff --git a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/ExamplePostgresContextModelSnapshot.cs b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/ExamplePostgresContextModelSnapshot.cs index d50726f..c9e51b6 100644 --- a/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/ExamplePostgresContextModelSnapshot.cs +++ b/samples/Atomizer.EFCore.Example/Data/Postgres/Migrations/ExamplePostgresContextModelSnapshot.cs @@ -57,6 +57,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -130,6 +133,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -183,10 +201,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("boolean"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -204,6 +218,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("timestamp with time zone"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("text"); @@ -239,7 +257,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.Designer.cs b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.Designer.cs similarity index 88% rename from samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.Designer.cs rename to samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.Designer.cs index a2172ab..107f373 100644 --- a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.Designer.cs +++ b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace Atomizer.EFCore.Example.Data.SqlServer.Migrations { [DbContext(typeof(ExampleSqlServerContext))] - [Migration("20260504194956_Initial")] + [Migration("20260505152234_Initial")] partial class Initial { /// @@ -60,6 +60,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -133,6 +136,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -186,10 +204,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("bit"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -207,6 +221,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("datetimeoffset"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -242,7 +260,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.cs b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.cs similarity index 81% rename from samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.cs rename to samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.cs index 097f394..4057f6a 100644 --- a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260504194956_Initial.cs +++ b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/20260505152234_Initial.cs @@ -129,6 +129,13 @@ protected override void Up(MigrationBuilder migrationBuilder) } ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId", + schema: "Atomizer", + table: "AtomizerActiveServers", + columns: new[] { "LastHeartbeatAt", "InstanceId" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerJobErrors_JobId", schema: "Atomizer", @@ -136,6 +143,48 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "JobId" ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_IdempotencyKey", + schema: "Atomizer", + table: "AtomizerJobs", + column: "IdempotencyKey" + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "PartitionKey", "SequenceNumber" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "Attempts", "PartitionKey" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "ScheduledAt", "Id" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_Status_LeaseToken", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "Status", "LeaseToken" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerSchedules_Enabled_NextRunAt_Id", + schema: "Atomizer", + table: "AtomizerSchedules", + columns: new[] { "Enabled", "NextRunAt", "Id" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerSchedules_JobKey", schema: "Atomizer", diff --git a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/ExampleSqlServerContextModelSnapshot.cs b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/ExampleSqlServerContextModelSnapshot.cs index 9571a29..3b5d747 100644 --- a/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/ExampleSqlServerContextModelSnapshot.cs +++ b/samples/Atomizer.EFCore.Example/Data/SqlServer/Migrations/ExampleSqlServerContextModelSnapshot.cs @@ -57,6 +57,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -130,6 +133,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -183,10 +201,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("bit"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("nvarchar(255)"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -204,6 +218,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("datetimeoffset"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + b.Property("Payload") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -239,7 +257,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.Designer.cs b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.Designer.cs similarity index 87% rename from samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.Designer.cs rename to samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.Designer.cs index 3b15222..731b370 100644 --- a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.Designer.cs +++ b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.Designer.cs @@ -11,7 +11,7 @@ namespace Atomizer.EFCore.Example.Data.Sqlite.Migrations { [DbContext(typeof(ExampleSqliteContext))] - [Migration("20260504195000_Initial")] + [Migration("20260505152238_Initial")] partial class Initial { /// @@ -55,6 +55,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -128,6 +131,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -181,10 +199,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("INTEGER"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("TEXT"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -202,6 +216,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("TEXT"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("TEXT"); + b.Property("Payload") .IsRequired() .HasColumnType("TEXT"); @@ -237,7 +255,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.cs b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.cs similarity index 80% rename from samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.cs rename to samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.cs index ec391d1..aa4c37a 100644 --- a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260504195000_Initial.cs +++ b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/20260505152238_Initial.cs @@ -129,6 +129,13 @@ protected override void Up(MigrationBuilder migrationBuilder) } ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId", + schema: "Atomizer", + table: "AtomizerActiveServers", + columns: new[] { "LastHeartbeatAt", "InstanceId" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerJobErrors_JobId", schema: "Atomizer", @@ -136,6 +143,48 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "JobId" ); + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_IdempotencyKey", + schema: "Atomizer", + table: "AtomizerJobs", + column: "IdempotencyKey" + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "PartitionKey", "SequenceNumber" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "Attempts", "PartitionKey" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "QueueKey", "Status", "ScheduledAt", "Id" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerJobs_Status_LeaseToken", + schema: "Atomizer", + table: "AtomizerJobs", + columns: new[] { "Status", "LeaseToken" } + ); + + migrationBuilder.CreateIndex( + name: "IX_AtomizerSchedules_Enabled_NextRunAt_Id", + schema: "Atomizer", + table: "AtomizerSchedules", + columns: new[] { "Enabled", "NextRunAt", "Id" } + ); + migrationBuilder.CreateIndex( name: "IX_AtomizerSchedules_JobKey", schema: "Atomizer", diff --git a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/ExampleSqliteContextModelSnapshot.cs b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/ExampleSqliteContextModelSnapshot.cs index e0a32cf..191784d 100644 --- a/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/ExampleSqliteContextModelSnapshot.cs +++ b/samples/Atomizer.EFCore.Example/Data/Sqlite/Migrations/ExampleSqliteContextModelSnapshot.cs @@ -52,6 +52,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("InstanceId"); + b.HasIndex("LastHeartbeatAt", "InstanceId") + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); + b.ToTable("AtomizerActiveServers", "Atomizer"); }); @@ -125,6 +128,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("IdempotencyKey") + .HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + + b.HasIndex("Status", "LeaseToken") + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + + b.HasIndex("QueueKey", "PartitionKey", "SequenceNumber") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + + b.HasIndex("QueueKey", "Status", "Attempts", "PartitionKey") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + + b.HasIndex("QueueKey", "Status", "ScheduledAt", "Id") + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + b.ToTable("AtomizerJobs", "Atomizer"); }); @@ -178,10 +196,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Enabled") .HasColumnType("INTEGER"); - b.Property("PartitionKey") - .HasMaxLength(255) - .HasColumnType("TEXT"); - b.Property("JobKey") .IsRequired() .HasMaxLength(255) @@ -199,6 +213,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("NextRunAt") .HasColumnType("TEXT"); + b.Property("PartitionKey") + .HasMaxLength(255) + .HasColumnType("TEXT"); + b.Property("Payload") .IsRequired() .HasColumnType("TEXT"); @@ -234,7 +252,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("JobKey") - .IsUnique(); + .IsUnique() + .HasDatabaseName("IX_AtomizerSchedules_JobKey"); + + b.HasIndex("Enabled", "NextRunAt", "Id") + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); b.ToTable("AtomizerSchedules", "Atomizer"); }); diff --git a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerActiveServerEntityConfiguration.cs b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerActiveServerEntityConfiguration.cs index c4413ee..f5dac9d 100644 --- a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerActiveServerEntityConfiguration.cs +++ b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerActiveServerEntityConfiguration.cs @@ -28,5 +28,8 @@ public void Configure(EntityTypeBuilder builder) builder.HasKey(server => server.InstanceId); builder.Property(server => server.InstanceId).IsRequired().HasMaxLength(512).ValueGeneratedNever(); builder.Property(server => server.LastHeartbeatAt).IsRequired(); + builder + .HasIndex(server => new { server.LastHeartbeatAt, server.InstanceId }) + .HasDatabaseName("IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId"); } } diff --git a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs index 628e6f1..8553293 100644 --- a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs +++ b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs @@ -62,5 +62,36 @@ public void Configure(EntityTypeBuilder builder) ); builder.Property(job => job.PartitionKey).HasMaxLength(255).IsRequired(false); builder.Property(job => job.SequenceNumber).IsRequired(false); + + builder + .HasIndex(job => new + { + job.QueueKey, + job.Status, + job.ScheduledAt, + job.Id, + }) + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id"); + builder + .HasIndex(job => new + { + job.QueueKey, + job.PartitionKey, + job.SequenceNumber, + }) + .HasDatabaseName("IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber"); + builder + .HasIndex(job => new + { + job.QueueKey, + job.Status, + job.Attempts, + job.PartitionKey, + }) + .HasDatabaseName("IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey"); + builder + .HasIndex(job => new { job.Status, job.LeaseToken }) + .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); + builder.HasIndex(job => job.IdempotencyKey).HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); } } diff --git a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerScheduleEntityConfiguration.cs b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerScheduleEntityConfiguration.cs index 9667576..a547501 100644 --- a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerScheduleEntityConfiguration.cs +++ b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerScheduleEntityConfiguration.cs @@ -61,6 +61,14 @@ public void Configure(EntityTypeBuilder builder) ) ); - builder.HasIndex(e => e.JobKey).IsUnique(); + builder + .HasIndex(e => new + { + e.Enabled, + e.NextRunAt, + e.Id, + }) + .HasDatabaseName("IX_AtomizerSchedules_Enabled_NextRunAt_Id"); + builder.HasIndex(e => e.JobKey).IsUnique().HasDatabaseName("IX_AtomizerSchedules_JobKey"); } } diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Configurations/AtomizerEntityConfigurationTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Configurations/AtomizerEntityConfigurationTests.cs new file mode 100644 index 0000000..caec980 --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Configurations/AtomizerEntityConfigurationTests.cs @@ -0,0 +1,115 @@ +using Atomizer.EntityFrameworkCore.Entities; +using Atomizer.EntityFrameworkCore.Tests.TestSetup; +using AwesomeAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Atomizer.EntityFrameworkCore.Tests.Configurations; + +public sealed class AtomizerEntityConfigurationTests +{ + [Fact] + public void AddAtomizerEntities_WhenConfiguringJobEntity_ShouldCreateQueryIndexes() + { + using var dbContext = CreateDbContext(); + + AssertIndex( + dbContext, + "IX_AtomizerJobs_QueueKey_Status_ScheduledAt_Id", + [ + nameof(AtomizerJobEntity.QueueKey), + nameof(AtomizerJobEntity.Status), + nameof(AtomizerJobEntity.ScheduledAt), + nameof(AtomizerJobEntity.Id), + ] + ); + AssertIndex( + dbContext, + "IX_AtomizerJobs_QueueKey_PartitionKey_SequenceNumber", + [ + nameof(AtomizerJobEntity.QueueKey), + nameof(AtomizerJobEntity.PartitionKey), + nameof(AtomizerJobEntity.SequenceNumber), + ] + ); + AssertIndex( + dbContext, + "IX_AtomizerJobs_QueueKey_Status_Attempts_PartitionKey", + [ + nameof(AtomizerJobEntity.QueueKey), + nameof(AtomizerJobEntity.Status), + nameof(AtomizerJobEntity.Attempts), + nameof(AtomizerJobEntity.PartitionKey), + ] + ); + AssertIndex( + dbContext, + "IX_AtomizerJobs_Status_LeaseToken", + [nameof(AtomizerJobEntity.Status), nameof(AtomizerJobEntity.LeaseToken)] + ); + AssertIndex( + dbContext, + "IX_AtomizerJobs_IdempotencyKey", + [nameof(AtomizerJobEntity.IdempotencyKey)] + ); + } + + [Fact] + public void AddAtomizerEntities_WhenConfiguringScheduleEntity_ShouldCreateQueryIndexes() + { + using var dbContext = CreateDbContext(); + + AssertIndex( + dbContext, + "IX_AtomizerSchedules_Enabled_NextRunAt_Id", + [ + nameof(AtomizerScheduleEntity.Enabled), + nameof(AtomizerScheduleEntity.NextRunAt), + nameof(AtomizerScheduleEntity.Id), + ] + ); + AssertIndex( + dbContext, + "IX_AtomizerSchedules_JobKey", + [nameof(AtomizerScheduleEntity.JobKey)], + isUnique: true + ); + } + + [Fact] + public void AddAtomizerEntities_WhenConfiguringActiveServerEntity_ShouldCreateQueryIndexes() + { + using var dbContext = CreateDbContext(); + + AssertIndex( + dbContext, + "IX_AtomizerActiveServers_LastHeartbeatAt_InstanceId", + [nameof(AtomizerActiveServerEntity.LastHeartbeatAt), nameof(AtomizerActiveServerEntity.InstanceId)] + ); + } + + private static IndexModelDbContext CreateDbContext() + { + var options = new DbContextOptionsBuilder().UseSqlite("Data Source=:memory:").Options; + + return new IndexModelDbContext(options); + } + + private static void AssertIndex( + DbContext dbContext, + string indexName, + string[] propertyNames, + bool isUnique = false + ) + { + var entityType = dbContext.Model.FindEntityType(typeof(TEntity)); + entityType.Should().NotBeNull(); + + var index = entityType!.GetIndexes().SingleOrDefault(index => index.GetDatabaseName() == indexName); + index.Should().NotBeNull(); + index!.Properties.Select(property => property.Name).Should().Equal(propertyNames); + index.IsUnique.Should().Be(isUnique); + } + + private sealed class IndexModelDbContext(DbContextOptions options) : TestDbContext(options); +}