mirror of https://github.com/AMT-Cheif/drift.git
Properly store text date time values
This commit is contained in:
parent
592e2cdd5d
commit
44e360e28d
|
@ -11,6 +11,10 @@ import '../query_builder/query_builder.dart';
|
||||||
/// literals.
|
/// literals.
|
||||||
@sealed
|
@sealed
|
||||||
class SqlTypes {
|
class SqlTypes {
|
||||||
|
// Stolen from DateTime._parseFormat
|
||||||
|
static final RegExp _timeZoneInDateTime =
|
||||||
|
RegExp(r' ?([-+])(\d\d)(?::?(\d\d))?$');
|
||||||
|
|
||||||
final bool _storeDateTimesAsText;
|
final bool _storeDateTimesAsText;
|
||||||
|
|
||||||
/// Creates an [SqlTypes] mapper from the provided options.
|
/// Creates an [SqlTypes] mapper from the provided options.
|
||||||
|
@ -25,7 +29,32 @@ class SqlTypes {
|
||||||
// These need special handling, all other types are a direct mapping
|
// These need special handling, all other types are a direct mapping
|
||||||
if (dartValue is DateTime) {
|
if (dartValue is DateTime) {
|
||||||
if (_storeDateTimesAsText) {
|
if (_storeDateTimesAsText) {
|
||||||
|
// sqlite3 assumes UTC by default, so we store the explicit UTC offset
|
||||||
|
// along with the value. For UTC datetimes, there's nothing to change
|
||||||
|
if (dartValue.isUtc) {
|
||||||
return dartValue.toIso8601String();
|
return dartValue.toIso8601String();
|
||||||
|
} else {
|
||||||
|
final offset = dartValue.timeZoneOffset;
|
||||||
|
// Quick sanity check: We can only store the UTC offset as `hh:mm`,
|
||||||
|
// so if the offset has seconds for some reason we should refuse to
|
||||||
|
// store that.
|
||||||
|
if (offset.inSeconds - 60 * offset.inMinutes != 0) {
|
||||||
|
throw ArgumentError.value(dartValue, 'dartValue',
|
||||||
|
'Cannot be mapped to SQL: Invalid UTC offset $offset');
|
||||||
|
}
|
||||||
|
|
||||||
|
final hours = offset.inHours.abs();
|
||||||
|
final minutes = offset.inMinutes.abs() - 60 * hours;
|
||||||
|
|
||||||
|
// For local date times, add the offset as ` +hh:mm` in the end. This
|
||||||
|
// format is understood by `DateTime.parse` and date time functions in
|
||||||
|
// sqlite.
|
||||||
|
final prefix = offset.isNegative ? ' -' : ' +';
|
||||||
|
final formattedOffset = '${hours.toString().padLeft(2, '0')}:'
|
||||||
|
'${minutes.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
return '${dartValue.toIso8601String()}$prefix$formattedOffset';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return dartValue.millisecondsSinceEpoch ~/ 1000;
|
return dartValue.millisecondsSinceEpoch ~/ 1000;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +88,8 @@ class SqlTypes {
|
||||||
return dart.toString();
|
return dart.toString();
|
||||||
} else if (dart is DateTime) {
|
} else if (dart is DateTime) {
|
||||||
if (_storeDateTimesAsText) {
|
if (_storeDateTimesAsText) {
|
||||||
return "'${dart.toIso8601String()}'";
|
final encoded = mapToSqlVariable(dart).toString();
|
||||||
|
return "'$encoded'";
|
||||||
} else {
|
} else {
|
||||||
return (dart.millisecondsSinceEpoch ~/ 1000).toString();
|
return (dart.millisecondsSinceEpoch ~/ 1000).toString();
|
||||||
}
|
}
|
||||||
|
@ -93,7 +123,19 @@ class SqlTypes {
|
||||||
return int.parse(sqlValue.toString()) as T;
|
return int.parse(sqlValue.toString()) as T;
|
||||||
case DriftSqlType.dateTime:
|
case DriftSqlType.dateTime:
|
||||||
if (_storeDateTimesAsText) {
|
if (_storeDateTimesAsText) {
|
||||||
return DateTime.parse(read(DriftSqlType.string, sqlValue)!) as T;
|
final rawValue = read(DriftSqlType.string, sqlValue)!;
|
||||||
|
final value = DateTime.parse(rawValue);
|
||||||
|
|
||||||
|
// The stored format is the same as toIso8601String for utc values,
|
||||||
|
// but for local date times we append the time zone offset.
|
||||||
|
// DateTime.parse picks that up, but then returns an UTC value. For
|
||||||
|
// round-trip equality, we recover that information and reutrn to
|
||||||
|
// a local date time.
|
||||||
|
if (_timeZoneInDateTime.hasMatch(rawValue)) {
|
||||||
|
return value.toLocal() as T;
|
||||||
|
} else {
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final unixSeconds = read(DriftSqlType.int, sqlValue)!;
|
final unixSeconds = read(DriftSqlType.int, sqlValue)!;
|
||||||
return DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000) as T;
|
return DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000) as T;
|
||||||
|
|
|
@ -25,4 +25,138 @@ void main() {
|
||||||
|
|
||||||
expect(ctx.sql, "name < strftime('%s', CURRENT_TIMESTAMP)");
|
expect(ctx.sql, "name < strftime('%s', CURRENT_TIMESTAMP)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('mapping datetime values', () {
|
||||||
|
group('from dart to sql', () {
|
||||||
|
final local = DateTime(2022, 07, 21, 22, 53, 12, 888, 999);
|
||||||
|
final utc = DateTime.utc(2022, 07, 21, 22, 53, 12, 888, 999);
|
||||||
|
|
||||||
|
test('as unix timestamp', () {
|
||||||
|
expect(Variable(local),
|
||||||
|
generates('?', [local.millisecondsSinceEpoch ~/ 1000]));
|
||||||
|
expect(Variable(utc), generates('?', [1658443992]));
|
||||||
|
|
||||||
|
expect(Constant(local),
|
||||||
|
generates('${local.millisecondsSinceEpoch ~/ 1000}'));
|
||||||
|
expect(Constant(utc), generates('1658443992'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('as text', () {
|
||||||
|
const options = DriftDatabaseOptions(storeDateTimeAsText: true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Variable(_MockDateTime(local, const Duration(hours: 1))),
|
||||||
|
generatesWithOptions(
|
||||||
|
'?',
|
||||||
|
variables: ['2022-07-21T22:53:12.888999 +01:00'],
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
Variable(_MockDateTime(local, const Duration(hours: 1, minutes: 12))),
|
||||||
|
generatesWithOptions(
|
||||||
|
'?',
|
||||||
|
variables: ['2022-07-21T22:53:12.888999 +01:12'],
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
Variable(
|
||||||
|
_MockDateTime(local, -const Duration(hours: 1, minutes: 29))),
|
||||||
|
generatesWithOptions(
|
||||||
|
'?',
|
||||||
|
variables: ['2022-07-21T22:53:12.888999 -01:29'],
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Variable(utc),
|
||||||
|
generatesWithOptions(
|
||||||
|
'?',
|
||||||
|
variables: ['2022-07-21T22:53:12.888999Z'],
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Constant(_MockDateTime(local, const Duration(hours: 1))),
|
||||||
|
generatesWithOptions(
|
||||||
|
"'2022-07-21T22:53:12.888999 +01:00'",
|
||||||
|
options: options,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
Constant(utc),
|
||||||
|
generatesWithOptions("'2022-07-21T22:53:12.888999Z'",
|
||||||
|
options: options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Writing date times with an UTC offset that isn't a whole minute
|
||||||
|
// is not supported and should throw.
|
||||||
|
expect(() {
|
||||||
|
final context = stubContext(options: options);
|
||||||
|
Variable(_MockDateTime(local, const Duration(seconds: 30)))
|
||||||
|
.writeInto(context);
|
||||||
|
}, throwsArgumentError);
|
||||||
|
|
||||||
|
expect(() {
|
||||||
|
final context = stubContext(options: options);
|
||||||
|
Constant(_MockDateTime(local, const Duration(seconds: 30)))
|
||||||
|
.writeInto(context);
|
||||||
|
}, throwsArgumentError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('from sql to dart', () {
|
||||||
|
test('as unix timestamp', () {
|
||||||
|
const types = SqlTypes(false);
|
||||||
|
|
||||||
|
expect(types.read(DriftSqlType.dateTime, 1658443992),
|
||||||
|
DateTime.utc(2022, 07, 21, 22, 53, 12).toLocal());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('as text', () {
|
||||||
|
const types = SqlTypes(true);
|
||||||
|
|
||||||
|
expect(types.read(DriftSqlType.dateTime, '2022-07-21T22:53:12Z'),
|
||||||
|
DateTime.utc(2022, 07, 21, 22, 53, 12));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
types.read(DriftSqlType.dateTime, '2022-07-21T22:53:12 -03:00'),
|
||||||
|
DateTime.utc(2022, 07, 21, 22, 53, 12)
|
||||||
|
.add(const Duration(hours: 3))
|
||||||
|
.toLocal(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MockDateTime implements DateTime {
|
||||||
|
final DateTime original;
|
||||||
|
final Duration utcOffset;
|
||||||
|
|
||||||
|
_MockDateTime(this.original, this.utcOffset) : assert(!original.isUtc);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isUtc => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get timeZoneOffset => utcOffset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toIso8601String() {
|
||||||
|
return original.toIso8601String();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '${original.toString()} with fake offset $utcOffset';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) {
|
||||||
|
return super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,24 @@ void expectNotEquals(dynamic a, dynamic expected) {
|
||||||
/// Matcher for [Component]-subclasses. Expect that a component generates the
|
/// Matcher for [Component]-subclasses. Expect that a component generates the
|
||||||
/// matching [sql] and, optionally, the matching [variables].
|
/// matching [sql] and, optionally, the matching [variables].
|
||||||
Matcher generates(dynamic sql, [dynamic variables = isEmpty]) {
|
Matcher generates(dynamic sql, [dynamic variables = isEmpty]) {
|
||||||
return _GeneratesSqlMatcher(wrapMatcher(sql), wrapMatcher(variables));
|
return _GeneratesSqlMatcher(
|
||||||
|
wrapMatcher(sql), wrapMatcher(variables), const DriftDatabaseOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher generatesWithOptions(dynamic sql,
|
||||||
|
{dynamic variables = isEmpty,
|
||||||
|
DriftDatabaseOptions options = const DriftDatabaseOptions()}) {
|
||||||
|
return _GeneratesSqlMatcher(
|
||||||
|
wrapMatcher(sql), wrapMatcher(variables), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GeneratesSqlMatcher extends Matcher {
|
class _GeneratesSqlMatcher extends Matcher {
|
||||||
final Matcher _matchSql;
|
final Matcher _matchSql;
|
||||||
final Matcher? _matchVariables;
|
final Matcher? _matchVariables;
|
||||||
|
|
||||||
_GeneratesSqlMatcher(this._matchSql, this._matchVariables);
|
final DriftDatabaseOptions options;
|
||||||
|
|
||||||
|
_GeneratesSqlMatcher(this._matchSql, this._matchVariables, this.options);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Description describe(Description description) {
|
Description describe(Description description) {
|
||||||
|
@ -68,7 +78,7 @@ class _GeneratesSqlMatcher extends Matcher {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ctx = stubContext();
|
final ctx = stubContext(options: options);
|
||||||
item.writeInto(ctx);
|
item.writeInto(ctx);
|
||||||
|
|
||||||
var matches = true;
|
var matches = true;
|
||||||
|
|
Loading…
Reference in New Issue