Skip to content

Commit

Permalink
Scaffolding fixes (ralmsdeveloper#35)
Browse files Browse the repository at this point in the history
* fix FbDesignTimeServices for use with ef context scaffolding

* use same namespace in FbDesignTimeServices  as everywhere else
* add FbOptions to serviceCollection in FbDesignTimeServices

* Fix type mapping for FB version < 3 with dialect version >= 3

* Add scaffolding test for testing EF DbContext scaffold

* scaffolding: better context generation

* handle indices without fields
* handle tables without PK
* fix nullable columns (NULL switched with NOT NULL)
* better foreign key detection

* scaffolding: add scaffold.ps1 for manual scaffolding testing

* fix failing unit test

After changing Book<->Author relation to many-to-many, inserted items count changed.

* use different db file in ScaffoldingTest to avoid file access errors

xunit allows different test classes to run in parallel, which caused
file access errors on TestContext
  • Loading branch information
qbikez authored and ralmsdeveloper committed Jun 24, 2018
1 parent 047ee62 commit ddb1ba9
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 29 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,6 @@ paket-files/
__pycache__/
*.pyc
*.FDB

# EF scaffolding test result
EFCore.FirerbirdSql.ScaffoldTest/Scaffolded/
139 changes: 139 additions & 0 deletions EFCore.FirebirdSql.FunctionalTests/ScaffoldingTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2017-2018 Rafael Almeida ([email protected])
*
* EntityFrameworkCore.FirebirdSql
*
* THIS MATERIAL IS PROVIDED AS IS, WITH ABSOLUTELY NO WARRANTY EXPRESSED
* OR IMPLIED. ANY USE IS AT YOUR OWN RISK.
*
* Permission is hereby granted to use or copy this program
* for any purpose, provided the above notices are retained on all copies.
* Permission to modify the code and to distribute modified code is granted,
* provided the above notices are retained, and a notice that the code was
* modified is included with the above copyright notice.
*
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Xunit;
using FB = FirebirdSql.Data.FirebirdClient;

namespace EFCore.FirebirdSql.FunctionalTests
{
public class ScaffoldingTest
{
private TestContext CreateContext() => new TestContext("ScaffoldingSample.fdb");
private string WorkingDir => Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "EFCore.FirerbirdSql.ScaffoldTest");
private string ClassDir => Path.Combine(WorkingDir, "Scaffolded");

private int RunEfScaffold(string connStr)
{
if (Directory.Exists(ClassDir))Directory.Delete(ClassDir, recursive : true);

var cmd = $"ef dbcontext scaffold {connStr} \"EntityFrameworkCore.FirebirdSQL\" -o {ClassDir} -c TestContext --force";
var p = new Process()
{
StartInfo = new ProcessStartInfo("dotnet", cmd)
{
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = WorkingDir
}
};
p.Start();
p.WaitForExit();

var errStr = p.StandardError.ReadToEnd();
var outStr = p.StandardOutput.ReadToEnd();

System.Console.WriteLine(outStr);
System.Console.WriteLine(errStr);

return p.ExitCode;
}

private FileContent GetEntityMap<T>()=> GetEntityMap(typeof(T).Name);
private FileContent GetEntityMap(string className)
{
var ctxFile = File.ReadAllLines(Path.Combine(ClassDir, "TestContext.cs"));

var entityMap = new List<string>();
int? startIdx = null;
for (int i = 0; i < ctxFile.Length; i++)
{
var line = ctxFile[i];

if (line.Contains($"modelBuilder.Entity<{className}>"))
{
startIdx = i;
}
if (startIdx != null)
{
entityMap.Add(line);
}
if (startIdx != null && i > startIdx && line.Contains("modelBuilder.Entity"))
{
break;
}
}

if (entityMap.Count == 0)
{
throw new Exception($"Entity mapping for class '{className}' not found.");
}
return new FileContent(entityMap);
}

private FileContent GetEntity<T>() => GetEntity(typeof(T).Name);
private FileContent GetEntity(string className) => new FileContent(File.ReadAllLines(Path.Combine(ClassDir, $"{className}.cs")));

[Fact]
public void scaffold_db()
{
string connStr;
using(var context = CreateContext())
{
connStr = context.Database.GetDbConnection().ConnectionString;
context.Database.EnsureDeleted();
context.Database.EnsureCreated();

// commputed index will have NULL as RBS$FIELD_NAME
context.Database.ExecuteSqlCommand(@"CREATE INDEX ""IX_AUTHOR_COMPUTED"" ON ""Author"" COMPUTED BY (""AuthorId"")");
// tables with no primary key are valid
context.Database.ExecuteSqlCommand(@"CREATE TABLE CourseTemplate(Tile varchar(100))");
}

var scaffoldExitCode = RunEfScaffold(connStr);

Assert.Equal(0, scaffoldExitCode);
Assert.True(Directory.Exists(ClassDir));

GetEntityMap<BookAuthor>().ShouldContain(new Regex(@"HasKey\(e => new { (e.BookId|e.AuthorId), (e.BookId|e.AuthorId) }\)"), "BookAuthor should have composite key");
GetEntity<BookAuthor>().ShouldNotContain("long? BookId");
GetEntity<BookAuthor>().ShouldContain("public long BookId");
GetEntity<Author>().ShouldContain("public byte[] TestBytes", "byte array should be properly mapped");
//GetEntity("CourseTemplate").ShouldContain("public string Title");
}

class FileContent
{
public IEnumerable<string> Lines { get; set; }

public FileContent(IEnumerable<string> lines)
{
this.Lines = lines;
}

public void ShouldNotContain(string text, string message = null) => Assert.False(Lines.Any(l => l.Contains(text)), message);
public void ShouldContain(string text, string message = null) => Assert.True(Lines.Any(l => l.Contains(text)), message);
public void ShouldContain(Regex regex, string message = null) => Assert.True(Lines.Any(l => regex.IsMatch(l)), message);
}
}
}
33 changes: 20 additions & 13 deletions EFCore.FirebirdSql.FunctionalTests/TestBasic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -87,7 +89,7 @@ public void Insert_data()

