From 52a5907e4342d2f95d2f821e79800614ff301896 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 5 Jul 2022 21:51:50 +0200 Subject: [PATCH] Integration tests for more date time modifiers --- .../query_builder/expressions/datetimes.dart | 221 +++++++----------- .../expressions_integration_test.dart | 46 ++++ 2 files changed, 133 insertions(+), 134 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/expressions/datetimes.dart b/drift/lib/src/runtime/query_builder/expressions/datetimes.dart index 7d1eb2ce..afc77b38 100644 --- a/drift/lib/src/runtime/query_builder/expressions/datetimes.dart +++ b/drift/lib/src/runtime/query_builder/expressions/datetimes.dart @@ -84,24 +84,21 @@ extension DateTimeExpressions on Expression { [Constant(format), this, const DateTimeModifier._unixEpoch()]); /// Apply a modifier that alters the date and/or time. + /// + /// See the factories on [DateTimeModifier] for a list of modifiers that can + /// be used with this method. Expression modify(DateTimeModifier modifier) => - FunctionCallExpression('strftime', [ - const Constant('%s'), - this, - const DateTimeModifier._unixEpoch(), - modifier - ]); + FunctionCallExpression( + 'unixepoch', [this, const DateTimeModifier._unixEpoch(), modifier]); - /// Applies modifiers that alters the date and/or time. Each modifier is a - /// transformation that is applied to the time value to its left. Modifiers - /// are applied from left to right; order is important. - Expression modifyThough(Iterable modifiers) => - FunctionCallExpression('strftime', [ - const Constant('%s'), - this, - const DateTimeModifier._unixEpoch(), - ...modifiers - ]); + /// Applies modifiers that alters the date and/or time. + /// + /// The [modifiers] are applied in sequence from left to right. + /// For a list of modifiers and how they behave, see the docs on + /// [DateTimeModifier] factories. + Expression modifyAll(Iterable modifiers) => + FunctionCallExpression('unixepoch', + [this, const DateTimeModifier._unixEpoch(), ...modifiers]); /// Returns an expression containing the amount of seconds from the unix /// epoch (January 1st, 1970) to `this` datetime expression. The datetime is @@ -110,13 +107,23 @@ extension DateTimeExpressions on Expression { // anything when converting Expression get secondsSinceEpoch => dartCast(); - /// Adds [duration] from this date. + /// Adds a [duration] to this date. + /// + /// Note that the curation is added as a value in seconds. Thus, adding a + /// `Duration(days: 1)` will not necessary yield the same time tomorrow in all + /// cases (due to daylight saving time switches). + /// To change the value in terms of calendar units, see [modify]. Expression operator +(Duration duration) { return _BaseInfixOperator(this, '+', Variable(duration.inSeconds), precedence: Precedence.plusMinus); } /// Subtracts [duration] from this date. + /// + /// Note that the curation is subtracted as a value in seconds. Thus, + /// subtracting a `Duration(days: 1)` will not necessary yield the same time + /// yesterday in all cases (due to daylight saving time switches). To change + /// the value in terms of calendar units, see [modify]. Expression operator -(Duration duration) { return _BaseInfixOperator(this, '-', Variable(duration.inSeconds), precedence: Precedence.plusMinus); @@ -149,163 +156,109 @@ class _StrftimeSingleFieldExpression extends Expression { } } -/// DateTime modifier constants +/// DateTime modifier constants. +/// +/// These modifiers are used on [DateTimeExpressions.modify] and +/// [DateTimeExpressions.modifyAll] to apply transformations on date time +/// values. +/// +/// For instance, [DateTimeModifier.days] can be used to add or subtract +/// calendar days from a date time value. Note that this is different from +/// just subtracting a duration with [DateTimeExpressions.+], which only adds a +/// duration as seconds without respecting calendar units. +/// +/// For another explanation of modifiers, see the [sqlite3 docs]. +/// +/// [sqlite3 docs]: https://sqlite.org/lang_datefunc.html#modifiers class DateTimeModifier extends Constant { const DateTimeModifier._(super.value); - /// The "n days" modifiers simply add the specified amount of time to the date - /// and time specified by the arguments to the left. Note that "±NNN months" - /// works by rendering the original date into the YYYY-MM-DD format, adding - /// the ±NNN to the MM month value, then normalizing the result. Thus, for - /// example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. + /// Adds or subtracts [days] calendar days from the date time value. const DateTimeModifier.days(int days) : this._('$days days'); - /// The "n hours" modifiers simply add the specified amount of time to the - /// date and time specified by the arguments to the left. Note that "±NNN - /// months" works by rendering the original date into the YYYY-MM-DD format, - /// adding the ±NNN to the MM month value, then normalizing the result. Thus, - /// for example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. - DateTimeModifier.hours(int hours) : this._('$hours hours'); + /// Adds or subtracts [hours] hours from this date time value. + const DateTimeModifier.hours(int hours) : this._('$hours hours'); - /// The "n minutes" modifiers simply add the specified amount of time to the - /// date and time specified by the arguments to the left. Note that "±NNN - /// months" works by rendering the original date into the YYYY-MM-DD format, - /// adding the ±NNN to the MM month value, then normalizing the result. Thus, - /// for example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. - DateTimeModifier.minutes(int minutes) : this._('$minutes minutes'); + /// Adds or subtracts [minutes] minutes from this date time value. + const DateTimeModifier.minutes(int minutes) : this._('$minutes minutes'); - /// The "n seconds" modifiers simply add the specified amount of time to the - /// date and time specified by the arguments to the left. Note that "±NNN - /// months" works by rendering the original date into the YYYY-MM-DD format, - /// adding the ±NNN to the MM month value, then normalizing the result. Thus, - /// for example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. - DateTimeModifier.seconds(num seconds) : this._('$seconds seconds'); + /// Adds or subtracts [seconds] seconds from this date time value. + /// + /// Note that drift assumes date time values to be encoded as unix timestamps + /// (with second accuracy) in the database. So adding seconds with a + /// fractional value may not always be preserved in a chain of computation. + const DateTimeModifier.seconds(num seconds) : this._('$seconds seconds'); - /// The "n months" modifiers simply add the specified amount of time to the - /// date and time specified by the arguments to the left. Note that "±NNN - /// months" works by rendering the original date into the YYYY-MM-DD format, - /// adding the ±NNN to the MM month value, then normalizing the result. Thus, - /// for example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. - DateTimeModifier.months(int months) : this._('$months months'); + /// Adds or subtracts [months] months from this date time value. + /// + /// Note that this works by rendering the original date into the `YYYY-MM-DD` + /// format, adding the [months] value to the `MM` field and normalizing the + /// result. Thus, for example, the date 2001-03-31 modified by '+1 month' + /// nitially yields 2001-04-31, but April only has 30 days so the date is + /// normalized to 2001-05-01. + const DateTimeModifier.months(int months) : this._('$months months'); - /// The "n years" modifiers simply add the specified amount of time to the - /// date and time specified by the arguments to the left. Note that "±NNN - /// months" works by rendering the original date into the YYYY-MM-DD format, - /// adding the ±NNN to the MM month value, then normalizing the result. Thus, - /// for example, the date 2001-03-31 modified by '+1 month' initially yields - /// 2001-04-31, but April only has 30 days so the date is normalized to - /// 2001-05-01. A similar effect occurs when the original date is February 29 - /// of a leapyear and the modifier is ±N years where N is not a multiple of - /// four. - DateTimeModifier.years(int years) : this._('$years years'); + /// Adds or subtracts [years] years from this date time value. + /// + /// Similar to the transformation on [DateTimeModifier.months], it may not + /// always be possible to keep the day and month field the same for this + /// transformation. For instance, if the original date is February 29 of a + /// leapyear and one year is added, the result will be in March 1 of the next + /// year as there is no February 29. + const DateTimeModifier.years(int years) : this._('$years years'); - /// The "start of day" modifier shift the date backwards to the beginning of + /// The "start of day" modifier shifts the date backwards to the beginning of /// the day. const DateTimeModifier.startOfDay() : this._('start of day'); - /// The "start of month" modifier shift the date backwards to the beginning of - /// the month. + /// The "start of month" modifier shifts the date backwards to the beginning + /// of the month. const DateTimeModifier.startOfMonth() : this._('start of month'); - /// The "start of year" modifier shift the date backwards to the beginning of + /// The "start of year" modifier shifts the date backwards to the beginning of /// the year. const DateTimeModifier.startOfYear() : this._('start of year'); - /// The "weekday" modifier advances the date forward, if necessary, to the - /// next date where the weekday number is N. Sunday is 0, Monday is 1, and so - /// forth. If the date is already on the desired weekday, the "weekday" - /// modifier leaves the date unchanged. + /// The "weekday" modifier shifts the date forward to the next date where the + /// weekday is the [weekday] provided here. + /// + /// If the source date is on the desired weekday, no transformation happens. DateTimeModifier.weekday(DateTimeWeekday weekday) : this._('weekday ${weekday.index}'); - /// The "unixepoch" modifier only works if it immediately follows a time - /// value in the DDDDDDDDDD format. This modifier causes the DDDDDDDDDD to be - /// interpreted not as a Julian day number as it normally would be, but as - /// Unix Time - the number of seconds since 1970. If the "unixepoch" modifier - /// does not follow a time value of the form DDDDDDDDDD which expresses the - /// number of seconds since 1970 or if other modifiers separate the - /// "unixepoch" modifier from prior DDDDDDDDDD then the behavior is undefined. - /// For SQLite versions before 3.16.0 (2017-01-02), the "unixepoch" modifier - /// only works for dates between 0000-01-01 00:00:00 and 5352-11-01 10:52:47 - /// (unix times of -62167219200 through 106751991167). const DateTimeModifier._unixEpoch() : this._('unixepoch'); - // The "julianday" modifier must immediately follow the initial time-value - // which must be of the form DDDDDDDDD. Any other use of the 'julianday' - // modifier is an error and causes the function to return NULL. The - // 'julianday' modifier forces the time-value number to be interpreted as a - // julian-day number. As this is the default behavior, the 'julianday' - // modifier is scarcely more than a no-op. The only difference is that adding - // 'julianday' forces the DDDDDDDDD time-value format, and causes a NULL to - // be returned if any other time-value format is used. - //const DateTimeModifier.julianDay() : this._('julianday'); - - // The "auto" modifier must immediately follow the initial time-value. If the - // time-value is numeric (the DDDDDDDDDD format) then the 'auto' modifier - // causes the time-value to interpreted as either a julian day number or a - // unix timestamp, depending on its magnitude. If the value is between 0.0 - // and 5373484.499999, then it is interpreted as a julian day number - // (corresponding to dates between -4713-11-24 12:00:00 and 9999-12-31 - // 23:59:59, inclusive). For numeric values outside of the range of valid - // julian day numbers, but within the range of -210866760000 to 253402300799, - // the 'auto' modifier causes the value to be interpreted as a unix - // timestamp. Other numeric values are out of range and cause a NULL return. - // The 'auto' modifier is a no-op for text time-values. - //const DateTimeModifier.auto() : this._('auto'); - - /// The "localtime" modifier (14) assumes the time value to its left is in - /// Universal Coordinated Time (UTC) and adjusts that time value so that it is - /// in localtime. If "localtime" follows a time that is not UTC, then the - /// behavior is undefined. + /// Move a date time that is in UTC to the local time zone. + /// + /// See also: [DateTime.toLocal]. const DateTimeModifier.localTime() : this._('localtime'); - /// The "utc" modifier is the opposite of "localtime". "utc" assumes that the - /// time value to its left is in the local timezone and adjusts that time - /// value to be in UTC. If the time to the left is not in localtime, then the - /// result of "utc" is undefined. + /// Move a date time that is in the local time zone back to UTC. + /// + /// See also: [DateTime.toLocal]. const DateTimeModifier.utc() : this._('utc'); } /// Weekday offset to be used with [DateTimeModifier.weekday] enum DateTimeWeekday { - /// Sunday (+0) + /// Sunday (+0 on [DateTimeModifier.weekday]) sunday, - /// Monday (+1) + /// Monday (+1 on [DateTimeModifier.weekday]) monday, - /// Tueday (+2) + /// Tueday (+2 on [DateTimeModifier.weekday]) tuesday, - /// Wednesday (+3) + /// Wednesday (+3 on [DateTimeModifier.weekday]) wednesday, - /// Thursday (+4) + /// Thursday (+4 on [DateTimeModifier.weekday]) thursday, - /// Friday (+5) + /// Friday (+5 on [DateTimeModifier.weekday]) friday, - /// Saturday (+6) - saturday, + /// Saturday (+6 on [DateTimeModifier.weekday]) + saturday } diff --git a/drift/test/database/expressions/expressions_integration_test.dart b/drift/test/database/expressions/expressions_integration_test.dart index af35bc5f..90fa3f54 100644 --- a/drift/test/database/expressions/expressions_integration_test.dart +++ b/drift/test/database/expressions/expressions_integration_test.dart @@ -58,6 +58,52 @@ void main() { completion('2020-09-03 23:55:00')); }); + test('date time modifiers', () { + final expr = Variable.withDateTime(DateTime.utc(2022, 07, 05)); + + expect(eval(expr.modify(const DateTimeModifier.days(2))), + completion(DateTime.utc(2022, 07, 07).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.months(-2))), + completion(DateTime.utc(2022, 05, 05).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.years(1))), + completion(DateTime.utc(2023, 07, 05).toLocal())); + + expect(eval(expr.modify(const DateTimeModifier.hours(12))), + completion(DateTime.utc(2022, 07, 05, 12).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.minutes(30))), + completion(DateTime.utc(2022, 07, 05, 0, 30).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.seconds(30))), + completion(DateTime.utc(2022, 07, 05, 0, 0, 30).toLocal())); + + expect(eval(expr.modify(const DateTimeModifier.startOfDay())), + completion(DateTime.utc(2022, 07, 05).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.startOfMonth())), + completion(DateTime.utc(2022, 07, 01).toLocal())); + expect(eval(expr.modify(const DateTimeModifier.startOfYear())), + completion(DateTime.utc(2022, 01, 01).toLocal())); + + // The original expression is a Tuesday + expect(eval(expr.modify(DateTimeModifier.weekday(DateTimeWeekday.tuesday))), + completion(DateTime.utc(2022, 07, 05).toLocal())); + expect( + eval(expr.modify(DateTimeModifier.weekday(DateTimeWeekday.saturday))), + completion(DateTime.utc(2022, 07, 09).toLocal()), + ); + + // drift interprets date time values as timestamps, so going to UTC means + // subtracting the UTC offset in SQL. Interpreting that timestamp in Dart + // will effectively add it back, so we have the same value bit without the + // UTC flag in Dart. + expect(eval(expr.modify(const DateTimeModifier.utc())), + completion(DateTime(2022, 07, 05))); + + // And vice-versa (note that original expr is in UTC, this one isn't) + expect( + eval(Variable.withDateTime(DateTime(2022, 07, 05)) + .modify(const DateTimeModifier.localTime())), + completion(DateTime.utc(2022, 07, 05).toLocal())); + }); + test('rowid', () { expect(eval(db.users.rowId), completion(1)); });