From 0c7056373e56e96869b71db15165e6bdfa8bc98b Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 3 Sep 2026 10:40:44 +0200 Subject: [PATCH] Answer the value at a timestamp from the MEOS entry that reports absence WITNESS: a temporal value holds nothing outside its own span, and MEOS says so by answering NULL. `CreateTemporal` reads `inner.temptype` with no nil test, so that answer is dereferenced: removing the guard this commit adds takes `ExampleCreateTemporal_absent` to panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x4] and 119 call sites reach `CreateTemporal` with a result that can be NULL. The five `ValueAtTimestamp` accessors compose that path -- each one calls `TemporalAtTimestamptz`, discards the conversion error with `_`, and reads the instant -- where MEOS publishes the question directly: `tbool_value_at_timestamptz(temp, t, strict, &value)` reports whether a value is there and writes it out, with twins for tint, tfloat, ttext and tgeo. The generated package already wraps all five, so the accessors delegate to them across the pointer boundary rather than composing a restriction. THE SHAPE COMES FROM THE SIBLING BINDINGS, WHICH SEPARATE ABSENCE FROM ERROR. MEOS.NET's generated accessors return a nullable -- `public bool? ValueAtTimestamptz(DateTime t, bool strict)`, and `int?` / `long?` / `double?` for its siblings -- with `Assert.IsNull` covering the absent case in its own test, while errors travel out of band through `SafeExecution`. PyMEOS-CFFI agrees: `if result: return out_result[0] ... return None`, with `_check_error()` raising separately. So absence is an optional result, not a failure. Go spells an optional as a second result, and the generated wrapper here already returns `(found, value, error)`, so the accessors return `(value, ok, error)`: `ok` false leaves the value at its zero and the error nil, and the error is reserved for a MEOS failure. MEASURED: the suite reads 121 result lines, 0 skipped, against 118 before. The three added examples pin the absent case for the boolean and float accessors and for `CreateTemporal` itself, and each fails with the guard removed, so they witness this change rather than accompany it. --- .github/workflows/build.yml | 2 +- example_main_tbool_test.go | 6 ++--- example_main_tfloat_test.go | 6 ++--- example_value_absent_test.go | 43 ++++++++++++++++++++++++++++++++++++ interfaces.go | 7 ++++++ main_tbool.go | 21 ++++++++++++++---- main_tfloat.go | 21 ++++++++++++++---- main_tint.go | 21 ++++++++++++++---- main_tpoint.go | 20 +++++++++++++---- main_ttext.go | 18 +++++++++++---- 10 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 example_value_absent_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a219ef..2980a50 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,4 +118,4 @@ jobs: uses: MobilityDB/MEOS-API/.github/actions/check-test-outcome@master with: log: test.log - min-tests: "118" + min-tests: "121" diff --git a/example_main_tbool_test.go b/example_main_tbool_test.go index b4f94fd..035c005 100644 --- a/example_main_tbool_test.go +++ b/example_main_tbool_test.go @@ -30,10 +30,10 @@ func ExampleTBoolOut() { func ExampleTBoolValueAtTimestamp() { g_is := NewTBoolSeq("{FALSE@2022-10-01, FALSE@2022-10-02,TRUE@2022-10-03}") ts, _ := time.Parse("2006-01-02", "2022-10-01") - res := TBoolValueAtTimestamp(g_is, ts) - fmt.Println(res) + res, ok, err := TBoolValueAtTimestamp(g_is, ts) + fmt.Println(res, ok, err) // Output: - // false + // false true } func ExampleEverEqTBoolBool() { diff --git a/example_main_tfloat_test.go b/example_main_tfloat_test.go index ef809b6..87d637c 100644 --- a/example_main_tfloat_test.go +++ b/example_main_tfloat_test.go @@ -32,8 +32,8 @@ func ExampleTFloatValues() { func ExampleTFloatValueAtTimestamp() { tf_seq := TFloatIn("{1.2@2022-10-01, 2.3@2022-10-02,3.4@2022-10-03}", &TFloatSeq{}) ts, _ := time.Parse("2006-01-02", "2022-10-01") - res := TFloatValueAtTimestamp(tf_seq, ts) - fmt.Println(res) + res, ok, err := TFloatValueAtTimestamp(tf_seq, ts) + fmt.Println(res, ok, err) // Output: - // 1.2 + // 1.2 true } diff --git a/example_value_absent_test.go b/example_value_absent_test.go new file mode 100644 index 0000000..8fa4e15 --- /dev/null +++ b/example_value_absent_test.go @@ -0,0 +1,43 @@ +package gomeos + +// A temporal value holds nothing outside its own span, and MEOS says so by +// answering NULL. These examples pin what the accessors do with that answer: +// they report absence through a second result and leave the error free for a +// MEOS failure, and the nil handle it arrives as is a value the caller can test +// rather than something to dereference. + +import ( + "fmt" + "time" +) + +func ExampleTBoolValueAtTimestamp_absent() { + tb := NewTBoolSeq("{FALSE@2022-10-01, FALSE@2022-10-02, TRUE@2022-10-03}") + outside, _ := time.Parse("2006-01-02", "2021-01-01") + + value, ok, err := TBoolValueAtTimestamp(tb, outside) + fmt.Println(value, ok, err) + // Output: + // false false +} + +func ExampleTFloatValueAtTimestamp_absent() { + tf := TFloatIn("{1.2@2022-10-01, 2.3@2022-10-02, 3.4@2022-10-03}", &TFloatSeq{}) + outside, _ := time.Parse("2006-01-02", "2021-01-01") + + value, ok, err := TFloatValueAtTimestamp(tf, outside) + fmt.Println(value, ok, err) + // Output: + // 0 false +} + +func ExampleCreateTemporal_absent() { + tb := NewTBoolSeq("{FALSE@2022-10-01, TRUE@2022-10-03}") + outside, _ := time.Parse("2006-01-02", "2021-01-01") + + // Restricting to a moment the value does not cover leaves nothing, which + // reaches Go as a nil Temporal rather than a handle onto nothing. + fmt.Println(TemporalAtTimestamptz(tb, outside) == nil) + // Output: + // true +} diff --git a/interfaces.go b/interfaces.go index bc6e642..2bdc506 100644 --- a/interfaces.go +++ b/interfaces.go @@ -97,7 +97,14 @@ type NumSpan interface { IsNumSpan() bool } +// CreateTemporal wraps a MEOS temporal value in the Go type its temptype and +// subtype name. A MEOS entry answers NULL for an empty result — a restriction +// that selects nothing, for one — so a nil pointer is a value rather than a +// fault, and it arrives back as a nil Temporal for the caller to test. func CreateTemporal(inner *C.Temporal) Temporal { + if inner == nil { + return nil + } meosType := inner.temptype subtype := inner.subtype // meosType MeosType, subtype MeosTemporalSubtype diff --git a/main_tbool.go b/main_tbool.go index 50da233..98083f0 100644 --- a/main_tbool.go +++ b/main_tbool.go @@ -11,6 +11,8 @@ import ( "fmt" "time" "unsafe" + + "github.com/MobilityDB/GoMEOS/functions" ) type TBoolInst struct { @@ -198,10 +200,21 @@ func TBoolEndValue[TB TBool](tb TB) bool { return bool(cValue) } -// TBoolValueAtTimestamp Return the value of a temporal boolean at a timestamptz -func TBoolValueAtTimestamp[TB TBool](tb TB, ts time.Time) bool { - tboolinst, _ := TemporalToTBoolInst(TemporalAtTimestamptz(tb, ts)) - return TBoolStartValue(tboolinst) +// TBoolValueAtTimestamp Return the value of a temporal boolean at a timestamptz. +// The second result reports whether the value holds anything at that moment; +// false leaves the first at its zero value. An error is reserved for a MEOS +// failure, which is a different thing from absence. +func TBoolValueAtTimestamp[TB TBool](tb TB, ts time.Time) (bool, bool, error) { + found, value, err := functions.TboolValueAtTimestamptz( + functions.TemporalFromPointer(unsafe.Pointer(tb.Inner())), + int64(DatetimeToTimestamptz(ts)), true) + if err != nil { + return false, false, err + } + if !found { + return false, false, nil + } + return value, true, nil } // AlwaysEqTBoolBool Return true if a temporal boolean is always equal to a boolean diff --git a/main_tfloat.go b/main_tfloat.go index ac04d6e..44b0293 100644 --- a/main_tfloat.go +++ b/main_tfloat.go @@ -11,6 +11,8 @@ import ( "fmt" "time" "unsafe" + + "github.com/MobilityDB/GoMEOS/functions" ) type TFloatInst struct { @@ -368,10 +370,21 @@ func TFloatMinusValue[TF TFloat](tf TF, value float64) Temporal { return CreateTemporal(c_tbools) } -// TFloatValueAtTimestamp Return the value of a temporal float at a timestamptz -func TFloatValueAtTimestamp[TF TFloat](tf TF, ts time.Time) float64 { - tfloatinst, _ := TemporalToTFloatInst(TemporalAtTimestamptz(tf, ts)) - return TFloatStartValue(tfloatinst) +// TFloatValueAtTimestamp Return the value of a temporal float at a timestamptz. +// The second result reports whether the value holds anything at that moment; +// false leaves the first at its zero value. An error is reserved for a MEOS +// failure, which is a different thing from absence. +func TFloatValueAtTimestamp[TF TFloat](tf TF, ts time.Time) (float64, bool, error) { + found, value, err := functions.TfloatValueAtTimestamptz( + functions.TemporalFromPointer(unsafe.Pointer(tf.Inner())), + int64(DatetimeToTimestamptz(ts)), true) + if err != nil { + return 0, false, err + } + if !found { + return 0, false, nil + } + return value, true, nil } // TFloatDerivative Return the derivative of a temporal number diff --git a/main_tint.go b/main_tint.go index a1787a2..facfaa0 100644 --- a/main_tint.go +++ b/main_tint.go @@ -11,6 +11,8 @@ import ( "fmt" "time" "unsafe" + + "github.com/MobilityDB/GoMEOS/functions" ) type TIntInst struct { @@ -252,10 +254,21 @@ func TIntMaxValue[TB TInt](tb TB) int { return int(cValue) } -// TIntValueAtTimestamp Return the value of a temporal int at a timestamptz -func TIntValueAtTimestamp[TF TInt](tf TF, ts time.Time) int { - tintinst, _ := TemporalToTIntInst(TemporalAtTimestamptz(tf, ts)) - return TIntStartValue(tintinst) +// TIntValueAtTimestamp Return the value of a temporal int at a timestamptz. +// The second result reports whether the value holds anything at that moment; +// false leaves the first at its zero value. An error is reserved for a MEOS +// failure, which is a different thing from absence. +func TIntValueAtTimestamp[TF TInt](tf TF, ts time.Time) (int, bool, error) { + found, value, err := functions.TintValueAtTimestamptz( + functions.TemporalFromPointer(unsafe.Pointer(tf.Inner())), + int64(DatetimeToTimestamptz(ts)), true) + if err != nil { + return 0, false, err + } + if !found { + return 0, false, nil + } + return value, true, nil } // AlwaysLtTIntInt Return true if a temporal integer is always less than an integer diff --git a/main_tpoint.go b/main_tpoint.go index 866f3ad..1642b3e 100644 --- a/main_tpoint.go +++ b/main_tpoint.go @@ -11,6 +11,8 @@ import ( "fmt" "time" "unsafe" + + "github.com/MobilityDB/GoMEOS/functions" ) // TPointOut Return a temporal geometry/geography point from its Well-Known Text (WKT) representation @@ -66,10 +68,20 @@ func TPointEndValue[TP TPoint](tp TP) *Geom { return &Geom{_inner: cValue} } -// TPointValueAtTimestamp Return the value of a temporal point at a timestamptz -func TPointValueAtTimestamp[TP TPoint](tp TP, ts time.Time) *Geom { - tpointinst, _ := TemporalToGeomPointInst(TemporalAtTimestamptz(tp, ts)) - return TPointStartValue(tpointinst) +// TPointValueAtTimestamp Return the value of a temporal point at a timestamptz. +// A temporal value that holds nothing at that moment yields an error rather than +// a value. +func TPointValueAtTimestamp[TP TPoint](tp TP, ts time.Time) (*Geom, bool, error) { + found, value, err := functions.TgeoValueAtTimestamptz( + functions.TemporalFromPointer(unsafe.Pointer(tp.Inner())), + int64(DatetimeToTimestamptz(ts)), true) + if err != nil { + return nil, false, err + } + if !found { + return nil, false, nil + } + return &Geom{_inner: (*C.GSERIALIZED)(value.Pointer())}, true, nil } // TPointValueSet Return the array of base values of a temporal geometry point diff --git a/main_ttext.go b/main_ttext.go index b6e3fcc..0b660b3 100644 --- a/main_ttext.go +++ b/main_ttext.go @@ -11,6 +11,8 @@ import "C" import ( "time" "unsafe" + + "github.com/MobilityDB/GoMEOS/functions" ) type TTextInst struct { @@ -191,10 +193,18 @@ func TTextMaxValue[TT TText](tt TT) string { return C.GoString(C.text_out(cValue)) } -// TTextValueAtTimestamp Return the value of a temporal text at a timestamptz -func TTextValueAtTimestamp[TT TText](tt TT, ts time.Time) string { - ttextinst, _ := TemporalToTTextInst(TemporalAtTimestamptz(tt, ts)) - return TTextStartValue(ttextinst) +// TTextValueAtTimestamp Return the value of a temporal text at a timestamptz. +// The second result reports whether the value holds anything at that moment; +// false leaves the first at its zero value. An error is reserved for a MEOS +// failure, which is a different thing from absence. +func TTextValueAtTimestamp[TT TText](tt TT, ts time.Time) (string, bool, error) { + found, value, err := functions.TtextValueAtTimestamptz( + functions.TemporalFromPointer(unsafe.Pointer(tt.Inner())), + int64(DatetimeToTimestamptz(ts)), true) + if err != nil { + return "", false, err + } + return value, found, nil } // TTextUpper Return a temporal text transformed to uppercase