for (var i = 1; i <= 10; i++)
{
context.Author.Add(new Author
var author = new Author
{
TestString = "EFCore FirebirdSQL 2.x",
TestInt = i,
Expand All @@ -97,19 +99,20 @@ public void Insert_data()
TestDecimal = i,
TestDouble = i,
TimeSpan = DateTime.Now.TimeOfDay,
Books = new List<Book>
{
new Book
{
AuthorId= i,
Title = $"Firebird 3.0.2 {i}"
}
},
Active = i % 2 == 0
};
var book = new Book
{
Title = $"Firebird 3.0.2 {i}"
};
author.Books.Add(new BookAuthor() {
Book = book,
Author = author
});
context.Author.Add(author);
}
var save = context.SaveChanges();
Assert.Equal(20, save);
Assert.Equal(30, save);

for (var i = 1; i <= 10; i++)
{
Expand Down Expand Up @@ -144,13 +147,17 @@ public void Insert_data()
{
for (var i = 1; i <= 10; i++)
{
context.Book.Add(new Book
var book = new Book
{
AuthorId = i,
Title = $"Test Insert Book {i}"
};
book.Authors.Add(new BookAuthor() {
Author = context.Author.Find((long)i),
Book = book
});
context.Book.Add(book);
}
Assert.Equal(10, context.SaveChanges());
Assert.Equal(20, context.SaveChanges());
}
}
}
Expand Down
30 changes: 27 additions & 3 deletions EFCore.FirebirdSql.FunctionalTests/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*
*/

using System.IO;
using FirebirdSql.Data.FirebirdClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
Expand All @@ -29,7 +31,7 @@ public class Issue28Context : DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = new FB.FbConnectionStringBuilder(
@"User=SYSDBA;Password=masterkey;Database=..\..\..\Issue28.fdb;DataSource=localhost;Port=3050;")
$@"User=SYSDBA;Password=masterkey;Database={Directory.GetCurrentDirectory()}..\..\..\Issue28.fdb;DataSource=localhost;Port=3050;")
{
// Dialect = 1
}.ConnectionString;
Expand Down Expand Up @@ -70,13 +72,19 @@ public class TestContext : DbContext

public DbSet<Course> Courses { get; set; }

private string dbFileName;
public TestContext(string dbFileName = "EFCoreSample.fdb")
{
this.dbFileName = dbFileName;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

var connectionString = new FB.FbConnectionStringBuilder(
@"User=SYSDBA;Password=masterkey;Database=..\..\..\EFCoreSample.fdb;DataSource=localhost;Port=3050;")
$@"User=SYSDBA;Password=masterkey;Database={Directory.GetCurrentDirectory()}\..\..\..\{dbFileName};DataSource=localhost;Port=3050;")
{
//Dialect = 1
//Dialect = 1,
}.ConnectionString;

optionsBuilder
Expand All @@ -102,6 +110,22 @@ protected override void OnModelCreating(ModelBuilder modelo)

modelo.Entity<Person>()
.HasKey(person => new { person.Name, person.LastName });

modelo.Entity<BookAuthor>()
.HasKey(ba => new { ba.BookId, ba.AuthorId } );

modelo.Entity<BookAuthor>()
.HasOne(ba => ba.Author)
.WithMany(a => a.Books)
.HasForeignKey(ba => ba.AuthorId);

modelo.Entity<BookAuthor>()
.HasOne(ba => ba.Book)
.WithMany(b => b.Authors)
.HasForeignKey(ba => ba.BookId);

