-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQLite: Enable Decimal #19635
Comments
|
Hi, first time here looking to contribute to OSS projects. Being tagged as a good first issue, maybe I can give it a shot? |
@lokalmatador Sure! You might want to start small on this one (e.g. just do ef_compare and it’s operators) in your first PR. |
Great! I guess I start out by looking at your changes in the above mentioned PR (PR#19617) to get an idea on how to possibly achieve this. Also, I may ask one or another question the next days :) |
Feel free to start a draft PR to ask questions about the implementation |
(Added modulo as part of PR #20024) |
First of, thanks for helping me in getting my first contribution done. I hope I wasn't too much of a burden. Would it be OK to continue on this task? I'd be happy to. |
No burden at all! Please feel free to continue |
Well, then I'd continue with ef_{add, divide, multiply, subtract} and ef_negate. |
Draft PR is here |
So, @JonPSmith got me thinking again about ORDER BY and I think we could actually do it via collation. connection.CreateCollation(
"EF_DECIMAL",
(x, y) => decimal.Compare(decimal.Parse(x), decimal.Parse(y)); SELECT *
FROM MyTable
ORDER BY DecimalColumn COLLATE EF_DECIMAL |
Wow, I wouldn't have thought of that! I did DM @ErikEJ about this and it put me on to #18593 and I saw the Sqlite limitations page. I use Sqlite in-memory as much as possible for unit testing. That's because a lot of people use 'standard' EF Core, i.e. don't use any of the SQL-specific parts, which means Sqlite in-memory works really well. I used Sqlite in-memory in my last client's project and they had an Azure CI/CD pipeline that includes unit tests and Sqlite in-memory works great! So, of course, I am interested in differences between Sqlite and say SQL Server (a typical database people use). Other than string case sensitivity and filter/order on decimal, then Sqlite in-memory is a good fit for unit testing. I suspect I will go with a ValueConvertion to double if the database provider is Sqlite to overcome the decimal limitation. PS. If you guys have any clever way to manage a real database in a CI/CD pipeline then I would love to know. Clearly I could call |
@JonPSmith In my experience, it's only the creation and dropping of databases that is really slow. (Also, dropping a database destabilizes SQL Server, so we avoid ever doing it when running tests on the C.I.) We have a database cleaner that uses our reverse engineering code to clear the database instead of dropping and re-creating it. We haven't productized it, but we're considering it. Also, it looks like @jbogard's Respawn does something similar. |
Hi @ajcvickers, Yes, I have a cleaner too but I think yours might be better, but the real problem is migrations, i.e. the Model changes during development, which is going to happen say 2% (??) of the push to CI/CD pipeline. I have a fix for handling changes to the Model that works well locally (and used it in real apps), i.e.
That works locally, but not for CI/CD. Here are two ideas I am thinking about.
Sorry, I'm unloading my ideas but I want to come up with something for an "article". If its off-topic then please ignore. |
@JonPSmith The key thing about the cleaner is that it can't be based on the EF model otherwise, as you point out, when the model changes it may no longer match the database structure. That's why the EF one uses the reverse engineering code to figure out what is in the database and then drop it all. The process used in the EF C.I. is that every test database is created if it doesn't already exist, and then cleaned. The database is then never dropped. |
@ajcvickers Wow - that is so cool! I want it 😄 Obviously I hadn't looked at your cleaner properly before, but I have now. Its pretty complex so I want to reflect back what I think your cleaner does so you can correct me. I think it does the following:
Is that correct? If so that solves the C.I. problem, but actually makes the local unit testing much better too (no need to run a method when the Model changes). And I tried a few of your unit tests and the initial test is pretty quick too. The next question is, can the Any comments? |
This is exactly what my Respawn tool does, ha!
https://github.com/jbogard/Respawn
…On Thu, Apr 9, 2020 at 5:38 AM Jon P Smith ***@***.***> wrote:
@ajcvickers <https://github.com/ajcvickers> Wow - that is so cool! I want
it 😄
Obviously I hadn't looked at your cleaner properly before, but I have now.
Its pretty complex so I want to reflect back what I think your cleaner does
so you can correct me. I *think* it does the following:
1. If there is an existing database it
1. Uses the reverse engineering code to read the schema
2. The sql code in the cleaner then deletes all the indexes,
foreign key (constraints?), tables, and sequences
3. Then it runs some code given by the caller - for SQL Server this
has code to delete other things like views, functions etc.
2. Then it creates the tables etc. defined by the current Model. I
presume its uses the code in context.Database.EnsureCreated
Is that correct? If so that solves the C.I. problem, but actually makes
the local unit testing much better too (no need to run a method when the
Model changes). And I tried a few of your unit tests and the initial test
is pretty quick too.
The next question is, can the SqlServerDatabaseCleaner be made available
easily? The RelationalDatabaseCleaner doesn't access anything internal,
but SqlServerDatabaseCleaner does.
Any comments?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#19635 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAZQMW5R5UOHMQOZFEHPILRLWQR5ANCNFSM4KIVZ3HA>
.
|
I agree, this is also true in our case.
Although there are some differences between capabilities, using SQLite makes it much easier for us to run test in parallel and faster. A lot of the time these differences are not even relevant for the unit-test. With support for decimals the differences between the data providers would be even less in our case. |
We definitely don't think of SQLite as only a testing database - it's an absolutely important database in many scenarios. However, we do discourage using SQLite as a database fake (test double) when the application actually runs against a different database in production. This used to be much more necessary as testing against the real database used to be difficult, error-prone and slow; but today with containerization technologies, spinning up a containerized database is trivial and fast. I suggest people check out testcontainers, which really makes this trivial. Beyond that, we do intend to improve decimal support for SQLite, but keep in mind that SQLite itself does not support a decimal data type, or provide any functions for working with them; that's why this isn't trivial. In addition, this issue has only received 13 votes, which means it is generally very low-priority for our users. |
We wanted to keep using SQLite with decimals in the DB, since in our test cases the lost precision was not critical. By applying these changes EF Core SQLite will happily convert your decimals to double and support all those missing operators: In DbContext partial class: public partial class MyEntities : DbContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") {
modelBuilder.Entity<MyTableEntity>(b =>
{
b.Property(e => e.OneDecimalProperty).HasConversion<double>();
b.Property(e => e.AnotherDecimalProperty).HasConversion<double>();
});
}
}
} The condition ensures that the conversions are only applied when the DbContext is used by SQLite, meaning only in our unit tests. A drawback is that you need to repeat this for all decimal properties that are included in non-supported operations (such as |
@bjowes usually, the point in using decimal in the first place is to do high-accuracy math; if you're just converting all your decimals to double in the database, it may be better to just have double properties in the first place (i.e. what's the point of making them decimal?). |
Agreed @roji - for users with SQLite as their production database, my suggestion would make the behavior similar to using double in the database. However, I think the suggestion is still valuable for others who use SQLite for unit testing (or integration testing) only, and another provider for system tests and production where decimals are properly supported. I see now that my comment was not clear on that this was the intended scenario. |
@bjowes see my comment just above about it being a bad idea to use Sqlite (or any other database) as a test double. Integration testing - at least in my understanding of the term - would mean that you use Sqlite in production, in which case it again doesn't make much sense to me to use |
I have read that comment @roji but I don't agree. Even with testcontainers, spinning up an SQL Server database with isolation between the tests is still an operation in the order of seconds. That same operation for SQLite in-memory is in the order of milliseconds. This fact alone makes SQLite a feasible option for unit testing, while testcontainer is not - given that unit tests are often in the order of hundreds (or thousands). As many others have pointed out in earlier comments, I think SQLite for testing provides a strong middle ground option. It is not exactly the same as the production database, but it is close enough to help and fast enough to be an option. The alternative of mocking out the database completely (repository pattern, mocked context or some other way) is error prone in itself, since it leaves the implementation of acting as a database to the test developer. Detecting changes, resolving and updating references, checking constraints is all on you. Sure, it doesn't have to be fully SQL compliant. But when it isn't you can end up having plenty of tests that are testing against data which could never occur in production, or tests that pass although they will fail when constraints are properly checked. |
The typical way to use a testcontainer database is indeed not to create a database for each test, but to reuse the same database across tests. That indeed raises the problem of test isolation, but there are well-known ways to deal with that which really aren't that hard (see our docs for discussion). More importantly... Our repo is full of issues from people asking us to, simply put, make the InMemory provider (and the Sqlite provider) behave like SQL Server (or some other database). The pattern is always the same: people start out with the "minimal effort" solution of using InMemory/Sqlite as a database fake, because it's easy, fast and doesn't require thinking about isolation. In all but the most trivial cases, these people run into some place where InMemory/Sqlite simply isn't SQL Server, and their SQL Server queries either don't translate, or return different results. They ask us to make SQLite be case-insensitive with strings (because SQL Server is), implement SQL Server-style full-text search (which is impossible for us to do), or to somehow make the SQL Server-specific functions such as EF.Functions.DateDiff work. None of that is feasible, and it's certainly not the job of the EF SQLite provider to make SQLite behave like SQL Server; SQLite is a database in its own right, with its own behaviors and featureset. The alternative is to invest a bit of time and effort up-front to handle test isolation; run all read-only tests against the same database (no isolation problems to begin with), run write tests within a transaction that can be rolled back, and for tests which require exercising code that manages transaction, clean and reseed the database between each test (tools such as Respawn can help there).
I think there may be a misunderstanding here; the alternative of a repository pattern isn't about mocking out the database (that's exactly what Sqlite/InMemory do), but rather mocking out your data layer. In other words, the idea is to cut EF out entirely from your application, and have high-level repository operations which encapsulate stuff like change detection (which is what EF does). That's the right pattern if you want pure unit tests, which verify that your code works correctly (and therefore EF shouldn't be involved at all, nor a fake database). There's unfortunately no middle ground that really works (assuming any but the most trivial cases): you can either do pure unit tests with a repository pattern (fully mocked data access), or you can run integration tests involving your production database system, at which point you need to take care of isolation. |
In my opinion, one of the dumbest arguments in tech, regardless of the context, is that "My way is better than your way so we are going to do it my way". There are always choices, tradeoffs, use cases, even experiments, that will satisfy some need. If you are a vendor, you can choose your way. But in the world of open source, everyone is free to do things differently. On the subject at hand, while I would like to see this request addressed, I perfectly understand the circumstances of a development team that has their own vision of how things should work as well as constraints on time and other resources that require them to make choices on what to support and enhance. As long as the EF Core Team is willing to accept a reasonable patch or support an extension mechanism by which other parties can integrate their own solution, I see no problem. Incidentally, there is somewhat of a hack for Sqlite to support a decimal type mentioned here: NUMERIC and DECIMAL support for Sqlite #2887 |
@bjowes The code that you posted code gave me an idea for our unit-test scenarios without having to mention all the columns specifically. I put the code below at the end of OnModelCreating: if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
{
foreach (Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType in builder.Model.GetEntityTypes())
{
foreach (Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property in entityType.GetProperties())
{
Type propertyType = Nullable.GetUnderlyingType(property.ClrType) ?? property.ClrType;
if (propertyType == typeof(Decimal))
{
property.SetProviderClrType(typeof(Double));//Decimals are treated as doubles
}
}
}
} This means we cannot test with the complete precision in the unit-tests, but it means we can test all methods using sum, avg, etc. with certain precision, which is a big step forward and maybe even enough for our unit-testing purposes. Thank you very much for this idea! |
Given that #26070 has been fixed, I would like to implement the aggregation functions as suggested in #19635 (comment) |
@ranma42 I'll bring it up with the team. |
@ranma42 We discussed this and we think you should be good to go, but @cincuranet is going to confirm. |
Contributes to dotnet#19635
I confirm. |
Contributes to dotnet#19635
Similar to PR #19617, we can enable more decimal operations by leveraging UDFs. Unlike TimeSpan, however, there isn't an acceptable type we can convert to to perform the operations, so it will require roughly one UDF per operation. For example:
The text was updated successfully, but these errors were encountered: