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 |
- null_suffix
null_suffix results in the null data value for the value type defined by the suffix.
data item with the value type defined by the suffix.
version 14.3.0
parameter<float32> param_null_f := null_f;
result: param_null_f = null
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.
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_pointwas expected NULL for orphan addresses (no matching verblijfsobject); it's never NULL. -
lp/point_srcwas expected NULL when the rjoin can't find a matching ligplaats; it's never NULL either. -
dbg_geom_X_mm's minimum is−1, notINT32_MIN— and#Nulls = 0. The hypothesis is confirmed: the rjoin's no-match path is producing defined ipoint components of−1, which is0xFFFFFFFF(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.