modelo.Entity<BookAuthor>()
.HasIndex(ba => new { ba.BookId, ba.AuthorId });
}
}
}
12 changes: 10 additions & 2 deletions EFCore.FirebirdSql.FunctionalTests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class Author

public bool Active { get; set; }

public virtual ICollection<Book> Books { get; set; } = new List<Book>();
public virtual ICollection<BookAuthor> Books { get; set; } = new List<BookAuthor>();
}

public class Book
Expand All @@ -65,8 +65,15 @@ public class Book
[StringLength(100)]
public string Title { get; set; }

public virtual ICollection<BookAuthor> Authors { get; set; } = new List<BookAuthor>();
}

public class BookAuthor
{
public long BookId { get; set; }
public long AuthorId { get; set; }
public virtual Author Author { get; set; }
public virtual Book Book { get; set; }
}

public class Person
Expand All @@ -83,9 +90,10 @@ public class Course
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public int Credits { get; set; }

[StringLength(100)]
public string Title { get; set; }

public ICollection<Person> Enrollments { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private DbCommand CreateCommand(string commandText, object[] parameters)

public static string CreateConnectionString(string database)
{
var root = Directory.GetDirectoryRoot(Directory.GetCurrentDirectory());
var root = Directory.GetCurrentDirectory();
var connectionString = new FbConnectionStringBuilder(@"User=SYSDBA;Password=masterkey;DataSource=localhost;Port=3050;")
{
Database = Path.Combine(root, $"{database}.fdb")
Expand Down
4 changes: 2 additions & 2 deletions EFCore.FirebirdSql/Internal/FbOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

using System;
using EntityFrameworkCore.FirebirdSql.Infrastructure.Internal;
using Firebird = FirebirdSql.Data.FirebirdClient;
using Data = FirebirdSql.Data.Services;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace EntityFrameworkCore.FirebirdSql.Internal
{
using Firebird = global::FirebirdSql.Data.FirebirdClient;
using Data = global::FirebirdSql.Data.Services;
public class FbOptions : IFbOptions
{
private bool IsLegacy { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ private IEnumerable<DatabaseTable> GetTables(DbConnection connection, IEnumerabl
using (var command = connection.CreateCommand())
{
command.CommandText = $@"
SELECT RDB$RELATION_NAME FROM
RDB$RELATIONS t
WHERE t.RDB$RELATION_NAME <> '{HistoryRepository.DefaultTableName}'
AND RDB$VIEW_BLR IS NULL AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0);";
SELECT RDB$RELATION_NAME FROM
RDB$RELATIONS t
WHERE t.RDB$RELATION_NAME <> '{HistoryRepository.DefaultTableName}'
AND RDB$VIEW_BLR IS NULL AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0);";

using (var reader = command.ExecuteReader())
{
Expand Down Expand Up @@ -126,7 +126,7 @@ WHERE t.RDB$RELATION_NAME <> '{HistoryRepository.DefaultTableName}'
// Insert in Logger - refactor - v2.2
Console.WriteLine($"index '{index.Name}' on table '{table.Name}' has no columns!");
}
table.Indexes.Add(index);
table.Indexes.Add(index);
}

yield return table;
Expand Down Expand Up @@ -223,7 +223,7 @@ FROM RDB$RELATION_FIELDS RF
{
var columnName = reader["FIELD_NAME"].ToString().Trim();
var dataType = reader["FIELD_TYPE"].ToString().Trim();
var notNull = reader["FIELD_NULL"].ToString().Trim().Equals("NULL", StringComparison.OrdinalIgnoreCase);
var notNull = reader["FIELD_NULL"].ToString().Trim().Equals("NOT NULL", StringComparison.OrdinalIgnoreCase);
var defaultValue = reader["FIELD_DEFAULT"].ToString().Trim();
var isIdentity = int.Parse(reader["IDENTITY"].ToString()) == 1;
var description = reader["FIELD_DESCRIPTION"].ToString().Trim();
Expand Down Expand Up @@ -325,7 +325,7 @@ private IEnumerable<DatabaseIndex> GetIndexes(

if (string.IsNullOrWhiteSpace(columnName))
{
// ignore invalid indices (without a column specified)
// ignore indices without a column specified (i.e. with COMPUTED BY)
#pragma warning disable CS1030 // diretiva de #aviso
#warning Analyze this for 2.2
#pragma warning restore CS1030 // diretiva de #aviso
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public FbTypeMappingSource(
DbType.Int64);

_boolean = new FbBoolTypeMapping(
_isLegacy
_isLegacy || options.ServerVersion?.Major < 3
? "SMALLINT" : "BOOLEAN");

_storeTypeMappings = new Dictionary<string, RelationalTypeMapping>(StringComparer.OrdinalIgnoreCase)
Expand Down
Loading

0 comments on commit ddb1ba9

Please sign in to comment.