diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 955f7b6..bd22153 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -90,18 +90,40 @@ jobs: version=$(basename "$nupkg_file" | sed 's/^atlas-ef\.//' | sed 's/\.nupkg$//') dotnet tool install --add-source ./nupkg --version "$version" -g atlas-ef echo "Installed atlas-ef version: $version" + + - name: Test tool without context flag (all contexts) + shell: bash + working-directory: ./src/Atlas.Provider.Demo + run: | atlas-ef -- sqlite + + - name: Test tool with context flag + shell: bash + working-directory: ./src/Atlas.Provider.Demo + run: | + atlas-ef --context BloggingContext -- sqlite + - name: Install Atlas CLI uses: ariga/setup-atlas@v0 + - name: Verify Atlas CLI version shell: pwsh run: atlas version + - name: Check migrations for sqlite working-directory: ./src/Atlas.Provider.Demo run: | atlas migrate diff --env ef --var dialect=sqlite env: ATLAS_TOKEN: ${{ secrets.ATLAS_TOKEN }} + + - name: Check migrations for sqlite (with context) + working-directory: ./src/Atlas.Provider.Demo + run: | + atlas migrate diff --env ef --var dialect=sqlite --var context=BloggingContext + env: + ATLAS_TOKEN: ${{ secrets.ATLAS_TOKEN }} + - name: Verify migrations generated shell: bash run: | diff --git a/src/Atlas.Provider.Core/Options.cs b/src/Atlas.Provider.Core/Options.cs index 06a8b3f..b836fc2 100644 --- a/src/Atlas.Provider.Core/Options.cs +++ b/src/Atlas.Provider.Core/Options.cs @@ -15,6 +15,7 @@ public class Options public string Framework { get; set; } = string.Empty; public string WorkingDir { get; set; } = string.Empty; public bool Nullable { get; set; } = true; + public string? Context { get; set; } public List? PositionalArgs { get; set; } public Options(string[] args) @@ -50,6 +51,9 @@ public Options(string[] args) case "--working-dir": if (i + 1 < args.Length) WorkingDir = args[++i]; break; + case "--context": + if (i + 1 < args.Length) Context = args[++i]; + break; default: if (PositionalArgs == null) { diff --git a/src/Atlas.Provider.Core/Program.cs b/src/Atlas.Provider.Core/Program.cs index 8bfb94f..760c4bc 100644 --- a/src/Atlas.Provider.Core/Program.cs +++ b/src/Atlas.Provider.Core/Program.cs @@ -27,14 +27,19 @@ static int Main(string[] args) var types = executor.GetContextTypes(); foreach (var type in types) { - if (!type.Contains("Name") || type["Name"] == null) + if (type["Name"] is not string name || string.IsNullOrEmpty(name)) { continue; } - var name = type["Name"]!.ToString(); - if (string.IsNullOrEmpty(name)) + if (!string.IsNullOrEmpty(options.Context)) { - continue; + var fullName = type["FullName"] as string; + var matches = name.Equals(options.Context, StringComparison.OrdinalIgnoreCase) || + (fullName?.Equals(options.Context, StringComparison.OrdinalIgnoreCase) ?? false); + if (!matches) + { + continue; + } } var ctxInfo = executor.GetContextInfo(name); if (ctxInfo == null || !ctxInfo.Contains("ProviderName") || ctxInfo["ProviderName"] == null) diff --git a/src/Atlas.Provider.Demo/Program.cs b/src/Atlas.Provider.Demo/Program.cs index b9a1383..0ad0adb 100644 --- a/src/Atlas.Provider.Demo/Program.cs +++ b/src/Atlas.Provider.Demo/Program.cs @@ -88,4 +88,96 @@ public class AuditEntry { public string? Name { get; set; } } + + // Second DbContext for testing --context flag + public class ShopContextFactory : IDesignTimeDbContextFactory + { + public ShopContext CreateDbContext(string[] args) + { + var provider = args.FirstOrDefault(); + return new ShopContext(provider!); + } + } + + public class ShopContext : DbContext + { + public DbSet? Products { get; set; } + public DbSet? Categories { get; set; } + + private readonly string _provider; + public ShopContext(string provider = "SqlServer") + { + _provider = provider; + } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + switch (_provider.ToLower()) + { + case "sqlserver": + options.UseSqlServer("Server=localhost;Database=ShopDb;User Id=your_username;Password=your_password;"); + break; + case "sqlite": + options.UseSqlite("Data Source=shop.db;"); + break; + case "mysql": + options.UseMySql( + "Server=localhost;Database=ShopDb;User=root;Password=your_password;", + ServerVersion.Create(8, 0, 0, ServerType.MySql), + optionsBuilder => optionsBuilder + .DisableLineBreakToCharSubstition() + .SchemaBehavior(MySqlSchemaBehavior.Ignore) + ); + break; + case "mariadb": + options.UseMySql("Server=localhost;Database=ShopDb;User=root;Password=your_password;", ServerVersion.Create(8, 7, 0, ServerType.MariaDb)); + break; + case "postgres": + options.UseNpgsql("Host=localhost;Database=ShopDb;Username=your_username;Password=your_password;"); + break; + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .ToTable("Products", schema: "Shop"); + + modelBuilder.Entity() + .ToTable("Categories", schema: "Shop"); + + modelBuilder.Entity() + .HasOne(p => p.Category) + .WithMany(c => c.Products) + .HasForeignKey(p => p.CategoryId); + + modelBuilder.Entity() + .Property(p => p.Name) + .HasMaxLength(100) + .IsRequired(); + + modelBuilder.Entity() + .Property(c => c.Name) + .HasMaxLength(50) + .IsRequired(); + } + } + + public class Product + { + [Key] + public int ProductId { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public int CategoryId { get; set; } + public Category? Category { get; set; } + } + + public class Category + { + [Key] + public int CategoryId { get; set; } + public string Name { get; set; } = string.Empty; + public List? Products { get; set; } + } } \ No newline at end of file diff --git a/src/Atlas.Provider.Demo/atlas.hcl b/src/Atlas.Provider.Demo/atlas.hcl index 5a7516a..d4ccd34 100644 --- a/src/Atlas.Provider.Demo/atlas.hcl +++ b/src/Atlas.Provider.Demo/atlas.hcl @@ -2,6 +2,11 @@ variable "dialect" { type = string } +variable "context" { + type = string + default = "" +} + locals { dev_url = { mysql = "docker://mysql/8/dev" @@ -9,21 +14,31 @@ locals { sqlserver = "docker://sqlserver/2022-latest" sqlite = "sqlite://file::memory:?cache=shared" }[var.dialect] + + schema_url = var.context == "" ? data.external_schema.efcore.url : data.external_schema.efcore_context.url } data "external_schema" "efcore" { program = [ - "atlas-ef", # this is the global tool installed with `dotnet tool install -g atlas-ef` + "atlas-ef", + "--", var.dialect, + ] +} + +data "external_schema" "efcore_context" { + program = [ + "atlas-ef", + "--context", var.context, "--", var.dialect, ] } env { name = atlas.env - src = data.external_schema.efcore.url + src = local.schema_url dev = local.dev_url migration { - dir = "file://migrations/${var.dialect}" + dir = var.context == "" ? "file://migrations/${var.dialect}" : "file://migrations/${var.dialect}/${var.context}" } format { migrate { diff --git a/src/Atlas.Provider.Demo/migrations/sqlite/20251020170346.sql b/src/Atlas.Provider.Demo/migrations/sqlite/20251020170346.sql new file mode 100644 index 0000000..601fa3a --- /dev/null +++ b/src/Atlas.Provider.Demo/migrations/sqlite/20251020170346.sql @@ -0,0 +1,15 @@ +-- Create "Categories" table +CREATE TABLE `Categories` ( + `CategoryId` integer NOT NULL PRIMARY KEY AUTOINCREMENT, + `Name` text NOT NULL +); +-- Create "Products" table +CREATE TABLE `Products` ( + `ProductId` integer NOT NULL PRIMARY KEY AUTOINCREMENT, + `Name` text NOT NULL, + `Price` text NOT NULL, + `CategoryId` integer NOT NULL, + CONSTRAINT `FK_Products_Categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `Categories` (`CategoryId`) ON UPDATE NO ACTION ON DELETE CASCADE +); +-- Create index "IX_Products_CategoryId" to table: "Products" +CREATE INDEX `IX_Products_CategoryId` ON `Products` (`CategoryId`); diff --git a/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/20251020170414.sql b/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/20251020170414.sql new file mode 100644 index 0000000..10edd3b --- /dev/null +++ b/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/20251020170414.sql @@ -0,0 +1,25 @@ +-- Create "AuditEntry" table +CREATE TABLE `AuditEntry` ( + `Name` text NULL +); +-- Create "Blogs" table +CREATE TABLE `Blogs` ( + `BlogId` integer NOT NULL PRIMARY KEY AUTOINCREMENT, + `Url` varchar NOT NULL, + `Rating` decimal NOT NULL, + `Title` text NOT NULL, + `Content` text NOT NULL, + `Author` text NOT NULL DEFAULT 'Anonymous' +); +-- Create index "Blogs_Url" to table: "Blogs" +CREATE UNIQUE INDEX `Blogs_Url` ON `Blogs` (`Url`); +-- Create "Posts" table +CREATE TABLE `Posts` ( + `PostId` integer NOT NULL PRIMARY KEY AUTOINCREMENT, + `Title` text NOT NULL, + `Content` text NOT NULL, + `BlogUrl` varchar NULL, + CONSTRAINT `FK_Posts_Blogs_BlogUrl` FOREIGN KEY (`BlogUrl`) REFERENCES `Blogs` (`Url`) ON UPDATE NO ACTION ON DELETE NO ACTION +); +-- Create index "IX_Posts_BlogUrl" to table: "Posts" +CREATE INDEX `IX_Posts_BlogUrl` ON `Posts` (`BlogUrl`); diff --git a/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/atlas.sum b/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/atlas.sum new file mode 100644 index 0000000..fbe26d8 --- /dev/null +++ b/src/Atlas.Provider.Demo/migrations/sqlite/BloggingContext/atlas.sum @@ -0,0 +1,2 @@ +h1:E3wbsnjAwijo4Pr+g9s7DMxRjAANdowsKWueiqjKpBA= +20251020170414.sql h1:z6C10/qQ2Ww6etuF6ze91skUJ92VJrJPBkJhD//h88w= diff --git a/src/Atlas.Provider.Demo/migrations/sqlite/atlas.sum b/src/Atlas.Provider.Demo/migrations/sqlite/atlas.sum index 6a834b1..405dfd9 100644 --- a/src/Atlas.Provider.Demo/migrations/sqlite/atlas.sum +++ b/src/Atlas.Provider.Demo/migrations/sqlite/atlas.sum @@ -1,2 +1,3 @@ -h1:riBDPl8T28Skduf/aAlfc9tHI2xUjO3z6YXGSfgbmyA= +h1:ScFSKLRSo2fXRtSczrJja7Vyk797oT3/rlZKxPj8EiU= 20250603180536.sql h1:BF13Vo+fIA/JsBAx+hmym/3pf3wUPXepEZI5HfvyCv0= +20251020170346.sql h1:uJAb6aBsAxW23xMgH9faLQQ2JjurbUNnes4CHnsy0kA= diff --git a/src/Atlas.Provider.Loader/Program.cs b/src/Atlas.Provider.Loader/Program.cs index 1876e45..243b5e4 100644 --- a/src/Atlas.Provider.Loader/Program.cs +++ b/src/Atlas.Provider.Loader/Program.cs @@ -86,6 +86,11 @@ static int Main( "--startup-assembly", Path.Combine(targetDir, _startupProject.TargetFileName!), "--startup-project", startupProjectFile, ]); + if (!string.IsNullOrEmpty(context)) + { + arguments.Add("--context"); + arguments.Add(context); + } if (string.Equals(_project.Nullable, "enable", StringComparison.OrdinalIgnoreCase) || string.Equals(_project.Nullable, "annotations", StringComparison.OrdinalIgnoreCase)) { diff --git a/test/Atlas.Provider.Test/GenerateSchemaTest.cs b/test/Atlas.Provider.Test/GenerateSchemaTest.cs index e458454..191fc2e 100644 --- a/test/Atlas.Provider.Test/GenerateSchemaTest.cs +++ b/test/Atlas.Provider.Test/GenerateSchemaTest.cs @@ -41,7 +41,7 @@ public void Can_generate_script(string providerName, string expectedFile) { WorkingDirectory = demoProjectPath, FileName = "dotnet", - Arguments = $"exec {dllFileName} -- {providerName}", + Arguments = $"exec {dllFileName} --context BloggingContext -- {providerName}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false,