Skip to content

Round-tripped Firestore Timestamp fields have altered nanoseconds #14580

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

Open
natep opened this issue Mar 14, 2025 · 4 comments
Open

Round-tripped Firestore Timestamp fields have altered nanoseconds #14580

natep opened this issue Mar 14, 2025 · 4 comments
Assignees

Comments

@natep
Copy link

natep commented Mar 14, 2025

Description

Expected Result:
When round-tripping a Timestamp through Firestore, it is exactly the same before and after.

Actual Result:
The nanosecond field of the Timestamp is consistently different.

Why Is This A Problem:
It makes data consistency checking very difficult. It may result in spurious change notifications.

Reproducing the issue

This simple XCTest demonstrates the problem:

struct TestStruct: Codable, Hashable {
    let timestamp: Timestamp
}

func testTimestampRoundtrip() async throws {
    
    let id = UUID().uuidString
    let initial = TestStruct(timestamp: .init(date: Date.now))
    
    try myCollection.document(id).setData(from: initial)
    
    let roundTripped = try await myCollection.document(id).getDocument(as: TestStruct.self)
    
    XCTAssertEqual(initial, roundTripped)
}

The assert will fail with an error like:

XCTAssertEqual failed:
("TestStruct(timestamp: <FIRTimestamp: seconds=1741964292 nanoseconds=868173122>)")
is not equal to
("TestStruct(timestamp: <FIRTimestamp: seconds=1741964292 nanoseconds=868173000>)")

Note that my test framework is configured to hit a local Firebase emulator instead of the real infrastructure - I don't know if that makes a difference. I also tried specifying cache as the source for getDocument(as:), but that didn't make a difference.

This does not appear to be a problem with encoding and decoding, because this test works just fine:

func testTimestampCoding() throws {
    
    let initial = TestStruct(timestamp: .init(date: Date.now))
    
    let encoder = Firestore.Encoder()
    let encoded = try encoder.encode(initial)
    
    let decoder = Firestore.Decoder()
    let decoded = try decoder.decode(TestStruct.self, from: encoded)
    
    XCTAssertEqual(initial, decoded)
}

Firebase SDK Version

11.7.0

Xcode Version

16.2

Installation Method

Swift Package Manager

Firebase Product(s)

Firestore

Targeted Platforms

iOS

Relevant Log Output

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
{
  "originHash" : "279a5cbd0a3b875be5bd9e97c69521ee23b546261f86798aaaf0e3bdbbebfc53",
  "pins" : [
    {
      "identity" : "abseil-cpp-binary",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/abseil-cpp-binary.git",
      "state" : {
        "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27",
        "version" : "1.2024011602.0"
      }
    },
    {
      "identity" : "app-check",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/app-check.git",
      "state" : {
        "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
        "version" : "11.2.0"
      }
    },
    {
      "identity" : "cocoalumberjack",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
      "state" : {
        "revision" : "4b8714a7fb84d42393314ce897127b3939885ec3",
        "version" : "3.8.5"
      }
    },
    {
      "identity" : "firebase-ios-sdk",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/firebase-ios-sdk.git",
      "state" : {
        "revision" : "0d885d28250fb1196b614bc9455079b75c531f72",
        "version" : "11.7.0"
      }
    },
    {
      "identity" : "googleappmeasurement",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleAppMeasurement.git",
      "state" : {
        "revision" : "be0881ff728eca210ccb628092af400c086abda3",
        "version" : "11.7.0"
      }
    },
    {
      "identity" : "googledatatransport",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleDataTransport.git",
      "state" : {
        "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
        "version" : "10.1.0"
      }
    },
    {
      "identity" : "googleutilities",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleUtilities.git",
      "state" : {
        "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
        "version" : "8.0.2"
      }
    },
    {
      "identity" : "grpc-binary",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/grpc-binary.git",
      "state" : {
        "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083",
        "version" : "1.65.1"
      }
    },
    {
      "identity" : "gtm-session-fetcher",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/gtm-session-fetcher.git",
      "state" : {
        "revision" : "85b7b231882c3c472b8bda4fb495324d3f19bab6",
        "version" : "4.2.0"
      }
    },
    {
      "identity" : "interop-ios-for-google-sdks",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
      "state" : {
        "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
        "version" : "100.0.0"
      }
    },
    {
      "identity" : "leveldb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/leveldb.git",
      "state" : {
        "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
        "version" : "1.22.5"
      }
    },
    {
      "identity" : "nanopb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/nanopb.git",
      "state" : {
        "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
        "version" : "2.30910.0"
      }
    },
    {
      "identity" : "promises",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/promises.git",
      "state" : {
        "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
        "version" : "2.4.0"
      }
    },
    {
      "identity" : "swift-log",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-log",
      "state" : {
        "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
        "version" : "1.6.2"
      }
    },
    {
      "identity" : "swift-protobuf",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-protobuf.git",
      "state" : {
        "revision" : "ebc7251dd5b37f627c93698e4374084d98409633",
        "version" : "1.28.2"
      }
    }
  ],
  "version" : 3
}

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
Replace this line with the contents of your Podfile.lock!
@google-oss-bot
Copy link

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@MarkDuckworth
Copy link
Contributor

MarkDuckworth commented Mar 14, 2025

Hi @natep, this is a limitation of Firestore's storage of date and time values:

When stored in Cloud Firestore, precise only to microseconds; any additional precision is rounded down.

Firestore is offered as part of the Firebase SDKs and uses Firebase's Timestamp class. This Timestamp class offers nanosecond precision, and is aligned with Google's proto timestamp format.

Any value you write to Firestore, either the SDK cache or the Firestore backend, should have the additional precision rounded down. That should ease your concern about spurious change notifications.

@natep
Copy link
Author

natep commented Mar 14, 2025

@MarkDuckworth thanks for taking a look at this.

Part of my concern is that it makes it harder to for me to compare my own data structures that use Timestamp. I.e. if I write my struct into Firestore, then pull it back out later, via simple equality comparison it looks to my code as if those fields had changed.

Do other parts of the Firebase iOS SDK actually use the nanosecond precision? What would be ideal is: if the SDK can only be precise to microseconds, that Timestamp would round to that precision in its initializer. Or at least do that in the initializer that takes a Swift Date.

But I get that this is a big SDK, and probably you can't undertake such a large change.

@MarkDuckworth
Copy link
Contributor

Do other parts of the Firebase iOS SDK actually use the nanosecond precision?

A quick internal search indicated that Firebase Functions support nanosecond precision. I'm not certain about others.

Your concern is noted, and this type of usability feedback is always valuable. However, I don't see this changing in the near term. I'm reaching out to the iOS team about a Timestamp initializer that could truncate to microseconds (cc: @ncooke3).

One alternative option is to convert your Date instance to number of nanoseconds since epoch. I don't know how easy this is in Swift, but I'm assuming there is a solution. Firestore supports Int64, so this should support nanosecond precision until year ~2262.

@ncooke3 ncooke3 unpinned this issue Mar 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants