How to return nulled time fields with a custom Go datasource?

I’m currently writing a custom datasource using Golang, for a query I need to return a table with entity’s informations, and one of this table column is a nullable time, but I can’t achieve to have it NULL.

Context:

An “entity” is an object returned by my MariaDB’s “events” table, it’s composed of a mere ID, Name and a OccurredAt column which is a DATETIME.

Everything works fine in my datasource, but I would like to optimize my queries by hidding the “OccurredAt” time field which can be NULL.
Currently, when the “occurred_at” of my event is NULL my query return an empty time.Time{} which display the “1754-08-30 22:53:02.128” date (which is normal), and so an example result would be:

ID | Name  | OccurredAt
1  | Event1 | 2024-10-01 13:35:30.614
2  | Event2 | 1754-08-30 22:53:02.128
3  | Event3 | 2024-09-30 15:14:30.232
4  | Event4 | 2024-09-10 13:24:31.089

(Note that here only my second event had a NULL’s occurred_at property)

In order to obtain this result, I’m just creating a simple frame with fields like this:

package data ("github.com/grafana/grafana-plugin-sdk-go/data")

// This function's the same as `NewStringField` or `NewInt64Field` with the according types
func NewTimeField(key, label string, values []time.Time) *data.Field {
	field := data.NewField(key, nil, values)
	field.Config = &data.FieldConfig{DisplayNameFromDS: label}
	return field
}

func GetEventsFrames(events []interface, q *query.QueryData) (data.Frames, error) {
	frame := data.NewFrame("operations")
	frame.RefID = q.RefID
	frame.SetMeta(&data.FrameMeta{PreferredVisualization: data.VisTypeTable})

	frame.Fields = append(frame.Fields,
		NewInt64Field("id", "ID", nil),
		NewStringField("name", "Name", nil),
		NewTimeField("occurred_at", "Occurred At", nil),
	)

	// events is my list of events returned by my database
	for _, e := range events {
		frame.AppendRow(
			e.ID,
			e.Name,
			// e.OccurredAt is a sql.NullTime, which is valid and checked beforehand
			e.OccurredAt.Time,
		)
	}

	return data.Frames{frame}, nil
}

What I tried

In order to display empty cells in my table when the events doesn’t have valid OccurredAt, I replaced my NewTimeField function by this one:

// It's the same as the previous function but `values` is a slice of pointers
func NewNullTimeField(key, label string, values []*time.Time) *data.Field {
	field := data.NewField(key, nil, values)
	field.Config = &data.FieldConfig{DisplayNameFromDS: label}
	return field
}

and in my function I did this:

for _, e := range events {
	var occurredAt *time.Time
	if e.OccurredAt.Valid {
		occurredAt = &e.OccurredAt.Time
	}

	frame.AppendRow(
		e.ID,
		e.Name,
		// Here `occurredAt` should be either nil or a valid `*time.Time`
		occurredAt,
	)
}

It should works just fine, it compile, it doesn’t panic, my table’s column are empty as excepted when OccurredAt’s nil, but all my other dates are now displayed as “1754-08-30 22:53:02.128” (because it’s equal to -6795364578872).
example:

ID | Name  | OccurredAt
1  | Event1 | 1754-08-30 22:53:02.128
2  | Event2 |
3  | Event3 | 1754-08-30 22:53:02.128
4  | Event4 | 1754-08-30 22:53:02.128

In the query inspector, we can see this:

name:"occurred_at"
type:"time"
	typeInfo:Object
		frame:"time.Time"
		nullable:true
	config:Object
		displayNameFromDS:"Occurred At"

The “nullable” property is true so I don’t really know where I get it wrong, has anybody any insights ?

Hi @Paul-Marie,

I haven’t got a quick way to test this specific issue. But you mentioned that time.Time{} defaulting to 1754-08-30 22:53:02.128 or -6795364578872 is normal.

Can you clarify why you believe this to be the case?

In Go, time.Time{} defaults to -62135596800 which is 0001-01-01 00:00:00 +0000 UTC (as per the official Go documentation). So it seems there is something else at play here which may be confusing matters.

Worth noting that the Go documentation suggests against passing time values as pointers:

Programs using times should typically store and pass them as values, not pointers. That is, time variables and struct fields should be of type time.Time, not *time.Time.

Hi,

I actually agree with you, but:

  • I wanted to use pointers in this situation because it’s the only way I found in order to display empty raw in my table panel (I want to display date which can not exists, and if they doesn’t exist I want an empty cell), so putting nil or *time.Time would’ve been convenient
  • I confirm that an empty time is passed down to frame.AppendRow(), which result to -6795364578872 when seeing the request on the Grafana’s query inspector, I can guarentee the time.Time{}'s value by logging it just before the .AppendRow() func. (But yeah I agree that it shouldn’t be the default value, but since I pass an empty time and it’s received like this, I assumed some nasty operation were made and it was “normal” ?

If you have other solution for me to display date with empty cells without using pointers feel free to tell me, I’m not a huge fan of this way too

What is the reason for needing to allow for nil timestamps for the OccuredAt field? What does it mean for an event to not have an associated timestamp?

The default value you’re seeing for a time.Time{} is rather strange. Can you try and see if t.IsZero() returns true (where t is your event’s OccuredAt time)? I suspect the answer here will be false, which would then indicates that the time does indeed have a non-zero/non-nil value.

You could for example try replacing your if statement with:

if e.OccurredAt.Valid && !e.OccurredAt.Time.IsZero() {
	occurredAt = &e.OccurredAt.Time
} else {
	occurredAt = nil // explicitly set to nil for invalid or zero dates
}