From 577284e4703924fb2f84aada06e5ea04ed6505b0 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sat, 15 Aug 2026 17:37:20 +0200 Subject: [PATCH 1/2] Compile the module against the installed MEOS umbrella header The module compiled against MobilityDB's source tree, where meos.h is not the header MEOS publishes. MEOS assembles the installed : it splices its PostgreSQL-compat definitions into it under #ifndef POSTGRES_H, which supplies Datum, DateADT, TimestampTz, Interval and the de-prefixed base I/O -- interval_in, date_in, timestamptz_in and their siblings, named without a meos_ prefix so they do not collide with a real PostgreSQL. None of that is in the source copy, so calls to those functions were implicitly declared and C99 rejects them. The build installs MEOS and compiles against that prefix. The preamble stops including the vendored , which defined POSTGRES_H first and made skip the whole spliced block, and takes directly rather than through it. The Datum accessors the spliced block leaves out are defined here, keyed on the pointer width exactly as PostgreSQL keys USE_FLOAT8_BYVAL on SIZEOF_VOID_P >= 8. With -sMEMORY64=1 a Datum is 8 bytes, so a TimestampTz or an int64 span bound rides inside it rather than behind a pointer; reading one through a pointer returns whatever the value addresses, which makes a bigint span report a neighbour it does not touch as adjacent. Both widths stay correct. --- Dockerfile | 9 +++- codegen/res/bindings_c_header.c.template | 67 +++++++++++++++++++++--- core/c-src/bindings.c | 67 +++++++++++++++++++++--- 3 files changed, 127 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc03c83..67b82fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -220,7 +220,12 @@ RUN PG_CONFIG_H=/root/MobilityDB/build/pgtypes/pg_config.h \ "$PG_CONFIG_H" # COMPILING: libmeos.a -RUN cmake --build /root/MobilityDB/build --target meos --parallel "$(nproc)" +# The install step produces the header this module compiles against. MEOS +# assembles the installed rather than shipping the source one: it +# splices the PostgreSQL-compat definitions and the de-prefixed base I/O into +# it, so interval_in, date_in and their siblings are declared only there. +RUN cmake --build /root/MobilityDB/build --target meos --parallel "$(nproc)" \ + && cmake --install /root/MobilityDB/build --prefix /root/meos-install # EMCC: link everything into meos.js + meos.wasm # @@ -264,7 +269,7 @@ RUN mkdir -p /app/wasm \ /root/gsl-wasm/lib/libgslcblas.a \ /root/json-c-install/lib/libjson-c.a \ /root/h3-install/lib/libh3.a \ - -I/root/MobilityDB/meos/include \ + -I/root/meos-install/include \ -I/root/geos/include \ -I/root/geos/build/capi \ -I/root/MobilityDB/pgtypes \ diff --git a/codegen/res/bindings_c_header.c.template b/codegen/res/bindings_c_header.c.template index 815c573..ecd4463 100644 --- a/codegen/res/bindings_c_header.c.template +++ b/codegen/res/bindings_c_header.c.template @@ -1,7 +1,21 @@ #include -#include -#include +#include +/* + * stands in for here, and including both breaks it. + * + * The installed is assembled rather than copied from the source tree: + * MEOS splices its PostgreSQL-compat definitions into it under `#ifndef + * POSTGRES_H`, which supplies Datum, DateADT, TimestampTz, Interval and the + * de-prefixed base I/O (interval_in, date_in, timestamptz_in, …) that carry no + * meos_ prefix precisely so they do not collide with a real PostgreSQL. Pulling + * in the vendored first defines POSTGRES_H, the spliced block is + * skipped, and those types and declarations vanish. + * + * So the module compiles against the installed umbrella alone. Only the Datum + * accessors are missing from it, and they are defined below beside the + * Int64GetDatum/Float8GetDatum this file already supplies for the same reason. + */ #include #include #include @@ -14,23 +28,62 @@ #include /* - * Implementations of Int64GetDatum and Float8GetDatum required when - * USE_FLOAT8_BYVAL is disabled (by-reference mode for 64-bit types). + * The Datum accessors PostgreSQL declares in postgres.h. + * + * Whether a 64-bit value rides inside a Datum or behind a pointer is decided by + * the pointer width: PostgreSQL sets USE_FLOAT8_BYVAL when SIZEOF_VOID_P >= 8, + * and Datum is uintptr_t. This module builds with -sMEMORY64=1, so pointers are + * 8 bytes and a TimestampTz or an int64 span bound rides inside the Datum + * itself. Reading such a Datum through a pointer returns whatever the value + * happens to address, which is how a bigint span reports a neighbour it does + * not touch as adjacent. + * + * The condition below reproduces PostgreSQL's, so both widths stay correct. */ -#ifndef USE_FLOAT8_BYVAL -Datum Int64GetDatum(int64 X) { +#include + +typedef double float8; + +#if UINTPTR_MAX > 0xFFFFFFFFu +#define USE_FLOAT8_BYVAL 1 +#endif + +#ifndef PointerGetDatum +#define PointerGetDatum(X) ((Datum) (X)) +#endif + +#ifdef USE_FLOAT8_BYVAL + +#define DatumGetInt64(X) ((int64) (X)) +#define Int64GetDatum(X) ((Datum) (X)) + +static inline Datum Float8GetDatum(float8 X) { + union { float8 value; int64 retval; } myunion; + myunion.value = X; + return (Datum) myunion.retval; +} + +#else + +#define DatumGetInt64(X) (*((int64 *) DatumGetPointer(X))) + +static inline Datum Int64GetDatum(int64 X) { int64 *ptr = (int64 *) malloc(sizeof(int64)); *ptr = X; return PointerGetDatum(ptr); } -Datum Float8GetDatum(float8 X) { +static inline Datum Float8GetDatum(float8 X) { float8 *ptr = (float8 *) malloc(sizeof(float8)); *ptr = X; return PointerGetDatum(ptr); } + #endif +#define DatumGetTimestampTz(X) ((TimestampTz) DatumGetInt64(X)) +#define TimestampTzGetDatum(X) Int64GetDatum(X) + /* --- Error handler --- */ /* * A static C error handler stores the last errlevel/errcode/errmsg produced diff --git a/core/c-src/bindings.c b/core/c-src/bindings.c index f14637c..e1d0fb2 100644 --- a/core/c-src/bindings.c +++ b/core/c-src/bindings.c @@ -1,8 +1,22 @@ /* AUTO-GENERATED - DO NOT EDIT. Run: npm run generate */ #include -#include -#include +#include +/* + * stands in for here, and including both breaks it. + * + * The installed is assembled rather than copied from the source tree: + * MEOS splices its PostgreSQL-compat definitions into it under `#ifndef + * POSTGRES_H`, which supplies Datum, DateADT, TimestampTz, Interval and the + * de-prefixed base I/O (interval_in, date_in, timestamptz_in, …) that carry no + * meos_ prefix precisely so they do not collide with a real PostgreSQL. Pulling + * in the vendored first defines POSTGRES_H, the spliced block is + * skipped, and those types and declarations vanish. + * + * So the module compiles against the installed umbrella alone. Only the Datum + * accessors are missing from it, and they are defined below beside the + * Int64GetDatum/Float8GetDatum this file already supplies for the same reason. + */ #include #include #include @@ -15,23 +29,62 @@ #include /* - * Implementations of Int64GetDatum and Float8GetDatum required when - * USE_FLOAT8_BYVAL is disabled (by-reference mode for 64-bit types). + * The Datum accessors PostgreSQL declares in postgres.h. + * + * Whether a 64-bit value rides inside a Datum or behind a pointer is decided by + * the pointer width: PostgreSQL sets USE_FLOAT8_BYVAL when SIZEOF_VOID_P >= 8, + * and Datum is uintptr_t. This module builds with -sMEMORY64=1, so pointers are + * 8 bytes and a TimestampTz or an int64 span bound rides inside the Datum + * itself. Reading such a Datum through a pointer returns whatever the value + * happens to address, which is how a bigint span reports a neighbour it does + * not touch as adjacent. + * + * The condition below reproduces PostgreSQL's, so both widths stay correct. */ -#ifndef USE_FLOAT8_BYVAL -Datum Int64GetDatum(int64 X) { +#include + +typedef double float8; + +#if UINTPTR_MAX > 0xFFFFFFFFu +#define USE_FLOAT8_BYVAL 1 +#endif + +#ifndef PointerGetDatum +#define PointerGetDatum(X) ((Datum) (X)) +#endif + +#ifdef USE_FLOAT8_BYVAL + +#define DatumGetInt64(X) ((int64) (X)) +#define Int64GetDatum(X) ((Datum) (X)) + +static inline Datum Float8GetDatum(float8 X) { + union { float8 value; int64 retval; } myunion; + myunion.value = X; + return (Datum) myunion.retval; +} + +#else + +#define DatumGetInt64(X) (*((int64 *) DatumGetPointer(X))) + +static inline Datum Int64GetDatum(int64 X) { int64 *ptr = (int64 *) malloc(sizeof(int64)); *ptr = X; return PointerGetDatum(ptr); } -Datum Float8GetDatum(float8 X) { +static inline Datum Float8GetDatum(float8 X) { float8 *ptr = (float8 *) malloc(sizeof(float8)); *ptr = X; return PointerGetDatum(ptr); } + #endif +#define DatumGetTimestampTz(X) ((TimestampTz) DatumGetInt64(X)) +#define TimestampTzGetDatum(X) Int64GetDatum(X) + /* --- Error handler --- */ /* * A static C error handler stores the last errlevel/errcode/errmsg produced From 153b4dcd604ae5c9d68983c44f86cf3e89b92ef2 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sat, 15 Aug 2026 17:37:35 +0200 Subject: [PATCH 2/2] Build the WebAssembly module in CI The workflow regenerates and type-checks, which leaves the C wrappers unbuilt: a wrapper calling a function no header declares passes both steps and fails only in the Docker emscripten build nobody runs. The job builds the module through that Dockerfile target. The MobilityDB clone layer is excluded from the cache, since it clones master and a cached copy would freeze MEOS at whatever commit first populated the cache; the dependency stages build pinned releases and cache safely, which is what makes the job affordable. --- .github/workflows/build.yml | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8181a8e..77e5101 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,3 +36,49 @@ jobs: - name: Type-check run: npx tsc --noEmit + + wasm: + name: Build the WebAssembly module + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + # The MEOS the module embeds comes from a clone of MobilityDB master made + # inside the image, so the clone layer must not be served from the cache: + # a cached mobilitydb_src freezes MEOS at whatever commit first populated + # it, and the tests would then pass against a MEOS nobody is running. The + # dependency stages (sqlite, geos, proj, json-c, gsl, h3) are pure builds + # of pinned releases, so they cache safely and carry the cost of this job. + - name: Build the WebAssembly module + uses: docker/build-push-action@v6 + with: + context: . + target: wasm + outputs: type=local,dest=./wasm + cache-from: type=gha + cache-to: type=gha,mode=max + no-cache-filters: mobilitydb_src + + # `npm test` belongs here and is left out for two reasons, both outside + # what this job builds. + # + # The module is built with -sMEMORY64=1, whose 64-bit tables need a V8 that + # accepts them: Node 22 (V8 12.4) raises "CompileError: invalid table + # elements limits flags" before any test body runs, while Node 24 + # (V8 13.6) instantiates the module and the suite executes. So the test job + # pins node-version 24 or newer, not the 20 above. + # + # The suite then still has failures that predate this workflow — three + # files import interval_make, which libmeos exports but the installed + # umbrella header does not declare, so the generator cannot emit it. That + # gap is upstream in MEOS. Adding the step before it closes would report + # someone else's gap as a failure on every pull request.