[GODRIVER-779] Why DateTime is int64 Created: 22/Jan/19  Updated: 11/Sep/19  Resolved: 25/Feb/19

Status: Closed
Project: Go Driver
Component/s: BSON
Affects Version/s: None
Fix Version/s: None

Type: Task Priority: Major - P3
Reporter: Oleg Romanov Assignee: Isabella Siu (Inactive)
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

go version go1.11 linux/amd64



 Description   

What's the reason of choosing int64 over time.Time?

Migration from mgo to official driver caused this unexpected behaviour.



 Comments   
Comment by Divjot Arora (Inactive) [ 06/Feb/19 ]

oleromd A couple of points here:

  1. We will not be changing the default behavior of time.Time, but it's very simple to create a new BSON registry that will decode BSON timestamps as time.Time instead of int64 when decoding into an interface.
  2. It's not recommended to go from BSON -> JSON -> a Go struct. BSON and JSON fields do not have a 1-1 mapping and this could cause issues with different types of structs. Furthermore, our BSON library creates a bson.D when decoding into an empty interface and it's currently not possible to uses json.Marshal to convert a bson.D into a JSON object. If your use case requires this, it's better to use the Extended JSON format, which is more inline with BSON. The Go driver has builtin support to do this.

This code snippet shows how to create a new registry and convert a decoded interface{} into a custom struct:

package main
 
import (
    "context"
    "fmt"
    "log"
    "time"
    "reflect"
    "github.com/mongodb/mongo-go-driver/bson"
    "github.com/mongodb/mongo-go-driver/bson/bsontype"
    "github.com/mongodb/mongo-go-driver/mongo"
    "github.com/mongodb/mongo-go-driver/mongo/options"
)
 
type Foo struct {
    T time.Time
}
 
func main() {
    ctx := context.Background()
    coll, err := initCollection(ctx)
    if err != nil {
        log.Fatal(err)
    }
 
    c, _ := coll.Find(ctx, bson.D{})
    for c.Next(ctx) {
        var obj interface{}
        if err := c.Decode(&obj); err != nil {
            log.Fatal(err)
        }
 
        foo, err := convertInterfaceToFoo(obj)
        if err != nil {
            log.Fatal(err)
        }
 
        fmt.Println(foo)
    }
}
 
func initCollection(ctx context.Context) (*mongo.Collection, error) {
    reg := bson.NewRegistryBuilder().RegisterTypeMapEntry(bsontype.Timestamp, reflect.TypeOf(time.Time{})).Build()
    clientOptions := options.Client().SetRegistry(reg)
    client, err := mongo.NewClientWithOptions("mongodb://localhost:27017", clientOptions)
    if err != nil {
        return nil, err
    }
 
    err = client.Connect(ctx)
    if err != nil {
        return nil, err
    }
    
    db := client.Database("db")
    coll := db.Collection("coll")    foo := Foo{
        T: time.Now(),
    }
    _, err = coll.InsertOne(ctx, foo)
    if err != nil {
        return nil, err
    }
    
    return coll, nil
}
 
func convertInterfaceToFoo(obj interface{}) (Foo, error) {
    extJSONbytes, err := bson.MarshalExtJSON(obj, true, true)
    if err != nil {
        return Foo{}, err
    }
 
    var foo Foo
    err = bson.UnmarshalExtJSON(extJSONbytes, true, &foo)
    if err != nil {
        return Foo{}, err
    }
 
    return foo, nil
}

Comment by Oleg Romanov [ 31/Jan/19 ]

Whoops, I've made a little mistake in code (type castng should be replaced with marhalling/unmarshalling), sorry. But the main idea is the same. Here's small example:

awesomeProject3/example/mgo.go:

package example
 
import (
   "time"
   "fmt"
   "encoding/json"
 
   "gopkg.in/mgo.v2"
   "gopkg.in/mgo.v2/bson"
)
 
type StructOne struct {
   One time.Time `bson:"one"`
   Two string    `bson:"two"`
}
 
func MGODriver() {
   session, err := mgo.DialWithTimeout("mongodb://localhost", time.Second)
   if err != nil {
      panic(err)
   }
 
   example := StructOne{
      One: time.Now(),
      Two: "Some value",
   }
   collection := session.DB("mgo").C("one")
 
   if err := collection.Insert(example); err != nil {
      panic(err)
   }
 
   query := bson.M{"two": example.Two}
 
   var selectValue interface{}
   if err := collection.Find(query).One(&selectValue); err != nil {
      panic(err)
   }
 
   fmt.Println(selectValue)
 
   var structOne StructOne
   if marshaledValue, err := json.Marshal(selectValue); err != nil {
      panic(err)
   } else {
      if err := json.Unmarshal(marshaledValue, &structOne); err != nil {
         panic(err)
      }
   }
 
   fmt.Println(structOne)
}
 

