firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.52k stars 3.92k forks source link

🐛 [cloud_firestore] Timestamp seconds out of range: -62135598540 #12410

Open grundid opened 4 months ago

grundid commented 4 months ago

Bug report

When saving a DateTime with Time of 00:00 and Date 1,1,1 the above error is thrown.

Steps to reproduce

Steps to reproduce the behavior:

  1. Save a document with DateTime.utc(1) or DateTime(1)

Expected behavior

When I output the seconds value of the DateTime.utc(1) object I get -62135596800 (0001-01-01 00:00:00.000Z) When I output the seconds value of a DateTime(1) in my local timezone (CET) I get -62135600400 (0001-01-01 00:00:00.000) So the _kStartOfTime constant should be a little bit lower.

Lyokone commented 4 months ago

Hello @grundid, can you confirm on which device/os this is happening?

grundid commented 4 months ago

I've made the outputs on a MacOS and I have a crashlytics report from an iPhone user.

danagbemava-nc commented 4 months ago

Reproducible using the plugin example app. Tested with the code sample below on android and iOS. Reproduced on both platforms.

code sample ```dart // Copyright 2020, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'firebase_options.dart'; /// Requires that a Firestore emulator is running locally. /// See https://firebase.flutter.dev/docs/firestore/usage#emulator-usage bool shouldUseFirestoreEmulator = false; Future loadBundleSetup(int number) async { // endpoint serves a bundle with 3 documents each containing // a 'number' property that increments in value 1-3. final url = Uri.https('api.rnfirebase.io', '/firestore/e2e-tests/bundle-$number'); final response = await http.get(url); String string = response.body; return Uint8List.fromList(string.codeUnits); } Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseFirestore.instance.settings = const Settings( persistenceEnabled: true, ); if (shouldUseFirestoreEmulator) { FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080); } runApp(FirestoreExampleApp()); } /// A reference to the list of movies. /// We are using `withConverter` to ensure that interactions with the collection /// are type-safe. final moviesRef = FirebaseFirestore.instance .collection('firestore-example-app') .withConverter( fromFirestore: (snapshots, _) => Movie.fromJson(snapshots.data()!), toFirestore: (movie, _) => movie.toJson(), ); /// The different ways that we can filter/sort movies. enum MovieQuery { year, likesAsc, likesDesc, rated, sciFi, fantasy, } extension on Query { /// Create a firebase query from a [MovieQuery] Query queryBy(MovieQuery query) { switch (query) { case MovieQuery.fantasy: return where('genre', arrayContainsAny: ['fantasy']); case MovieQuery.sciFi: return where('genre', arrayContainsAny: ['sci-fi']); case MovieQuery.likesAsc: case MovieQuery.likesDesc: return orderBy('likes', descending: query == MovieQuery.likesDesc); case MovieQuery.year: return orderBy('year', descending: true); case MovieQuery.rated: return orderBy('rated', descending: true); } } } /// The entry point of the application. /// /// Returns a [MaterialApp]. class FirestoreExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Firestore Example App', theme: ThemeData.dark(), home: const Scaffold( body: Center(child: FilmList()), ), ); } } /// Holds all example app films class FilmList extends StatefulWidget { const FilmList({Key? key}) : super(key: key); @override _FilmListState createState() => _FilmListState(); } class _FilmListState extends State { MovieQuery query = MovieQuery.year; @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () async { await FirebaseFirestore.instance .collection('firestore-example-app') .add({ 'genre': ['fantasy'], 'likes': 0, 'poster': 'https://www.example.com/poster.jpg', 'rated': 'PG-13', 'runtime': '2h 49m', 'title': 'LOTR: Return of the King', 'year': 2003, 'created_at': DateTime(1), }); }, ), appBar: AppBar( title: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Firestore Example: Movies'), // This is a example use for 'snapshots in sync'. // The view reflects the time of the last Firestore sync; which happens any time a field is updated. StreamBuilder( stream: FirebaseFirestore.instance.snapshotsInSync(), builder: (context, _) { return Text( 'Latest Snapshot: ${DateTime.now()}', style: Theme.of(context).textTheme.bodySmall, ); }, ), ], ), actions: [ PopupMenuButton( onSelected: (value) => setState(() => query = value), icon: const Icon(Icons.sort), itemBuilder: (BuildContext context) { return [ const PopupMenuItem( value: MovieQuery.year, child: Text('Sort by Year'), ), const PopupMenuItem( value: MovieQuery.rated, child: Text('Sort by Rated'), ), const PopupMenuItem( value: MovieQuery.likesAsc, child: Text('Sort by Likes ascending'), ), const PopupMenuItem( value: MovieQuery.likesDesc, child: Text('Sort by Likes descending'), ), const PopupMenuItem( value: MovieQuery.fantasy, child: Text('Filter genre fantasy'), ), const PopupMenuItem( value: MovieQuery.sciFi, child: Text('Filter genre sci-fi'), ), ]; }, ), PopupMenuButton( onSelected: (value) async { switch (value) { case 'reset_likes': return _resetLikes(); case 'aggregate': // Count the number of movies final _count = await FirebaseFirestore.instance .collection('firestore-example-app') .count() .get(); print('Count: ${_count.count}'); // Average the number of likes final _average = await FirebaseFirestore.instance .collection('firestore-example-app') .aggregate(average('likes')) .get(); print('Average: ${_average.getAverage('likes')}'); // Sum the number of likes final _sum = await FirebaseFirestore.instance .collection('firestore-example-app') .aggregate(sum('likes')) .get(); print('Sum: ${_sum.getSum('likes')}'); // In one query final _all = await FirebaseFirestore.instance .collection('firestore-example-app') .aggregate( average('likes'), sum('likes'), count(), ) .get(); print('Average: ${_all.getAverage('likes')} ' 'Sum: ${_all.getSum('likes')} ' 'Count: ${_all.count}'); return; case 'load_bundle': Uint8List buffer = await loadBundleSetup(2); LoadBundleTask task = FirebaseFirestore.instance.loadBundle(buffer); final list = await task.stream.toList(); print( list.map((e) => e.totalDocuments), ); print( list.map((e) => e.bytesLoaded), ); print( list.map((e) => e.documentsLoaded), ); print( list.map((e) => e.totalBytes), ); print( list, ); LoadBundleTaskSnapshot lastSnapshot = list.removeLast(); print(lastSnapshot.taskState); print( list.map((e) => e.taskState), ); return; default: return; } }, itemBuilder: (BuildContext context) { return [ const PopupMenuItem( value: 'reset_likes', child: Text('Reset like counts (WriteBatch)'), ), const PopupMenuItem( value: 'aggregate', child: Text('Get aggregate data'), ), const PopupMenuItem( value: 'load_bundle', child: Text('Load bundle'), ), ]; }, ), ], ), body: StreamBuilder>( stream: moviesRef.queryBy(query).snapshots(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Text(snapshot.error.toString()), ); } if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final data = snapshot.requireData; return ListView.builder( itemCount: data.size, itemBuilder: (context, index) { return _MovieItem( data.docs[index].data(), data.docs[index].reference, ); }, ); }, ), ); } Future _resetLikes() async { final movies = await moviesRef.get( const GetOptions( serverTimestampBehavior: ServerTimestampBehavior.previous, ), ); WriteBatch batch = FirebaseFirestore.instance.batch(); for (final movie in movies.docs) { batch.update(movie.reference, {'likes': 0}); } await batch.commit(); } } /// A single movie row. class _MovieItem extends StatelessWidget { _MovieItem(this.movie, this.reference); final Movie movie; final DocumentReference reference; /// Returns the movie poster. Widget get poster { return SizedBox( width: 100, child: Image.network(movie.poster), ); } /// Returns movie details. Widget get details { return Padding( padding: const EdgeInsets.only(left: 8, right: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ title, metadata, genres, Likes( reference: reference, currentLikes: movie.likes, ), ], ), ); } /// Return the movie title. Widget get title { return Text( '${movie.title} (${movie.year})', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ); } /// Returns metadata about the movie. Widget get metadata { return Padding( padding: const EdgeInsets.only(top: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(right: 8), child: Text('Rated: ${movie.rated}'), ), Text('Runtime: ${movie.runtime}'), ], ), ); } /// Returns a list of genre movie tags. List get genreItems { return [ for (final genre in movie.genre) Padding( padding: const EdgeInsets.only(right: 2), child: Chip( backgroundColor: Colors.lightBlue, label: Text( genre, style: const TextStyle(color: Colors.white), ), ), ), ]; } /// Returns all genres. Widget get genres { return Padding( padding: const EdgeInsets.only(top: 8), child: Wrap( children: genreItems, ), ); } @override Widget build(BuildContext context) { if(movie.createdAt != null) { print("Movie created at: ${movie.createdAt!.millisecondsSinceEpoch} ${movie.createdAt!.toDate()}"); } return Padding( padding: const EdgeInsets.only(bottom: 4, top: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ poster, Flexible(child: details), ], ), ); } } /// Displays and manages the movie 'like' count. class Likes extends StatefulWidget { /// Constructs a new [Likes] instance with a given [DocumentReference] and /// current like count. Likes({ Key? key, required this.reference, required this.currentLikes, }) : super(key: key); /// The reference relating to the counter. final DocumentReference reference; /// The number of current likes (before manipulation). final int currentLikes; @override _LikesState createState() => _LikesState(); } class _LikesState extends State { /// A local cache of the current likes, used to immediately render the updated /// likes count after an update, even while the request isn't completed yet. late int _likes = widget.currentLikes; Future _onLike() async { final currentLikes = _likes; // Increment the 'like' count straight away to show feedback to the user. setState(() { _likes = currentLikes + 1; }); try { // Update the likes using a transaction. // We use a transaction because multiple users could update the likes count // simultaneously. As such, our likes count may be different from the likes // count on the server. int newLikes = await FirebaseFirestore.instance .runTransaction((transaction) async { DocumentSnapshot movie = await transaction.get(widget.reference); if (!movie.exists) { throw Exception('Document does not exist!'); } int updatedLikes = movie.data()!.likes + 1; transaction.update(widget.reference, {'likes': updatedLikes}); return updatedLikes; }); // Update with the real count once the transaction has completed. setState(() => _likes = newLikes); } catch (e, s) { print(s); print('Failed to update likes for document! $e'); // If the transaction fails, revert back to the old count setState(() => _likes = currentLikes); } } @override void didUpdateWidget(Likes oldWidget) { super.didUpdateWidget(oldWidget); // The likes on the server changed, so we need to update our local cache to // keep things in sync. Otherwise if another user updates the likes, // we won't see the update. if (widget.currentLikes != oldWidget.currentLikes) { _likes = widget.currentLikes; } } @override Widget build(BuildContext context) { return Row( children: [ IconButton( iconSize: 20, onPressed: _onLike, icon: const Icon(Icons.favorite), ), Text('$_likes likes'), ], ); } } @immutable class Movie { Movie({ required this.genre, required this.likes, required this.poster, required this.rated, required this.runtime, required this.title, required this.year, this.createdAt, }); Movie.fromJson(Map json) : this( genre: (json['genre']! as List).cast(), likes: json['likes']! as int, poster: json['poster']! as String, rated: json['rated']! as String, runtime: json['runtime']! as String, title: json['title']! as String, year: json['year']! as int, createdAt: json['created_at'] as Timestamp?, ); final String poster; final int likes; final String title; final int year; final String runtime; final String rated; final List genre; final Timestamp? createdAt; Map toJson() { return { 'genre': genre, 'likes': likes, 'poster': poster, 'rated': rated, 'runtime': runtime, 'title': title, 'year': year, 'created_at': createdAt, }; } } ```
Lyokone commented 4 months ago

Thanks for the report, it seems that there are some differences between how milliseconds from epoch is calculated on Dart and the native SDKs. I'm looking into it