Skip to content

Use the last day of the month for AddMonths and AddYears on SQLite - #39548

Open
officialasishkumar wants to merge 2 commits into
dotnet:mainfrom
officialasishkumar:fix/39545-sqlite-add-months-floor
Open

officialasishkumar wants to merge 2 commits into
dotnet:mainfrom
officialasishkumar:fix/39545-sqlite-add-months-floor

Conversation

@officialasishkumar

@officialasishkumar officialasishkumar commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #39545

On SQLite, AddMonths and AddYears on DateTime and DateOnly returned a different date from .NET when the day doesn't exist in the resulting month. SQLite overflows into the next month, so 2023-01-31 + 1 month was 2023-03-03 and 2024-02-29 + 1 year was 2025-03-01. .NET, SQL Server and PostgreSQL all use the last day of the resulting month. There's no error: the query just returns a wrong date, and a filter on the result silently misses rows.

#25851 closed this as by design in 2022, because SQLite had no way to get the other behavior. SQLite 3.46 has since added the floor modifier for this case.

Fix

SqliteDateTimeMethodTranslator now adds 'floor' straight after the months or years modifier:

date("b"."DateOnly", CAST(1 AS TEXT) || ' months', 'floor')

Each AddMonths or AddYears call gets its own 'floor', so chained calls clamp at each step as they do in .NET, including when SqliteSqlExpressionFactory merges nested strftime and date calls into one. Days, hours, minutes, seconds, milliseconds and ticks aren't affected.

Tests

  • DateOnlyTranslationsSqliteTest: new tests for adding months past the end of a month, in both directions, and for adding years to a leap day.
  • DateTimeTranslationsSqliteTest: new tests for adding months past the end of a month and for adding years to a leap day.
  • Select_expression_datetime_add_month: removed the workaround that allowed up to 3 days of difference, so the SQLite test now runs the base test with exact comparison.
  • Updated the SQL baselines for the 'floor' modifier.

Without the change, the new tests and Select_expression_datetime_add_month fail on the data, not only on the SQL (for example Expected: 1996-11-30, Actual: 1996-12-01 on Northwind). With it, all of EFCore.Sqlite.FunctionalTests passes locally (38714 passed, 284 skipped).

Behavior notes

  • This changes the results of existing queries when the day doesn't exist in the resulting month. They now match client evaluation and the other providers.

  • 'floor' needs SQLite 3.46 or later. The SQLitePCLRaw.bundle_e_sqlite3 version that EF Core depends on supports it. With an older native SQLite, the translator checks the version and leaves out 'floor', so those applications keep the previous overflow behavior instead of getting NULL.

  • I've read the guidelines for contributing and seen the walkthrough

  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team

  • The code builds and tests pass locally (also verified by our automated build checks)

  • Commit messages follow this format:

        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

SQLite resolves a day that doesn't exist in the resulting month by
overflowing into the next month, so 2023-01-31 + 1 month was 2023-03-03
and 2024-02-29 + 1 year was 2025-03-01. .NET and the other providers use
the last day of the resulting month, so the query silently returned a
different date than the same expression evaluated on the client.

- Add the 'floor' modifier after the months and years modifiers for
  DateTime and DateOnly. SQLite added it in 3.46 for this case; dotnet#25851
  was closed as by design before it existed.
- Remove the workaround in Select_expression_datetime_add_month that
  allowed up to 3 days of difference, so it now uses the base test.
- Add tests for adding months past the end of a month, in both
  directions, and for adding years to a leap day.

Fixes dotnet#39545
@officialasishkumar
officialasishkumar requested a review from a team as a code owner November 6, 2026 04:49
Copilot AI balanced review requested due to automatic review settings November 6, 2026 04:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The unconditional SQLite floor modifier returns NULL on native versions before 3.46, silently dropping matching rows from existing date-addition filters.

2 open issues
What changed in this PR

This PR addresses #39545 by aligning SQLite’s DateTime and DateOnly month/year additions with .NET’s end-of-month clamping behavior.

Changes:

  • Adds SQLite’s floor modifier after each month or year adjustment.
  • Adds month-end and leap-day regression tests and updates SQL baselines.
  • Restores exact date comparison in the Northwind month-addition test.
File Description
test/​EFCore.Sqlite.FunctionalTests/​Query/​Translations/​Temporal/​DateTimeTranslationsSqliteTest.cs Adds DateTime boundary tests and updates the year-addition baseline.
test/​EFCore.Sqlite.FunctionalTests/​Query/​Translations/​Temporal/​DateOnlyTranslationsSqliteTest.cs Adds DateOnly boundary tests and updates standalone and chained SQL baselines.
test/​EFCore.Sqlite.FunctionalTests/​Query/​NorthwindSelectQuerySqliteTest.cs Updates the year-addition SQL baseline.
test/​EFCore.Sqlite.FunctionalTests/​Query/​NorthwindMiscellaneousQuerySqliteTest.cs Removes the date-tolerance workaround and updates SQL baselines.
src/​EFCore.Sqlite.Core/​Query/​Internal/​Translators/​SqliteDateTimeMethodTranslator.cs Emits floor after month and year modifiers for both date types.

Review effort: Balanced

// By default SQLite overflows a day past the end of the resulting month into the next month (e.g. January 31 + 1 month gives
// March 3); 'floor' makes it the last day of the resulting month instead, which is what DateTime and DateOnly do.
private SqlExpression[] MakeClampedModifiers(SqlExpression argument, string unitSuffix)
=> [MakeModifier(argument, unitSuffix), sqlExpressionFactory.Constant("floor")];
Comment on lines +247 to +251
public virtual async Task AddMonths_past_end_of_month_gives_last_day_of_month()
{
// 1998-05-31 + 1 month
await AssertQuery(
ss => ss.Set<BasicTypesEntity>().Where(o => o.DateTime.AddDays(27).AddMonths(1) == new DateTime(1998, 6, 30, 15, 30, 10)));
@cincuranet cincuranet self-assigned this Nov 6, 2026
SQLite returns NULL for an unknown date modifier, so on native versions
older than 3.46 every AddMonths and AddYears translation would return
NULL. Check the version as the other SQLite feature checks do and keep
the previous overflow behavior when 'floor' isn't available.
Copilot AI balanced review requested due to automatic review settings November 10, 2026 12:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite: AddMonths and AddYears overflow into the next month instead of using the last day of the month

3 participants