awesomeProject3/example/mongo.go:

package example
 
import (
   "context"
   "time"
   "fmt"
 
   "github.com/mongodb/mongo-go-driver/mongo"
   "github.com/mongodb/mongo-go-driver/bson"
   "encoding/json"
)
 
func MongoGoDriver() {
   client, err := mongo.Connect(context.TODO(), "mongodb://localhost")
   if err != nil {
      panic(err)
   }
 
   database := client.Database("example")
   collection := database.Collection("one")
 
   object := StructOne{
      One: time.Now(),
      Two: "two",
   }
   collection.InsertOne(context.TODO(), object)
 
   filter := bson.M{"one": object.One}
   cursor, err := collection.Find(context.TODO(), filter)
   if err != nil {
      panic(err)
   }
 
   for cursor.Next(context.TODO()) {
      var object bson.M
      if err := cursor.Decode(&object); err != nil {
         panic(err)
      }
 
      fmt.Println(object)
 
      var structOne StructOne
      if marshaledValue, err := json.Marshal(object); err != nil {
         panic(err)
      } else {
         if err := json.Unmarshal(marshaledValue, &structOne); err != nil {
            // panic: parsing time "1548944996876" as ""2006-01-02T15:04:05Z07:00"": cannot parse "1548944996876" as """
            // Since type of object.One is int64, not time.Time
            // But everything goes ok if decoding is happening with StructOne type. Ex:
            // var object StructOne
            // if err := cursor.Decode(&object); err != nil {
            //      panic(err)
            // }
            // Field One is decoded correctly
            panic(err)
         }
      }
 
      fmt.Println(structOne)
   }
}

 

awesomeProject3/main.go

package main
 
import "awesomeProject3/example"
 
func main() {
   //example.MGODriver()
   example.MongoGoDriver()
}
 

 

So I wonder if DateTime as int64 can cause some other unexpected problems while migration

 

 

Comment by Divjot Arora (Inactive) [ 30/Jan/19 ]

oleromd I'm not sure this is possible. The default decoder for an empty interface will generate a bson.D and as far as I know, it's not possible to typecast a bson.D to a struct type. Can you link the code that accomplished this with mgo?

Comment by Oleg Romanov [ 29/Jan/19 ]

Thank you for reply, @divjot.arora

My point is that it is impossible to unmarshall cursor's result into empty interface and cast it later (mgo driver allows this feature).  And it also brakes generic alike code.  Since now I have to implement low-level methods for each object type (it's a little bit dirty).

Ex, I have 3 structures: StructOne, StructTwo, StructThree. And I don't want to re-implement 3 methods for each structure. Instead I want to use one method (with usage of empty interface) since there are no generics.

Will DateTime still be int64? 

Comment by Divjot Arora (Inactive) [ 29/Jan/19 ]

oleromd DateTimes are serialized to the server as int64. When you unmarshal the cursor's result into an interface, the datetime will be unmarshalled as int64 as well. Instead, you can umarshal directly to the SomeObject type.

for cursor.Next(context.TODO()) {
    var someObj SomeObject
    if err := cursor.Decode(&someObj); err != nil {
        panic(err)
    }
    
    fmt.Println(someObj)
}

Comment by Oleg Romanov [ 22/Jan/19 ]

Example:

package main
 
import (
   "context"
   "time"
   "fmt"
 
   "github.com/mongodb/mongo-go-driver/mongo"
   "github.com/mongodb/mongo-go-driver/bson"
)
 
type SomeObject struct {
   A time.Time `bson:"a"`
}
 
func main() {
   client, err := mongo.Connect(context.TODO(), "mongodb://localhost")
   if err != nil {
      panic(err)
   }
 
   database := client.Database("example")
   collection := database.Collection("one")
 
   object := SomeObject{A: time.Now()}
   collection.InsertOne(context.TODO(), object)
 
   filter := bson.M{"a": object.A}
   cursor, err := collection.Find(context.TODO(), filter)
   if err != nil {
      panic(err)
   }
 
   for cursor.Next(context.TODO()) {
      var object interface{}
      if err := cursor.Decode(&object); err != nil {
         panic(err)
      }
      fmt.Println(object)
 
      if _, ok := object.(SomeObject); !ok {
         fmt.Println("Unexpected type of a field. Expected time.Time, got int64")
      }
   }
}
 

Generated at Thu Feb 08 08:34:56 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.