Null - ObjectVision/GeoDMS GitHub Wiki

Constant functions null

The keyword Null is used in the GeoDMS to indicate missing data.

The constant: null_suffix can be used in expressions, it is important to use the version with the correct value type, see the following table:

Value type Name
uint8 null_b
uint16 null_w
uint32 null_u
uint64 null_u64
int8 null_c
int16 null_s
int32 null_i
int64 null_i64
float32 null_f
float64 null_d
spoint null_sp
wpoint null_wp
ipoint null_ip
upoint null_up
fpoint null_fp
dpoint null_dp

syntax

  • null_suffix

definition

null_suffix results in the null data value for the value type defined by the suffix.

applies to

data item with the value type defined by the suffix.

since

version 14.3.0

example

parameter<float32> param_null_f := null_f;

result: param_null_f = null

Internal sentinel encoding (and a worked debugging example)

Each value type encodes "null" with a fixed bit pattern. Knowing the exact pattern matters when diagnosing "I expected NULL but got a defined-looking value" bugs, because a sentinel from one type reinterpreted as another can produce surprising defined values.

Value type NULL sentinel (bits) If reinterpreted as same-width signed Int
UInt8 0xFF −1
UInt16 0xFFFF −1
UInt32 0xFFFFFFFF −1
UInt64 0xFFFF…FFFF −1
Int8 0x80 = INT8_MIN −128
Int16 0x8000 = INT16_MIN −32 768
Int32 0x80000000 = INT32_MIN −2 147 483 648
Float32 / Float64 NaN n/a
2D points (spoint, ipoint, fpoint, dpoint, …) both components = the type's NULL sentinel see signed/unsigned components above

Note that the NULL pattern for unsigned integer types is −1 (all-ones), while for signed integer types it is INT_MIN (sign bit only). If a NULL written as one type is byte-reinterpreted as the other, the resulting value is defined (and meaningless): a UInt32 NULL becomes Int32 −1, an Int32 NULL becomes UInt32 2 147 483 648, etc.

Worked example: diagnosing a NULL-loss bug with @statistics

In May 2026, the t060 BAG snapshot regression was producing 407 301 of 1 138 872 adres rows at coordinate (−0.001, −0.001) instead of the proper "no geometry" of the 19.x reference (which had POINT EMPTY / NaN). The cfg uses an rdc_mm ipoint grid (cell size 0.001 m, origin at 0), so the dpoint (−0.001, −0.001) corresponds to the ipoint (−1, −1).

−1 is not the Int32 NULL sentinel (INT32_MIN = −2 147 483 648). It is what you get when you reinterpret 0xFFFFFFFF (the UInt32 NULL sentinel) through an Int32 lens. So the working hypothesis was:

Somewhere in the rjoin chain, a UInt32 NULL row-relation is being byte-cast through to the result point's Int32 components instead of being translated to per-component INT32_MIN.

To confirm without re-writing the gpkg every iteration, diagnostic intermediates were added to adres with DisableStorage = "True":

attribute<bool>  dbg_vbo_isNull       := IsNull(impl/nummeraanduiding_met_geom/vbo_point),    DisableStorage = "True";
attribute<bool>  dbg_lp_src_isNull    := IsNull(impl/nummeraanduiding_met_geom/lp/point_src), DisableStorage = "True";
attribute<bool>  dbg_geom_mm_isNull   := IsNull(geometry_mm),                                 DisableStorage = "True";
attribute<int32> dbg_geom_X_mm        := PointCol(geometry_mm),                               DisableStorage = "True";
attribute<int32> dbg_geom_Y_mm        := PointRow(geometry_mm),                               DisableStorage = "True";

Then queried via GeoDmsRun … @statistics …:

GeoDmsRun.exe /S1 /S2 /S3 cfg.dms @file diag.txt @statistics ^
    .../adres/dbg_vbo_isNull ^
    .../adres/dbg_lp_src_isNull ^
    .../adres/dbg_geom_mm_isNull ^
    .../adres/dbg_geom_X_mm ^
    .../adres/dbg_geom_Y_mm

Output (abridged):

dbg_vbo_isNull       True: 0          False: 1,138,872
dbg_lp_src_isNull    True: 0          False: 1,138,872
dbg_geom_mm_isNull   True: 0          False: 1,138,872
dbg_geom_X_mm        Minimum: -1      Maximum: 171,423,377    #Nulls: 0
dbg_geom_Y_mm        Minimum: -1      Maximum: 479,558,859    #Nulls: 0

Reading these:

  • vbo_point was expected NULL for orphan addresses (no matching verblijfsobject); it's never NULL.
  • lp/point_src was expected NULL when the rjoin can't find a matching ligplaats; it's never NULL either.
  • dbg_geom_X_mm's minimum is −1, not INT32_MIN — and #Nulls = 0. The hypothesis is confirmed: the rjoin's no-match path is producing defined ipoint components of −1, which is 0xFFFFFFFF (UInt32 NULL) reinterpreted as Int32.

The bug is therefore in rjoin's handling of the no-match case for point-typed source attributes: the internal UInt32 row-relation NULL bytes are flowing through to the Int32 point components instead of being translated to per-component INT32_MIN. Downstream MakeDefined(vbo_point, lp/point, sp/point) then picks the (−1, −1) "defined" value over the genuinely NULL fallbacks, and the gpkg writer emits POINT(−0.001 −0.001) instead of POINT EMPTY.

Lesson: When debugging NULL propagation, @statistics on IsNull(...)-typed and component-extracted (PointCol / PointRow) intermediates is a very fast way to locate where a NULL gets converted to a defined-but-garbage value. Compare the observed minimum against the table above to identify which type's NULL sentinel was reinterpreted.

⚠️ **GitHub.com Fallback** ⚠️